feat: 重构 Reminder Notification 系统并更新应用包名
This commit is contained in:
@@ -1,556 +0,0 @@
|
||||
# Calendar Share Redesign - Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 重写日历分享功能,支持选择多个好友/用户并单独设置权限,同时后端新增批量分享接口。
|
||||
|
||||
**Architecture:**
|
||||
- **后端**: 新增 `POST /api/v1/schedule-items/{id}/share/batch` 批量分享接口,原有单用户接口保留。
|
||||
- **前端**: 重构 `CalendarEventShareScreen` 和 `CalendarShareDialog`,移除硬编码手机号输入,改为用户搜索 + 多选 + 独立权限设置。
|
||||
|
||||
**Tech Stack:** Flutter (Riverpod/BLoC), FastAPI (Pydantic), PostgreSQL, 现有 API 契约
|
||||
|
||||
---
|
||||
|
||||
## 背景与现状
|
||||
|
||||
### 现有流程
|
||||
```
|
||||
用户 → CalendarEventShareScreen → CalendarShareDialog
|
||||
├── 手机号输入 + 前缀选择
|
||||
├── 权限开关 (View/Edit/Invite)
|
||||
└── POST /api/v1/schedule-items/{id}/share
|
||||
```
|
||||
|
||||
### 问题
|
||||
1. 仅支持单用户分享
|
||||
2. 需要手动输入手机号,不支持好友选择
|
||||
3. 权限是全局的,无法为不同用户设置不同权限
|
||||
|
||||
### 目标流程
|
||||
```
|
||||
用户 → CalendarEventShareScreen → CalendarShareDialog
|
||||
├── 搜索框 (好友/用户名/手机号搜索)
|
||||
├── 用户列表 (可多选)
|
||||
│ └── 每行: 头像 + 名字 + 权限开关 (选中后显示)
|
||||
├── 已选用户 Chips
|
||||
└── POST /api/v1/schedule-items/{id}/share/batch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端 - 新增批量分享接口
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/src/v1/schedule_items/schemas.py` (新增 `ScheduleItemShareBatchRequest`)
|
||||
- Modify: `backend/src/v1/schedule_items/router.py` (新增 `/share/batch` 端点)
|
||||
- Modify: `backend/src/v1/schedule_items/service.py` (新增 `share_batch` 方法)
|
||||
- Verify: `backend/tests/**`
|
||||
|
||||
- [ ] **Step 1: 创建批量分享请求 Schema**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/schemas.py
|
||||
|
||||
class ScheduleItemShareBatchRequest(BaseModel):
|
||||
shares: List[ShareTarget] = Field(..., description="List of users to share with")
|
||||
permission_view: bool = Field(True, description="Default view permission")
|
||||
permission_edit: bool = Field(False, description="Default edit permission")
|
||||
permission_invite: bool = Field(False, description="Default invite permission")
|
||||
|
||||
class ShareTarget(BaseModel):
|
||||
phone: str = Field(..., pattern=r"^\+861[3-9]\d{9}$")
|
||||
permission_view: Optional[bool] = None
|
||||
permission_edit: Optional[bool] = None
|
||||
permission_invite: Optional[bool] = None
|
||||
|
||||
class ScheduleItemShareBatchResponse(BaseModel):
|
||||
message: str
|
||||
success_count: int
|
||||
failure_count: int
|
||||
failures: List[ShareFailure] = []
|
||||
|
||||
class ShareFailure(BaseModel):
|
||||
phone: str
|
||||
reason: str
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现批量分享服务方法**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/service.py
|
||||
|
||||
async def share_batch(
|
||||
db: AsyncSession,
|
||||
current_user: User,
|
||||
schedule_item_id: UUID,
|
||||
shares: List[ShareTarget],
|
||||
default_permission_view: bool = True,
|
||||
default_permission_edit: bool = False,
|
||||
default_permission_invite: bool = False,
|
||||
) -> Tuple[int, int, List[Dict]]:
|
||||
"""批量分享日程给多个用户"""
|
||||
# 1. 验证当前用户是否有邀请权限
|
||||
# 2. 对每个 share 调用现有的 share_single 逻辑
|
||||
# 3. 返回 (成功数, 失败数, 失败详情列表)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 新增批量分享路由**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/router.py
|
||||
|
||||
@router.post("/{item_id}/share/batch", response_model=ScheduleItemShareBatchResponse)
|
||||
async def share_batch(
|
||||
item_id: UUID,
|
||||
request: ScheduleItemShareBatchRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: ScheduleItemService = Depends(),
|
||||
):
|
||||
"""批量分享日程给多个用户"""
|
||||
success_count, failure_count, failures = await service.share_batch(
|
||||
db=db,
|
||||
current_user=current_user,
|
||||
schedule_item_id=item_id,
|
||||
shares=request.shares,
|
||||
default_permission_view=request.permission_view,
|
||||
default_permission_edit=request.permission_edit,
|
||||
default_permission_invite=request.permission_invite,
|
||||
)
|
||||
return ScheduleItemShareBatchResponse(
|
||||
message="Batch share completed",
|
||||
success_count=success_count,
|
||||
failure_count=failure_count,
|
||||
failures=[ShareFailure(**f) for f in failures],
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写测试**
|
||||
|
||||
```python
|
||||
# backend/tests/v1/test_schedule_items.py
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_share_batch_success():
|
||||
"""测试批量分享成功"""
|
||||
# Arrange
|
||||
# Act
|
||||
# Assert
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_share_batch_partial_failure():
|
||||
"""测试批量分享部分失败(非好友用户应被拒绝)"""
|
||||
# Arrange
|
||||
# Act
|
||||
# Assert
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行测试验证**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest tests/v1/test_schedule_items.py -v -k "share_batch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 前端 - 新增批量分享 API 方法
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/data/apis/calendar_api.dart`
|
||||
|
||||
- [ ] **Step 1: 新增批量分享 API 方法**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/data/apis/calendar_api.dart
|
||||
|
||||
class CalendarApi {
|
||||
// 现有单用户分享保留
|
||||
Future<void> share(...) { ... }
|
||||
|
||||
// 新增批量分享
|
||||
Future<ShareBatchResponse> shareBatch({
|
||||
required String eventId,
|
||||
required List<ShareTarget> shares,
|
||||
bool permissionView = true,
|
||||
bool permissionEdit = false,
|
||||
bool permissionInvite = false,
|
||||
}) async {
|
||||
final response = await _dio.post(
|
||||
'/api/v1/schedule-items/$eventId/share/batch',
|
||||
data: {
|
||||
'shares': shares.map((s) => s.toJson()).toList(),
|
||||
'permission_view': permissionView,
|
||||
'permission_edit': permissionEdit,
|
||||
'permission_invite': permissionInvite,
|
||||
},
|
||||
);
|
||||
return ShareBatchResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareTarget {
|
||||
final String phone;
|
||||
final bool? permissionView;
|
||||
final bool? permissionEdit;
|
||||
final bool? permissionInvite;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'phone': phone,
|
||||
if (permissionView != null) 'permission_view': permissionView,
|
||||
if (permissionEdit != null) 'permission_edit': permissionEdit,
|
||||
if (permissionInvite != null) 'permission_invite': permissionInvite,
|
||||
};
|
||||
}
|
||||
|
||||
class ShareBatchResponse {
|
||||
final String message;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final List<ShareFailure> failures;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 API 编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/data/apis/calendar_api.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 前端 - 创建分享页面状态管理
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart`
|
||||
- Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart`
|
||||
|
||||
- [ ] **Step 1: 创建 State**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart
|
||||
|
||||
enum CalendarShareStatus { initial, loading, success, failure }
|
||||
|
||||
class SelectedUser {
|
||||
final String id;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
final String phone;
|
||||
bool permissionView;
|
||||
bool permissionEdit;
|
||||
bool permissionInvite;
|
||||
}
|
||||
|
||||
class CalendarShareState {
|
||||
final CalendarShareStatus status;
|
||||
final List<FriendResponse> friends;
|
||||
final List<UserProfile> searchResults;
|
||||
final List<SelectedUser> selectedUsers;
|
||||
final String searchQuery;
|
||||
final String? errorMessage;
|
||||
|
||||
// 权限默认值(应用于新选中的用户)
|
||||
bool defaultPermissionView;
|
||||
bool defaultPermissionEdit;
|
||||
bool defaultPermissionInvite;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 Cubit**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart
|
||||
|
||||
class CalendarShareCubit extends Cubit<CalendarShareState> {
|
||||
final FriendsApi _friendsApi;
|
||||
final UsersApi _usersApi;
|
||||
final CalendarApi _calendarApi;
|
||||
|
||||
CalendarShareCubit({
|
||||
required FriendsApi friendsApi,
|
||||
required UsersApi usersApi,
|
||||
required CalendarApi calendarApi,
|
||||
}) : super(CalendarShareState.initial());
|
||||
|
||||
// 加载好友列表
|
||||
Future<void> loadFriends() async { ... }
|
||||
|
||||
// 搜索用户
|
||||
Future<void> searchUsers(String query) async { ... }
|
||||
|
||||
// 切换用户选中状态
|
||||
void toggleUser(String userId) { ... }
|
||||
|
||||
// 更新单个用户的权限
|
||||
void updateUserPermission(String userId, {bool? view, bool? edit, bool? invite}) { ... }
|
||||
|
||||
// 移除已选用户
|
||||
void removeUser(String userId) { ... }
|
||||
|
||||
// 设置默认权限(对新选用户生效)
|
||||
void setDefaultPermissions({bool? view, bool? edit, bool? invite}) { ... }
|
||||
|
||||
// 发送批量分享
|
||||
Future<void> share(String eventId) async { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 验证编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/presentation/cubits/calendar_share_cubit.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 前端 - 重构分享对话框 UI
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/presentation/widgets/user_select_list.dart`
|
||||
- Create: `apps/lib/features/calendar/presentation/widgets/user_select_item.dart`
|
||||
- Modify: `apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart`
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart`
|
||||
|
||||
### 4.1 用户选择列表组件
|
||||
|
||||
- [ ] **Step 1: 创建 UserSelectItem**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/widgets/user_select_item.dart
|
||||
|
||||
class UserSelectItem extends StatelessWidget {
|
||||
final UserBasicInfo user;
|
||||
final bool isSelected;
|
||||
final bool showPermissions; // 选中后才显示权限开关
|
||||
final SelectedUser? selectedUser; // 如果选中,包含权限状态
|
||||
final ValueChanged<bool> onToggle;
|
||||
final Function(PermissionType, bool) onPermissionChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Avatar(url: user.avatarUrl, name: user.username),
|
||||
title: Text(user.username),
|
||||
subtitle: showPermissions && isSelected
|
||||
? Row(
|
||||
children: [
|
||||
_PermissionChip('V', selectedUser?.permissionView ?? true, (v) => onPermissionChanged(PermissionType.view, v)),
|
||||
_PermissionChip('E', selectedUser?.permissionEdit ?? false, (v) => onPermissionChanged(PermissionType.edit, v)),
|
||||
_PermissionChip('I', selectedUser?.permissionInvite ?? false, (v) => onPermissionChanged(PermissionType.invite, v)),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
trailing: Checkbox(value: isSelected, onChanged: (v) => onToggle(v ?? false)),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 重构 CalendarShareDialog
|
||||
|
||||
- [ ] **Step 2: 重构对话框**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart
|
||||
|
||||
class CalendarShareDialog extends StatefulWidget {
|
||||
// ... existing props
|
||||
|
||||
@override
|
||||
State<CalendarShareDialog> createState() => _CalendarShareDialogState();
|
||||
}
|
||||
|
||||
class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
late final CalendarShareCubit _cubit;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cubit = context.read<CalendarShareCubit>();
|
||||
_cubit.loadFriends();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cubit,
|
||||
child: BlocBuilder<CalendarShareCubit, CalendarShareState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context, state),
|
||||
|
||||
// 搜索框
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索好友或输入手机号...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onChanged: (value) => _cubit.searchUsers(value),
|
||||
),
|
||||
|
||||
// 已选用户 Chips
|
||||
if (state.selectedUsers.isNotEmpty) ...[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: state.selectedUsers.map((user) {
|
||||
return Chip(
|
||||
label: Text('${user.username} (${_getPermissionSummary(user)})'),
|
||||
onDeleted: () => _cubit.removeUser(user.id),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
|
||||
// 用户列表
|
||||
Flexible(
|
||||
child: _buildUserList(state),
|
||||
),
|
||||
|
||||
// 发送按钮
|
||||
AppButton(
|
||||
text: '发送邀请 (${state.selectedUsers.length}人)',
|
||||
onPressed: state.selectedUsers.isEmpty
|
||||
? null
|
||||
: () => _handleShare(context, state),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserList(CalendarShareState state) {
|
||||
// 如果有搜索结果,显示搜索结果;否则显示好友列表
|
||||
final users = state.searchQuery.isNotEmpty
|
||||
? state.searchResults
|
||||
: state.friends.map((f) => f.friend).toList();
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
final selectedUser = state.selectedUsers.cast<SelectedUser?>().firstWhere(
|
||||
(s) => s?.id == user.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
final isSelected = selectedUser != null;
|
||||
|
||||
return UserSelectItem(
|
||||
user: user,
|
||||
isSelected: isSelected,
|
||||
showPermissions: isSelected,
|
||||
selectedUser: selectedUser,
|
||||
onToggle: (selected) {
|
||||
if (selected) {
|
||||
_cubit.addUser(user, state.defaultPermissionView, state.defaultPermissionEdit, state.defaultPermissionInvite);
|
||||
} else {
|
||||
_cubit.removeUser(user.id);
|
||||
}
|
||||
},
|
||||
onPermissionChanged: (type, value) {
|
||||
_cubit.updateUserPermission(user.id, type: type, value: value);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 移除顶部栏,简化 CalendarEventShareScreen**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart
|
||||
|
||||
class CalendarEventShareScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 直接显示分享对话框,不需要额外顶部栏
|
||||
Expanded(
|
||||
child: CalendarShareDialog(
|
||||
eventId: eventId,
|
||||
eventTitle: event.title,
|
||||
canInvite: event.canInvite,
|
||||
canEdit: event.canEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 验证编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/presentation/widgets/calendar_share_dialog.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart`
|
||||
|
||||
- [ ] **Step 1: 编写 Widget 测试**
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
testWidgets('选择用户后显示权限开关', (tester) async {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
|
||||
testWidgets('已选用户显示在 Chips 中', (tester) async {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试**
|
||||
|
||||
```bash
|
||||
cd apps && flutter test test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Task 1 (后端批量接口)
|
||||
↓
|
||||
Task 2 (前端批量分享 API)
|
||||
↓
|
||||
Task 3 (分享状态管理 Cubit)
|
||||
↓
|
||||
Task 4 (分享对话框 UI)
|
||||
↓
|
||||
Task 5 (集成测试)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **后端兼容性**: 批量接口上线前,前端使用原有单用户接口
|
||||
2. **搜索防抖**: 搜索用户时需要 debounce 避免频繁请求
|
||||
3. **好友列表分页**: 如果好友数量大,需要分页加载
|
||||
4. **权限验证**: 前端权限不能超过当前用户的权限(由后端控制)
|
||||
@@ -0,0 +1,74 @@
|
||||
# Agent Calendar/Inbox Stability Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 修复 agent 日历分享失败、对话后日历不刷新、邀请信息不完整,并新增设置页一键清理本地缓存后强制重新拉取。
|
||||
|
||||
**Architecture:** 后端补齐 `calendar_share` 依赖注入与邀请 payload 字段,确保工具链与 API 路由行为一致。前端在 Chat 工具结果事件上增加日历刷新钩子,并在设置页提供缓存清理入口,复用既有 prewarm 机制触发重新拉取。协议文档同步更新 inbox 邀请结构,避免前后端契约漂移。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy + AgentScope tools, Flutter + CachedRepository + SharedPreferences cache.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 修复 calendar_share 在 Agent 工具链中的依赖缺失
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/core/agentscope/tools/utils/calendar_domain.py`
|
||||
|
||||
**Steps:**
|
||||
1. 在 `create_schedule_service` 注入 `SQLAlchemyUserRepository`。
|
||||
2. 保持路由层与工具层对 `ScheduleItemService` 的依赖一致。
|
||||
3. 回归验证 `calendar_share` 不再因为 actor lookup 依赖缺失而失败。
|
||||
|
||||
### Task 2: 扩充 calendar invite payload(邀请人 + 时间 + 描述)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/service.py`
|
||||
- Modify: `docs/protocols/models/inbox-messages.md`
|
||||
- Test: `backend/tests/unit/v1/schedule_items/test_share.py`
|
||||
|
||||
**Steps:**
|
||||
1. 在 `share` 中构建邀请消息时写入 `actor.phone`。
|
||||
2. 在 `item` 中写入 `description/start_at/end_at/timezone`。
|
||||
3. 更新协议文档 `CalendarInviteContent`。
|
||||
4. 补充/更新单测断言新增字段。
|
||||
|
||||
### Task 3: 对话工具成功后触发日历缓存刷新钩子
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/chat/presentation/bloc/chat_bloc.dart`
|
||||
- Modify: `apps/lib/features/chat/presentation/bloc/chat_bloc_events.dart`
|
||||
- Modify: `apps/lib/app/di/injection.dart`
|
||||
- Test: `apps/test/features/chat/presentation/bloc/chat_bloc_test.dart`
|
||||
|
||||
**Steps:**
|
||||
1. 给 `ChatBloc` 增加可注入回调 `onCalendarMutated`。
|
||||
2. 在 `ToolCallResultEvent` 中识别 `calendar_write` 成功/部分成功并触发回调。
|
||||
3. DI 中将回调绑定为 `CalendarRepository.getDayEvents/getMonthEvents(forceRefresh: true)`。
|
||||
4. 添加回归测试验证回调触发。
|
||||
|
||||
### Task 4: 设置页新增“清理缓存”并触发重新拉取
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/data/cache/cache_store.dart`
|
||||
- Modify: `apps/lib/features/settings/presentation/screens/settings_screen.dart`
|
||||
- Modify: `apps/lib/l10n/app_zh.arb`
|
||||
- Modify: `apps/lib/l10n/app_en.arb`
|
||||
|
||||
**Steps:**
|
||||
1. 在 `HybridCacheStore` 增加按前缀清理能力(`cache:`)。
|
||||
2. 设置页在“检查更新”下新增“清理缓存”。
|
||||
3. 点击后清理缓存并触发 prewarm + inbox 快照刷新。
|
||||
4. 同步中英文文案并生成本地化代码。
|
||||
|
||||
### Task 5: 验证
|
||||
|
||||
**Commands:**
|
||||
- `uv run pytest backend/tests/unit/v1/schedule_items/test_share.py backend/tests/unit/core/agentscope/test_calendar_tools.py -k "share or calendar_share"`
|
||||
- `flutter test test/features/chat/presentation/bloc/chat_bloc_test.dart`
|
||||
- `flutter analyze`
|
||||
|
||||
**Expected:**
|
||||
- 后端分享链路测试通过,新增邀请字段存在。
|
||||
- ChatBloc 回归测试通过,`calendar_write` 成功时触发刷新回调。
|
||||
- Flutter 静态检查通过,无新增错误。
|
||||
@@ -1,214 +0,0 @@
|
||||
# Calendar Detail - Show Subscribers Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在日历详情页显示已订阅此日历的用户列表
|
||||
|
||||
**Architecture:**
|
||||
- 后端:在 `GET /api/v1/schedule-items/{id}` 响应中返回 `subscribers` 列表
|
||||
- 前端:在详情页渲染订阅者列表组件
|
||||
|
||||
**Tech Stack:** Flutter, FastAPI, PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端 - 返回订阅者列表
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/schemas.py`
|
||||
- Modify: `backend/src/v1/schedule_items/service.py`
|
||||
- Verify: `backend/tests/integration/v1/test_schedule_items_routes.py`
|
||||
|
||||
- [ ] **Step 1: 新增 SubscriberInfo Schema**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/schemas.py
|
||||
|
||||
class SubscriberInfo(BaseModel):
|
||||
"""订阅者信息"""
|
||||
user_id: UUID
|
||||
username: str
|
||||
avatar_url: str | None = None
|
||||
permission: int # 位标志: 1=view, 2=invite, 4=edit
|
||||
status: str # active, pending, paused, unsubscribed
|
||||
subscribed_at: datetime
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 ScheduleItemResponse**
|
||||
|
||||
```python
|
||||
class ScheduleItemResponse(BaseModel):
|
||||
# ... existing fields ...
|
||||
subscribers: List[SubscriberInfo] = [] # 新增
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 service.get_by_id 填充 subscribers**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/service.py
|
||||
|
||||
async def get_by_id(self, item_id: UUID) -> ScheduleItemResponse:
|
||||
item = await self._repository.get_by_id(item_id)
|
||||
if not item:
|
||||
raise ScheduleItemNotFoundError(item_id)
|
||||
|
||||
# 获取订阅者列表
|
||||
subscriptions = await self._repository.get_subscriptions_by_item_id(item_id)
|
||||
subscribers = []
|
||||
for sub in subscriptions:
|
||||
if sub.status == 'active': # 只返回活跃订阅者
|
||||
user = await self._user_repo.get_by_id(sub.subscriber_id)
|
||||
if user:
|
||||
subscribers.append(SubscriberInfo(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
avatar_url=user.avatar_url,
|
||||
permission=sub.permission,
|
||||
status=sub.status,
|
||||
subscribed_at=sub.created_at,
|
||||
))
|
||||
|
||||
return ScheduleItemResponse(
|
||||
# ... existing fields ...,
|
||||
subscribers=subscribers,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写集成测试**
|
||||
|
||||
```python
|
||||
# backend/tests/integration/v1/test_schedule_items_routes.py
|
||||
|
||||
async def test_get_item_returns_subscribers(self):
|
||||
"""测试获取日程时返回订阅者列表"""
|
||||
# Arrange: 创建日程,添加订阅者
|
||||
# Act: GET /api/v1/schedule-items/{id}
|
||||
# Assert: 响应包含 subscribers 字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 前端 - Model 更新
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
|
||||
|
||||
- [ ] **Step 1: 新增 Subscriber 模型**
|
||||
|
||||
```dart
|
||||
class Subscriber {
|
||||
final String userId;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
final int permission;
|
||||
final String status;
|
||||
final DateTime subscribedAt;
|
||||
|
||||
bool get canView => (permission & 1) != 0;
|
||||
bool get canEdit => (permission & 4) != 0;
|
||||
bool get canInvite => (permission & 2) != 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 ScheduleItemModel 中添加 subscribers 字段**
|
||||
|
||||
```dart
|
||||
class ScheduleItemModel {
|
||||
// ... existing fields ...
|
||||
final List<Subscriber> subscribers;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 更新 fromJson**
|
||||
|
||||
```dart
|
||||
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScheduleItemModel(
|
||||
// ... existing fields ...,
|
||||
subscribers: (json['subscribers'] as List<dynamic>?)
|
||||
?.map((s) => Subscriber.fromJson(s as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 前端 - 详情页渲染订阅者
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart`
|
||||
|
||||
- [ ] **Step 1: 在 _buildMetaSurface 或新方法中渲染订阅者**
|
||||
|
||||
```dart
|
||||
Widget _buildSubscribersSurface(ScheduleItemModel event) {
|
||||
if (event.subscribers.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: _colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: _colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailSubscribers,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
...event.subscribers.map((sub) => _buildSubscriberRow(sub)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubscriberRow(Subscriber subscriber) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
|
||||
child: Row(
|
||||
children: [
|
||||
Avatar(url: subscriber.avatarUrl, name: subscriber.username),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(child: Text(subscriber.username)),
|
||||
if (subscriber.canEdit) Icon(Icons.edit, size: 16),
|
||||
if (subscriber.canInvite) Icon(Icons.person_add, size: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 build 方法中添加**
|
||||
|
||||
```dart
|
||||
// 在 _buildExtraSurface 之后添加
|
||||
if (event.subscribers.isNotEmpty) [
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildSubscribersSurface(event),
|
||||
],
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 添加 l10n 文本**
|
||||
|
||||
```arb
|
||||
// apps/lib/l10n/app_zh.arb
|
||||
"calendarDetailSubscribers": "已订阅 ({count}人)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 验证
|
||||
|
||||
- [ ] 运行后端测试: `cd backend && uv run pytest -v -k "subscriber"`
|
||||
- [ ] 运行前端分析: `cd apps && flutter analyze`
|
||||
- [ ] 手动测试分享流程
|
||||
@@ -1,473 +0,0 @@
|
||||
# Calendar Permission Refactoring Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 统一权限判断逻辑,将 `is_owner` 和 `permission` 的双重判断合并为纯 `permission` 判断,并新增 Owner 徽章 UI。
|
||||
|
||||
**Architecture:**
|
||||
- 后端:扩展权限位掩码,添加 DELETE=8,OWNER=15。简化 Repository 和 Service 层的重复方法。
|
||||
- 前端:Model 层 `canEdit`/`canDelete` 改为纯 permission 判断,`isOwner` 仅保留用于 UI 徽章显示。
|
||||
- Protocol:更新位掩码说明文档。
|
||||
|
||||
**Tech Stack:** Python (FastAPI/SQLAlchemy), Flutter, Supabase
|
||||
|
||||
---
|
||||
|
||||
## 变更范围总结
|
||||
|
||||
### 后端 (5 个文件)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `backend/src/schemas/enums.py` | `DELETE=8`, `OWNER=15` |
|
||||
| `backend/src/v1/schedule_items/service.py` | 移除双重判断,统一 permission 逻辑 |
|
||||
| `backend/src/v1/schedule_items/repository.py` | 合并 `get_by_item_id` + `update_by_item_id` → 统一方法 |
|
||||
| `backend/tests/integration/test_schedule_items_routes.py` | `permission=7` → `permission=15` |
|
||||
| `docs/protocols/calendar/schedule-items.md` | 更新位掩码说明 |
|
||||
|
||||
### 前端 (4 个文件)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `apps/lib/features/calendar/data/models/schedule_item_model.dart` | `canEdit/canDelete` 改为纯 permission |
|
||||
| `apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart` | 添加 Owner 徽章 |
|
||||
| `apps/lib/l10n/app_zh.arb` | 新增 owner badge 文案 |
|
||||
| `apps/lib/l10n/app_en.arb` | 新增 owner badge 文案 |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端 - 更新权限枚举
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/schemas/enums.py:96-100`
|
||||
|
||||
- [ ] **Step 1: 更新 SubscriptionPermission 枚举**
|
||||
|
||||
```python
|
||||
class SubscriptionPermission(int, Enum):
|
||||
VIEW = 1
|
||||
INVITE = 2
|
||||
EDIT = 4
|
||||
DELETE = 8
|
||||
OWNER = 15 # VIEW | INVITE | EDIT | DELETE
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行语法检查**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run python -m py_compile src/schemas/enums.py
|
||||
```
|
||||
|
||||
Expected: 无错误输出
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 后端 - 简化 Repository 层
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/repository.py`
|
||||
|
||||
**原始方法 (需合并/删除):**
|
||||
- `get_by_item_id(item_id, owner_id)` - 仅用于 owner 判断,可删除
|
||||
- `update_by_item_id(item_id, owner_id, data)` - 仅用于 owner 更新,可删除
|
||||
- `update_item_by_id(item_id, data)` - 保留,订阅者更新用
|
||||
|
||||
**新逻辑:**
|
||||
- `get_item(item_id)` - 获取 item,不带 owner 过滤
|
||||
- `update_item(item_id, data)` - 统一更新,不带 owner 过滤(权限判断移至 Service 层)
|
||||
- `delete_item(item_id)` - 统一删除,不带 owner 过滤(权限判断移至 Service 层)
|
||||
|
||||
- [ ] **Step 1: 添加统一方法**
|
||||
|
||||
在 `SQLAlchemyScheduleItemRepository` 类中添加:
|
||||
|
||||
```python
|
||||
async def get_item(self, item_id: UUID) -> ScheduleItem | None:
|
||||
"""Get item by id without owner filter. Permission check done at service layer."""
|
||||
try:
|
||||
stmt = (
|
||||
select(ScheduleItem)
|
||||
.where(ScheduleItem.id == item_id)
|
||||
.where(ScheduleItem.deleted_at.is_(None))
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Schedule item lookup failed", item_id=str(item_id))
|
||||
raise
|
||||
|
||||
async def update_item(self, item_id: UUID, data: dict) -> ScheduleItem | None:
|
||||
"""Update item by id without owner filter. Permission check done at service layer."""
|
||||
if not data:
|
||||
return await self.get_item(item_id)
|
||||
try:
|
||||
existing = await self.get_item(item_id)
|
||||
if existing is None:
|
||||
return None
|
||||
stmt = (
|
||||
update(ScheduleItem)
|
||||
.where(ScheduleItem.id == item_id)
|
||||
.where(ScheduleItem.deleted_at.is_(None))
|
||||
.values(**data)
|
||||
.returning(ScheduleItem)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Schedule item update failed", item_id=str(item_id))
|
||||
raise
|
||||
|
||||
async def delete_item(self, item_id: UUID) -> ScheduleItem | None:
|
||||
"""Soft delete item by id without owner filter. Permission check done at service layer."""
|
||||
try:
|
||||
stmt = (
|
||||
update(ScheduleItem)
|
||||
.where(ScheduleItem.id == item_id)
|
||||
.where(ScheduleItem.deleted_at.is_(None))
|
||||
.values(deleted_at=datetime.now(timezone.utc))
|
||||
.returning(ScheduleItem)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Schedule item delete failed", item_id=str(item_id))
|
||||
raise
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新 Repository Protocol**
|
||||
|
||||
修改 `ScheduleItemRepository` Protocol 定义:
|
||||
|
||||
```python
|
||||
class ScheduleItemRepository(Protocol):
|
||||
async def get_by_id(self, entity_id: UUID) -> ScheduleItem | None: ...
|
||||
async def get_item(self, item_id: UUID) -> ScheduleItem | None: ... # 新增
|
||||
async def get_subscription(self, item_id: UUID, subscriber_id: UUID) -> ScheduleSubscription | None: ...
|
||||
async def create(self, data: dict) -> ScheduleItem: ...
|
||||
async def update_item(self, item_id: UUID, data: dict) -> ScheduleItem | None: ... # 合并
|
||||
async def delete_item(self, item_id: UUID) -> ScheduleItem | None: ... # 合并
|
||||
# ... 其他方法保持不变
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行语法检查**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run python -m py_compile src/v1/schedule_items/repository.py
|
||||
```
|
||||
|
||||
Expected: 无错误输出
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 后端 - 简化 Service 层
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/service.py`
|
||||
|
||||
### 3.1 get_by_id 方法简化
|
||||
|
||||
**原始逻辑 (lines 175-180):**
|
||||
```python
|
||||
is_owner = item.owner_id == user_id
|
||||
permission = 1
|
||||
if not is_owner:
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
if subscription:
|
||||
permission = subscription.permission
|
||||
```
|
||||
|
||||
**新逻辑:**
|
||||
```python
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
permission = subscription.permission if subscription else 1
|
||||
is_owner = item.owner_id == user_id
|
||||
```
|
||||
|
||||
### 3.2 update 方法简化
|
||||
|
||||
**原始逻辑 (lines 231-264):**
|
||||
```python
|
||||
existing = await self._repository.get_by_item_id(item_id, user_id)
|
||||
is_owner = existing is not None
|
||||
|
||||
if not is_owner:
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
if subscription is None or subscription.status != SubscriptionStatus.ACTIVE:
|
||||
raise 404
|
||||
if not (subscription.permission & SubscriptionPermission.EDIT):
|
||||
raise 403
|
||||
existing = await self._repository.get_by_id(item_id)
|
||||
...
|
||||
# owner 用 update_by_item_id,订阅者用 update_item_by_id
|
||||
if is_owner:
|
||||
item = await self._repository.update_by_item_id(item_id, user_id, update_data)
|
||||
else:
|
||||
item = await self._repository.update_item_by_id(item_id, update_data)
|
||||
```
|
||||
|
||||
**新逻辑:**
|
||||
```python
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
if subscription is None or subscription.status != SubscriptionStatus.ACTIVE:
|
||||
raise 404
|
||||
if not (subscription.permission & SubscriptionPermission.EDIT):
|
||||
raise 403
|
||||
# 统一用 update_item
|
||||
item = await self._repository.update_item(item_id, update_data)
|
||||
is_owner = item.owner_id == user_id if item else False
|
||||
```
|
||||
|
||||
### 3.3 delete 方法简化
|
||||
|
||||
**原始逻辑 (lines 338-366):**
|
||||
```python
|
||||
existing = await self._repository.get_by_item_id(item_id, user_id)
|
||||
if existing is None:
|
||||
raise 404
|
||||
...
|
||||
await self._repository.delete_by_item_id(item_id, user_id)
|
||||
```
|
||||
|
||||
**新逻辑:**
|
||||
```python
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
if subscription is None or not (subscription.permission & SubscriptionPermission.DELETE):
|
||||
raise 403
|
||||
item = await self._repository.delete_item(item_id)
|
||||
if item is None:
|
||||
raise 404
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 修改 get_by_id (lines 175-180)**
|
||||
|
||||
```python
|
||||
# 替换为:
|
||||
subscription = await self._repository.get_subscription(item_id, user_id)
|
||||
permission = subscription.permission if subscription else 1
|
||||
is_owner = item.owner_id == user_id
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 update 方法 (lines 226-336)**
|
||||
|
||||
简化权限检查逻辑,统一使用 `update_item` 方法
|
||||
|
||||
- [ ] **Step 3: 修改 delete 方法 (lines 338-366)**
|
||||
|
||||
简化权限检查逻辑,统一使用 `delete_item` 方法
|
||||
|
||||
- [ ] **Step 4: 更新 _to_response 响应方法 (line 645)**
|
||||
|
||||
```python
|
||||
# 原来
|
||||
permission=permission if not is_owner else 7,
|
||||
# 改为 (如果 subscription 有值就用其 permission,否则默认 1)
|
||||
permission=subscription.permission if subscription else 1,
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行语法检查**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run python -m py_compile src/v1/schedule_items/service.py
|
||||
```
|
||||
|
||||
Expected: 无错误输出
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 后端 - 更新测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/tests/integration/test_schedule_items_routes.py`
|
||||
|
||||
- [ ] **Step 1: 更新所有 permission=7 为 permission=15**
|
||||
|
||||
```bash
|
||||
# 使用 replaceAll 功能
|
||||
# 将 permission=7 替换为 permission=15
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试验证**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run pytest tests/integration/test_schedule_items_routes.py -v
|
||||
```
|
||||
|
||||
Expected: 所有测试通过
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 前端 - 简化 Model 权限判断
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
|
||||
|
||||
- [ ] **Step 1: 添加 DELETE 权限常量**
|
||||
|
||||
```dart
|
||||
static const int permissionDelete = 8;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 canEdit/canDelete**
|
||||
|
||||
```dart
|
||||
// 原来
|
||||
bool get canEdit => isOwner || (permission & permissionEdit) != 0;
|
||||
bool get canDelete => isOwner;
|
||||
|
||||
// 改为
|
||||
bool get canEdit => (permission & permissionEdit) != 0;
|
||||
bool get canDelete => (permission & permissionDelete) != 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/apps && flutter analyze lib/features/calendar/data/models/schedule_item_model.dart
|
||||
```
|
||||
|
||||
Expected: No issues found
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 前端 - 添加 Owner 徽章
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart`
|
||||
|
||||
- [ ] **Step 1: 在标题栏添加 Owner 徽章**
|
||||
|
||||
找到标题显示的位置(约 line 250-280),在标题后添加:
|
||||
|
||||
```dart
|
||||
if (event.isOwner)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.calendarOwnerBadge,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 flutter analyze**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/apps && flutter analyze lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart
|
||||
```
|
||||
|
||||
Expected: No issues found
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 前端 - 添加 L10n 文案
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/l10n/app_zh.arb`
|
||||
- Modify: `apps/lib/l10n/app_en.arb`
|
||||
|
||||
- [ ] **Step 1: 添加中文文案**
|
||||
|
||||
在 `app_zh.arb` 末尾添加:
|
||||
|
||||
```json
|
||||
"calendarOwnerBadge": "我的日历",
|
||||
"@calendarOwnerBadge": {
|
||||
"description": "Owner badge shown when user owns the calendar"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加英文文案**
|
||||
|
||||
在 `app_en.arb` 末尾添加:
|
||||
|
||||
```json
|
||||
"calendarOwnerBadge": "My Calendar",
|
||||
"@calendarOwnerBadge": {
|
||||
"description": "Owner badge shown when user owns the calendar"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 生成 l10n**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/apps && flutter gen-l10n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: 更新 Protocol 文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/protocols/calendar/schedule-items.md`
|
||||
|
||||
- [ ] **Step 1: 更新 SubscriberInfo 的 permission 说明**
|
||||
|
||||
找到 SubscriberInfo 部分,添加 DELETE 权限说明:
|
||||
|
||||
```
|
||||
permission: "int (位掩码: 1=view, 2=invite, 4=edit, 8=delete)"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新 PATCH `/{item_id}` 的 Authorization 说明**
|
||||
|
||||
```
|
||||
- **Owner**: 可更新所有字段 (permission=15)
|
||||
- **Subscriber (DELETE permission)**: 可删除日程(权限位掩码包含 `8`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: 最终验证
|
||||
|
||||
- [ ] **Step 1: 后端语法检查**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run python -m py_compile src/schemas/enums.py src/v1/schedule_items/repository.py src/v1/schedule_items/service.py
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 后端测试**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/backend && uv run pytest tests/integration/test_schedule_items_routes.py -v
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 前端分析**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app/apps && flutter analyze lib/features/calendar
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Git 状态检查**
|
||||
|
||||
```bash
|
||||
cd /Users/zl-q/Code/social-app && git status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚计划
|
||||
|
||||
如果出现问题:
|
||||
|
||||
1. **后端回滚**: `git checkout HEAD~1 -- backend/`
|
||||
2. **前端回滚**: `git checkout HEAD~1 -- apps/`
|
||||
3. **Protocol 回滚**: `git checkout HEAD~1 -- docs/`
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. Repository 层的方法合并后,Service 层必须确保权限判断正确
|
||||
2. Owner 创建日程时,subscription.permission 应该设置为 15 (OWNER)
|
||||
3. 测试中的 permission=7 需要全部更新为 15
|
||||
4. 前端 isOwner 字段仍保留,但仅用于 UI 显示,不参与权限判断
|
||||
Reference in New Issue
Block a user