13 KiB
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 枚举
class SubscriptionPermission(int, Enum):
VIEW = 1
INVITE = 2
EDIT = 4
DELETE = 8
OWNER = 15 # VIEW | INVITE | EDIT | DELETE
- Step 2: 运行语法检查
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 类中添加:
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 定义:
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: 运行语法检查
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):
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
新逻辑:
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):
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)
新逻辑:
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):
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)
新逻辑:
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)
# 替换为:
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)
# 原来
permission=permission if not is_owner else 7,
# 改为 (如果 subscription 有值就用其 permission,否则默认 1)
permission=subscription.permission if subscription else 1,
- Step 5: 运行语法检查
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
# 使用 replaceAll 功能
# 将 permission=7 替换为 permission=15
- Step 2: 运行测试验证
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 权限常量
static const int permissionDelete = 8;
- Step 2: 修改 canEdit/canDelete
// 原来
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
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),在标题后添加:
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
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 末尾添加:
"calendarOwnerBadge": "我的日历",
"@calendarOwnerBadge": {
"description": "Owner badge shown when user owns the calendar"
}
- Step 2: 添加英文文案
在 app_en.arb 末尾添加:
"calendarOwnerBadge": "My Calendar",
"@calendarOwnerBadge": {
"description": "Owner badge shown when user owns the calendar"
}
- Step 3: 生成 l10n
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: 后端语法检查
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: 后端测试
cd /Users/zl-q/Code/social-app/backend && uv run pytest tests/integration/test_schedule_items_routes.py -v
- Step 3: 前端分析
cd /Users/zl-q/Code/social-app/apps && flutter analyze lib/features/calendar
- Step 4: Git 状态检查
cd /Users/zl-q/Code/social-app && git status
回滚计划
如果出现问题:
- 后端回滚:
git checkout HEAD~1 -- backend/ - 前端回滚:
git checkout HEAD~1 -- apps/ - Protocol 回滚:
git checkout HEAD~1 -- docs/
注意事项
- Repository 层的方法合并后,Service 层必须确保权限判断正确
- Owner 创建日程时,subscription.permission 应该设置为 15 (OWNER)
- 测试中的 permission=7 需要全部更新为 15
- 前端 isOwner 字段仍保留,但仅用于 UI 显示,不参与权限判断