Files
social-app/docs/plans/2026-03-30-calendar-permission-refactor.md
T

13 KiB
Raw Blame History

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_ownerpermission 的双重判断合并为纯 permission 判断,并新增 Owner 徽章 UI。

Architecture:

  • 后端:扩展权限位掩码,添加 DELETE=8OWNER=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=7permission=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

回滚计划

如果出现问题:

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