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

474 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 显示,不参与权限判断