docs: 更新协议文档并清理过时计划文件

This commit is contained in:
zl-q
2026-03-30 09:07:07 +08:00
parent 5999d0edd1
commit 0f3175e303
13 changed files with 1359 additions and 842 deletions
@@ -0,0 +1,473 @@
# 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=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=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 显示,不参与权限判断