# 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 显示,不参与权限判断