docs: 更新协议文档并清理过时计划文件
This commit is contained in:
@@ -1,178 +0,0 @@
|
||||
# Apps 通用数据采集与报错日志系统设计(一阶段)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前前端需要在应用分发后具备可观测性能力,以支持线上报错定位和关键行为分析。
|
||||
|
||||
一阶段目标聚焦于“最小可用且可扩展”的通用采集体系,覆盖:
|
||||
|
||||
- 报错日志收集与排查支持。
|
||||
- 用户打开应用时间与会话持续时长。
|
||||
- 页面停留时长。
|
||||
- 对话输入耗时与发送次数。
|
||||
|
||||
本设计优先保证数据质量、隐私安全、稳定性与后续重构兼容性,不绑定当前目录结构和具体实现文件。
|
||||
|
||||
## 2. 设计范围
|
||||
|
||||
### 2.1 一阶段纳入
|
||||
|
||||
- 全局异常采集(框架异常、异步未捕获异常、业务显式上报异常)。
|
||||
- 会话生命周期采集(开始、结束、时长)。
|
||||
- 页面生命周期采集(进入、离开、停留时长)。
|
||||
- 对话输入行为采集(输入开始、输入提交)。
|
||||
|
||||
### 2.2 一阶段不纳入
|
||||
|
||||
- 复杂埋点体系(曝光、点击流全量追踪、实验分流)。
|
||||
- 全量性能指标体系(FPS、内存、卡顿详细分层)。
|
||||
- 非关键业务域的大规模事件扩展。
|
||||
|
||||
## 3. 方案选型结论
|
||||
|
||||
采用“自研通用采集 SDK + 自有后端接收”的主路径。
|
||||
|
||||
原因:
|
||||
|
||||
- 关键数据字段需与业务强关联,需高可定制能力。
|
||||
- 需要强约束隐私边界与脱敏策略。
|
||||
- 一阶段目标明确且有限,自研成本可控。
|
||||
- 后续可平滑扩展第三方崩溃平台作为补充,不影响主链路。
|
||||
|
||||
## 4. 总体架构
|
||||
|
||||
系统分为四层:
|
||||
|
||||
1. 采集层:负责统一接入错误、会话、页面、输入等事件源。
|
||||
2. 处理层:负责事件标准化、上下文补全、脱敏、去重与采样。
|
||||
3. 存储层:负责本地队列缓存、离线持久化、容量控制。
|
||||
4. 上报层:负责批量传输、失败重试、退避与状态感知。
|
||||
|
||||
核心原则:
|
||||
|
||||
- 所有事件必须走统一入口。
|
||||
- 业务代码不直接请求上报接口。
|
||||
- 上报失败不能影响用户主流程。
|
||||
|
||||
## 5. 事件模型设计
|
||||
|
||||
### 5.1 事件分类
|
||||
|
||||
- error:异常与失败事件。
|
||||
- lifecycle:应用/页面生命周期事件。
|
||||
- behavior:关键操作行为事件。
|
||||
|
||||
### 5.2 一阶段标准事件
|
||||
|
||||
- app_session_started
|
||||
- app_session_ended
|
||||
- page_view_started
|
||||
- page_view_ended
|
||||
- chat_input_started
|
||||
- chat_input_submitted
|
||||
|
||||
说明:
|
||||
|
||||
- 输入“次数”由 chat_input_submitted 聚合统计,不新增独立事件。
|
||||
- 应用“持续事件”采用开始+结束计算时长,不使用固定频率心跳。
|
||||
|
||||
### 5.3 通用字段规范
|
||||
|
||||
每条事件统一包含:
|
||||
|
||||
- event_id:事件唯一标识。
|
||||
- event_name:事件名。
|
||||
- event_type:事件分类。
|
||||
- event_time:客户端事件时间。
|
||||
- session_id:会话标识。
|
||||
- user_id_hash:用户标识哈希(可空)。
|
||||
- app_version:应用版本。
|
||||
- platform:平台类型。
|
||||
- route:当前页面标识(可空)。
|
||||
- payload:事件特有字段。
|
||||
|
||||
错误事件附加字段:
|
||||
|
||||
- error_type、error_message、stacktrace(按策略裁剪)。
|
||||
- severity(warning/error/fatal)。
|
||||
- fingerprint(聚合键)。
|
||||
|
||||
## 6. 关键指标口径
|
||||
|
||||
### 6.1 应用打开时间与会话时长
|
||||
|
||||
- 会话开始:应用进入可交互前台时记录。
|
||||
- 会话结束:应用切到后台时记录。
|
||||
- 时长计算:session_end_time - session_start_time。
|
||||
- 异常中断补偿:若会话未正常结束,在下次启动时补偿结束记录,并标记为补偿事件。
|
||||
|
||||
### 6.2 页面停留时长
|
||||
|
||||
- 页面进入时记录 page_view_started。
|
||||
- 页面离开时记录 page_view_ended 与 duration_ms。
|
||||
- 页面切换由统一路由观察机制触发,确保口径一致。
|
||||
|
||||
### 6.3 对话输入时间与次数
|
||||
|
||||
- 用户首次进入输入状态时记录 chat_input_started。
|
||||
- 用户提交输入时记录 chat_input_submitted。
|
||||
- 输入时长:submit_time - input_start_time。
|
||||
- 输入次数:按提交事件数量聚合。
|
||||
- 不采集输入正文,仅采集长度与时长。
|
||||
|
||||
## 7. 隐私与安全设计
|
||||
|
||||
必须遵循最小采集原则与默认脱敏策略:
|
||||
|
||||
- 禁止采集或上报:token、密码、手机号、邮箱、聊天正文、敏感证件信息。
|
||||
- 标识类字段统一哈希化或匿名化。
|
||||
- 错误消息与堆栈按规则裁剪,避免包含敏感上下文。
|
||||
- 采用字段白名单策略,非白名单字段默认不上报。
|
||||
|
||||
安全底线:
|
||||
|
||||
- 采集系统本身不可引入认证绕过与敏感信息泄露风险。
|
||||
- 上报通道需具备鉴权与重放防护能力。
|
||||
|
||||
## 8. 稳定性与性能策略
|
||||
|
||||
- 事件写入本地队列后异步上报,主线程不阻塞。
|
||||
- 批量上传,控制单次包体和频率。
|
||||
- 失败使用指数退避重试,达到上限后丢弃并记录内部统计。
|
||||
- 本地队列设置容量上限,采用环形覆盖或优先级淘汰策略。
|
||||
- 在弱网/离线场景允许延迟上报,恢复后补发。
|
||||
|
||||
## 9. 质量保障与验证
|
||||
|
||||
一阶段验收重点:
|
||||
|
||||
- 正确性:六类标准事件均可稳定产出且字段完整。
|
||||
- 一致性:同类事件口径一致,可跨版本对比。
|
||||
- 安全性:敏感字段泄露检测通过。
|
||||
- 稳定性:采集开启后不影响主要业务链路。
|
||||
|
||||
建议验证维度:
|
||||
|
||||
- 会话开始/结束与时长计算一致性。
|
||||
- 页面进出成对率。
|
||||
- 输入开始到提交链路闭环率。
|
||||
- 错误事件聚合有效性(fingerprint 去重后可读)。
|
||||
|
||||
## 10. 演进路线
|
||||
|
||||
二阶段建议按需扩展:
|
||||
|
||||
- 增加更多关键业务事件(保留最小集合原则)。
|
||||
- 引入崩溃平台作为补充通道(仅 fatal/crash)。
|
||||
- 建立统一查询与告警规则(高频错误、关键路径失败率)。
|
||||
- 增强版本对比分析能力,支持发布质量回归判断。
|
||||
|
||||
## 11. 决策摘要
|
||||
|
||||
一阶段采用通用采集体系,围绕“报错日志 + 三类关键行为”快速落地:
|
||||
|
||||
- 会话:打开与持续时长。
|
||||
- 页面:停留时长。
|
||||
- 对话:输入时长与提交次数。
|
||||
|
||||
在不依赖具体重构代码结构的前提下,该设计可作为后续大重构期间的稳定观测基线。
|
||||
@@ -1,126 +0,0 @@
|
||||
# App Theme Dark Mode and Hardcoded Color Migration Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Enable automatic light/dark mode following system settings and gradually migrate UI color usage from direct `AppColors` references to theme-driven semantic colors.
|
||||
|
||||
**Architecture:** Introduce a dual-theme foundation in `AppTheme` (`light` and `dark`) and route app root to `ThemeMode.system`. Then migrate color access in batches: first shared widgets, then high-traffic feature pages, then long-tail modules. Keep `AppColors` as design-token source only inside theme/token layers during migration.
|
||||
|
||||
**Tech Stack:** Flutter Material 3 (`ThemeData`, `ColorScheme`, `ThemeMode`), existing design tokens (`AppColors`, `AppSpacing`, `AppRadius`), flutter analyze/test verification.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Theme Foundation and System Dark Mode Wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/theme/app_theme.dart`
|
||||
- Modify: `apps/lib/app/app.dart`
|
||||
|
||||
**Step 1: Build dual-theme entry points**
|
||||
|
||||
- Add `AppTheme.dark` and a shared private builder `AppTheme._buildTheme(Brightness)`.
|
||||
- Ensure light and dark themes both use Material 3 and `ColorScheme.fromSeed`.
|
||||
|
||||
**Step 2: Remove theme-level hardcoded semantic colors**
|
||||
|
||||
- Replace direct `AppColors` usage in app bar/button/input decoration theme with color-scheme-driven values.
|
||||
- Keep radii and spacing tokens unchanged.
|
||||
|
||||
**Step 3: Wire root app to system mode**
|
||||
|
||||
- In `MaterialApp.router`, set `theme`, `darkTheme`, and `themeMode: ThemeMode.system`.
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: no new analyzer errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Shared Widget Batch (P0) - Replace Direct AppColors with Theme Semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/shared/widgets/error_retry_surface.dart`
|
||||
- Modify: `apps/lib/shared/widgets/confirm_sheet.dart`
|
||||
- Modify: `apps/lib/shared/widgets/destructive_action_sheet.dart`
|
||||
- Modify: `apps/lib/shared/widgets/toast/toast_type_config.dart`
|
||||
|
||||
**Step 1: Migrate simple error surface**
|
||||
|
||||
- Use `Theme.of(context).colorScheme.error` for retry message color.
|
||||
|
||||
**Step 2: Migrate confirm/destructive sheets**
|
||||
|
||||
- Replace sheet background/border/text/primary-action color bindings with `ColorScheme` semantic roles (`surface`, `outlineVariant`, `onSurface`, `onSurfaceVariant`, `primary`, `onPrimary`, `error`, `onError`).
|
||||
|
||||
**Step 3: Migrate toast style mapping**
|
||||
|
||||
- Replace static feedback colors with semantic color-scheme mappings per toast type.
|
||||
- Preserve existing icon and l10n label behavior.
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: no new analyzer errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Batch Migration Governance and Rollout Rules
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md`
|
||||
|
||||
**Step 1: Document batch strategy**
|
||||
|
||||
- Add phased rollout strategy: Foundation -> Shared widgets (P0) -> Core pages (P1) -> Long tail (P2) -> guardrails.
|
||||
|
||||
**Step 2: Define acceptance and rollback criteria**
|
||||
|
||||
- Include per-batch verification (`flutter analyze`, targeted widget tests/manual dark mode checks).
|
||||
- Require small-scope commits per batch for safe rollback.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: P1/P2 Execution Backlog (Follow-up)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/home/**`
|
||||
- Modify: `apps/lib/features/settings/**`
|
||||
- Modify: `apps/lib/features/auth/**`
|
||||
- Modify: remaining `apps/lib/features/**`
|
||||
|
||||
**Step 1: Prioritize by user path**
|
||||
|
||||
- P1 order: home -> settings -> auth.
|
||||
- P2: remaining feature pages and low-frequency screens.
|
||||
|
||||
**Step 2: Migrate per file with semantic mapping**
|
||||
|
||||
- Replace direct `AppColors` usage in widget code with `Theme.of(context).colorScheme` or scoped theme helpers.
|
||||
- Keep visual behavior equivalent first; optimize polish after semantic migration is stable.
|
||||
|
||||
**Step 3: Verify per batch**
|
||||
|
||||
Run:
|
||||
- `cd apps && flutter analyze`
|
||||
- `cd apps && flutter test` (targeted where tests exist)
|
||||
|
||||
Expected: no regressions in targeted flows and theme switching.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: dark mode contrast regression in legacy widgets.
|
||||
- Mitigation: migrate shared widgets first, then high-traffic pages with screenshot/manual checks.
|
||||
- Risk: mixed semantic + static colors during transition.
|
||||
- Mitigation: enforce migration order and avoid adding new direct `AppColors` in UI layers.
|
||||
- Risk: broad-scope change fatigue.
|
||||
- Mitigation: ship in small batches with isolated verification and rollback points.
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- `MaterialApp` follows `ThemeMode.system` with `theme` and `darkTheme` configured.
|
||||
- Theme layer no longer relies on static semantic colors for app-wide component themes.
|
||||
- P0 shared widget batch migrated to theme semantics.
|
||||
- Migration plan and execution progress documented in bug tracker doc.
|
||||
@@ -1,144 +0,0 @@
|
||||
# Data Repositories Cache Strategy Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Establish a shared cache abstraction and shared data-repository entrypoints so cross-feature data access no longer depends on direct feature-to-feature data imports.
|
||||
|
||||
**Architecture:** Keep cache infrastructure centralized in `apps/lib/data/cache/`, and expose cross-feature data through `apps/lib/data/repositories/` facades registered in DI as singletons. Feature screens consume repositories, while cache policy/key/invalidation remain in repository layer.
|
||||
|
||||
**Tech Stack:** Flutter, Dart, GetIt, flutter_test
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Finalize Shared Cache Foundation
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/data/cache/cached_repository.dart`
|
||||
- Modify: `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
- Test: `apps/test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add an invalidate-vs-inflight regression case in `user_profile_cache_repository_test.dart` asserting stale in-flight refresh cannot restore `cachedUser` after invalidate.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
Expected: FAIL before generation-guard fix.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add generation/version guard in `UserProfileCacheRepository` so `invalidate()` increments generation and stale async loader results are ignored for in-memory snapshot updates.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/data/cache/cached_repository.dart apps/lib/features/settings/data/services/user_profile_cache_repository.dart apps/test/features/settings/data/services/user_profile_cache_repository_test.dart
|
||||
git commit -m "refactor: harden user profile cache invalidation race handling"
|
||||
```
|
||||
|
||||
### Task 2: Introduce Shared Data Repositories Module
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/data/repositories/inbox_repository.dart`
|
||||
- Create: `apps/lib/data/repositories/calendar_event_repository.dart`
|
||||
- Create: `apps/lib/data/repositories/user_repository.dart`
|
||||
- Modify: `apps/lib/app/di/injection.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a DI registration smoke test (or lightweight compile-level usage test) that resolves the new repositories from GetIt.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test <new-test-file>`
|
||||
|
||||
Expected: FAIL because repositories are not yet defined/registered.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement repository facades that wrap existing APIs (`InboxApi`, `CalendarApi`, `UsersApi`) and register them as singletons in DI.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test <new-test-file>`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/data/repositories apps/lib/app/di/injection.dart
|
||||
git commit -m "refactor: add shared data repositories module"
|
||||
```
|
||||
|
||||
### Task 3: Migrate Cross-Feature Screens to Shared Repositories
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart`
|
||||
- Modify: `apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart`
|
||||
- Modify: `apps/lib/features/todo/presentation/screens/todo_edit_screen.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add targeted tests for message detail/list data loading and todo edit schedule loading using repository dependencies.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test <affected-test-files>`
|
||||
|
||||
Expected: FAIL before dependency switch.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Replace direct API injections with shared repository injections from `apps/lib/data/repositories/*`.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test <affected-test-files>`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart apps/lib/features/todo/presentation/screens/todo_edit_screen.dart
|
||||
git commit -m "refactor: switch cross-feature screens to shared repositories"
|
||||
```
|
||||
|
||||
### Task 4: Verification and Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/AGENTS.md`
|
||||
- Modify: `docs/bugs/2026-03-27-repository缓存抽象.md`
|
||||
- Modify: `docs/bugs/服务层与Repository层职责混乱.md`
|
||||
|
||||
**Step 1: Run full targeted verification**
|
||||
|
||||
Run: `flutter analyze lib/data/cache lib/data/repositories lib/app/di/injection.dart lib/features/messages/presentation/screens/message_invite_detail_screen.dart lib/features/messages/presentation/screens/message_invite_list_screen.dart lib/features/todo/presentation/screens/todo_edit_screen.dart`
|
||||
|
||||
Expected: No issues.
|
||||
|
||||
**Step 2: Run regression tests**
|
||||
|
||||
Run: `flutter test test/data/cache/cached_repository_test.dart test/features/settings/data/services/user_profile_cache_repository_test.dart test/app/router/app_router_redirect_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 3: Update docs status**
|
||||
|
||||
Mark relevant bug docs as in-progress/resolved and document new layering rules.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/AGENTS.md docs/bugs/2026-03-27-repository缓存抽象.md docs/bugs/服务层与Repository层职责混乱.md
|
||||
git commit -m "docs: document shared repository and cache strategy"
|
||||
```
|
||||
@@ -1,317 +0,0 @@
|
||||
# L10n Cleanup + Stable Error Code + Frontend Text Migration Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Remove redundant l10n wrappers, introduce backend stable/mappable error codes for HTTP contracts, and continue frontend hardcoded-text localization migration to zh/en with default zh.
|
||||
|
||||
**Architecture:** Keep Flutter UI localization in `lib/l10n` as single source of truth, minimize cross-layer localization coupling, and use backend RFC7807 + `code`/`params` as machine-readable contract. Frontend maps `code -> l10n key` for user-facing messages while preserving fallback behavior.
|
||||
|
||||
**Tech Stack:** Flutter gen-l10n, FastAPI (RFC7807), Pydantic models, Dio client error mapping, existing AGENTS/rules constraints.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Freeze and baseline current behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: none (read-only task)
|
||||
- Verify: `apps/lib/**`, `backend/src/**`, `docs/protocols/**`
|
||||
|
||||
**Step 1: Snapshot app localization status**
|
||||
|
||||
Run: `python scripts/count_cn_literals.py` (or equivalent one-off command)
|
||||
Expected: baseline count and top files with Chinese literals.
|
||||
|
||||
**Step 2: Snapshot backend detail-string usage**
|
||||
|
||||
Run: `python scripts/count_http_detail_usage.py` (or equivalent one-off command)
|
||||
Expected: per-file count of `HTTPException(detail=...)` hotspots.
|
||||
|
||||
**Step 3: Capture baseline checks**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: no new errors, only known existing warnings/infos.
|
||||
|
||||
Run: `cd backend && uv run pytest -q` (or targeted fast suite if full too slow)
|
||||
Expected: baseline pass/fail recorded for regression comparison.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Refactor l10n structure to remove redundant wrapper responsibilities
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/app/app.dart`
|
||||
- Modify: UI files currently importing `apps/lib/core/l10n/l10n.dart`
|
||||
- Delete/Modify: `apps/lib/core/l10n/l10n.dart` (depending on final outcome)
|
||||
- Verify: generated files under `apps/lib/l10n/`
|
||||
|
||||
**Step 1: Define target rule**
|
||||
|
||||
Rule:
|
||||
- UI layer uses `context.l10n`.
|
||||
- Non-UI layer does not depend on ad-hoc global locale state.
|
||||
- If non-UI needs localization, pass already-localized strings in from caller or inject mapper service.
|
||||
|
||||
**Step 2: Write failing/static guard checks**
|
||||
|
||||
Add temporary grep checks:
|
||||
- Fail when new code adds `L10n.current` in feature/presentation.
|
||||
- Fail when `core/l10n/l10n.dart` is reintroduced for convenience access.
|
||||
|
||||
**Step 3: Replace call sites incrementally**
|
||||
|
||||
For each file:
|
||||
1. Replace `L10n.current.xxx` with `context.l10n.xxx` where `BuildContext` exists.
|
||||
2. For cubit/service/form validators, inject message providers or pass messages from UI.
|
||||
3. Keep behavior unchanged.
|
||||
|
||||
**Step 4: Remove locale global mutation path**
|
||||
|
||||
In `app.dart`:
|
||||
- Remove `L10n.setLocale(...)` style side effects.
|
||||
- Keep Flutter-native delegates + `supportedLocales` + default locale logic.
|
||||
|
||||
**Step 5: Delete redundant wrapper (if no remaining valid use case)**
|
||||
|
||||
Delete `apps/lib/core/l10n/l10n.dart` only after all references are removed and non-UI strategy is in place.
|
||||
|
||||
**Step 6: Verify**
|
||||
|
||||
Run: `cd apps && flutter gen-l10n && flutter analyze`
|
||||
Expected: no errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Define backend stable error code contract (RFC7807 extension)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/core/http/response.py`
|
||||
- Modify: `backend/src/app.py`
|
||||
- Create: `backend/src/core/http/errors.py`
|
||||
- Modify: `docs/protocols/agent/api-endpoints.md`
|
||||
- (optional) Create: `docs/protocols/common/error-contract.md`
|
||||
|
||||
**Step 1: Extend problem details schema**
|
||||
|
||||
Add fields:
|
||||
- `code: str | None`
|
||||
- `params: dict[str, str | int | float | bool] | None`
|
||||
|
||||
Preserve RFC7807 required fields and media type.
|
||||
|
||||
**Step 2: Introduce unified domain error type**
|
||||
|
||||
In `core/http/errors.py`, create exception class carrying:
|
||||
- http status
|
||||
- stable error code (UPPER_SNAKE_CASE)
|
||||
- optional params
|
||||
- optional internal detail
|
||||
|
||||
**Step 3: Wire global exception handlers**
|
||||
|
||||
In `app.py`:
|
||||
- Convert domain exceptions to problem+json with `code` and `params`.
|
||||
- Keep fallback for unknown exceptions.
|
||||
|
||||
**Step 4: Define code naming convention**
|
||||
|
||||
Examples:
|
||||
- `AUTH_INVALID_TOKEN`
|
||||
- `AUTH_TOKEN_EXPIRED`
|
||||
- `SCHEDULE_ITEM_NOT_FOUND`
|
||||
- `TODO_TITLE_REQUIRED`
|
||||
- `FRIENDSHIP_ALREADY_EXISTS`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Migrate backend hotspots from free-text detail to stable codes
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/friendships/service.py`
|
||||
- Modify: `backend/src/v1/schedule_items/service.py`
|
||||
- Modify: `backend/src/v1/todo/service.py`
|
||||
- Modify: `backend/src/v1/agent/service.py`
|
||||
- Modify: `backend/src/v1/users/service.py`
|
||||
- Modify: `backend/src/v1/memories/service.py`
|
||||
- Modify: `backend/src/v1/auth/gateway.py`
|
||||
- Modify: `backend/src/v1/agent/router.py`
|
||||
- Modify: other files with `HTTPException(detail=...)`
|
||||
|
||||
**Step 1: Prioritize by impact**
|
||||
|
||||
Order:
|
||||
1. Auth
|
||||
2. Agent
|
||||
3. Todo/Schedule/Friendships
|
||||
4. Users/Memories
|
||||
|
||||
**Step 2: Replace throw sites**
|
||||
|
||||
For each detail-based throw:
|
||||
1. Map to stable `code`.
|
||||
2. Keep detail only as optional server diagnostic text.
|
||||
3. Add params when useful (e.g., max size, field, limit).
|
||||
|
||||
**Step 3: Preserve backwards compatibility window**
|
||||
|
||||
During transition:
|
||||
- Keep `detail` present.
|
||||
- Add `code`/`params` immediately.
|
||||
- Frontend prefers `code`, falls back to existing behavior.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Frontend network error mapping to l10n via backend code
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/network/api_exception.dart`
|
||||
- Create: `apps/lib/core/network/error_code_mapper.dart`
|
||||
- Modify: call sites currently displaying raw backend detail
|
||||
- Modify: `apps/lib/l10n/app_zh.arb`, `apps/lib/l10n/app_en.arb`
|
||||
|
||||
**Step 1: Parse `code` and `params` from response payload**
|
||||
|
||||
In `ApiException.fromDioError`:
|
||||
- Read RFC7807 + extension fields.
|
||||
- Keep `statusCode` fallback behavior.
|
||||
|
||||
**Step 2: Map `code -> localized message`**
|
||||
|
||||
Implement central mapper:
|
||||
- Input: code/status/params
|
||||
- Output: localized user-facing string key resolution
|
||||
|
||||
**Step 3: Fallback strategy**
|
||||
|
||||
Priority:
|
||||
1. known code mapping
|
||||
2. status-based generic mapping
|
||||
3. safe generic fallback (`request failed` localized)
|
||||
|
||||
**Step 4: Replace UI direct usage of raw server detail**
|
||||
|
||||
Audit and update places where `e.toString()` or backend detail is shown directly.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Continue frontend hardcoded text migration (remaining files)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/settings/presentation/screens/*.dart` (remaining high-count files)
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/*.dart`
|
||||
- Modify: `apps/lib/features/calendar/presentation/widgets/*.dart`
|
||||
- Modify: `apps/lib/l10n/app_zh.arb`, `apps/lib/l10n/app_en.arb`
|
||||
|
||||
**Step 1: Batch by screen group**
|
||||
|
||||
Batch A: settings deep pages
|
||||
Batch B: calendar pages
|
||||
Batch C: shared/home leftovers
|
||||
|
||||
**Step 2: Migrate with key hygiene**
|
||||
|
||||
Rules:
|
||||
- key names are feature-prefixed and stable
|
||||
- dynamic texts use placeholders, not string concatenation
|
||||
- avoid duplicate semantic keys
|
||||
|
||||
**Step 3: After each batch, run verification**
|
||||
|
||||
Run:
|
||||
- `cd apps && flutter gen-l10n`
|
||||
- `cd apps && flutter analyze`
|
||||
|
||||
Track remaining hardcoded-literal count after each batch.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Protocol docs and test updates
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/protocols/agent/api-endpoints.md`
|
||||
- Modify/Create: `docs/protocols/common/error-contract.md`
|
||||
- Modify: backend integration/unit tests asserting only `detail`
|
||||
- Modify: frontend tests around error display/mapping
|
||||
|
||||
**Step 1: Document new error response shape**
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": "Unprocessable Entity",
|
||||
"status": 422,
|
||||
"detail": "Validation failed",
|
||||
"code": "TODO_TITLE_REQUIRED",
|
||||
"params": {"field": "title"},
|
||||
"instance": "/api/v1/todo"
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update tests to assert codes first**
|
||||
|
||||
Replace brittle text assertions with:
|
||||
- `status`
|
||||
- `code`
|
||||
- optional `params`
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Final verification gate
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Apps verification**
|
||||
|
||||
Run:
|
||||
- `cd apps && flutter gen-l10n`
|
||||
- `cd apps && flutter analyze`
|
||||
|
||||
**Step 2: Backend verification**
|
||||
|
||||
Run:
|
||||
- `cd backend && uv run ruff check .`
|
||||
- `cd backend && uv run basedpyright`
|
||||
- `cd backend && uv run pytest -q`
|
||||
|
||||
**Step 3: Cross-contract smoke**
|
||||
|
||||
Run targeted API checks ensuring error payload includes `code` for representative modules.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Rollout and compatibility
|
||||
|
||||
**Files:**
|
||||
- Modify: release notes/changelog if used
|
||||
|
||||
**Step 1: Progressive rollout strategy**
|
||||
|
||||
- Phase 1: backend emits both `detail` + `code`
|
||||
- Phase 2: frontend consumes `code` with fallback
|
||||
- Phase 3: clean up legacy detail-dependent branches
|
||||
|
||||
**Step 2: Monitoring**
|
||||
|
||||
- Log unknown/unmapped error codes on frontend
|
||||
- Add backend metrics for top emitted error codes
|
||||
|
||||
---
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- Risk: non-UI code loses localization access after wrapper removal
|
||||
- Mitigation: inject messages from UI/service boundary; avoid static locale globals.
|
||||
- Risk: backend code migration is broad (many detail throws)
|
||||
- Mitigation: staged module-by-module migration + compatibility window.
|
||||
- Risk: front/back mismatch in error code enum
|
||||
- Mitigation: shared protocol doc + CI checks for known code list.
|
||||
|
||||
## Done criteria
|
||||
|
||||
- `apps/lib/core/l10n/l10n.dart` removed or reduced to zero-overlap minimal utility with explicit justification.
|
||||
- Backend RFC7807 responses include stable `code` (and optional `params`) on migrated endpoints.
|
||||
- Frontend maps known codes to zh/en l10n; raw detail is no longer primary user-facing string.
|
||||
- Hardcoded visible Chinese text count in `apps/lib` reduced to agreed threshold or zero for targeted modules.
|
||||
- Docs and tests updated accordingly.
|
||||
@@ -0,0 +1,556 @@
|
||||
# Calendar Share Redesign - Implementation 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:** 重写日历分享功能,支持选择多个好友/用户并单独设置权限,同时后端新增批量分享接口。
|
||||
|
||||
**Architecture:**
|
||||
- **后端**: 新增 `POST /api/v1/schedule-items/{id}/share/batch` 批量分享接口,原有单用户接口保留。
|
||||
- **前端**: 重构 `CalendarEventShareScreen` 和 `CalendarShareDialog`,移除硬编码手机号输入,改为用户搜索 + 多选 + 独立权限设置。
|
||||
|
||||
**Tech Stack:** Flutter (Riverpod/BLoC), FastAPI (Pydantic), PostgreSQL, 现有 API 契约
|
||||
|
||||
---
|
||||
|
||||
## 背景与现状
|
||||
|
||||
### 现有流程
|
||||
```
|
||||
用户 → CalendarEventShareScreen → CalendarShareDialog
|
||||
├── 手机号输入 + 前缀选择
|
||||
├── 权限开关 (View/Edit/Invite)
|
||||
└── POST /api/v1/schedule-items/{id}/share
|
||||
```
|
||||
|
||||
### 问题
|
||||
1. 仅支持单用户分享
|
||||
2. 需要手动输入手机号,不支持好友选择
|
||||
3. 权限是全局的,无法为不同用户设置不同权限
|
||||
|
||||
### 目标流程
|
||||
```
|
||||
用户 → CalendarEventShareScreen → CalendarShareDialog
|
||||
├── 搜索框 (好友/用户名/手机号搜索)
|
||||
├── 用户列表 (可多选)
|
||||
│ └── 每行: 头像 + 名字 + 权限开关 (选中后显示)
|
||||
├── 已选用户 Chips
|
||||
└── POST /api/v1/schedule-items/{id}/share/batch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端 - 新增批量分享接口
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/src/v1/schedule_items/schemas.py` (新增 `ScheduleItemShareBatchRequest`)
|
||||
- Modify: `backend/src/v1/schedule_items/router.py` (新增 `/share/batch` 端点)
|
||||
- Modify: `backend/src/v1/schedule_items/service.py` (新增 `share_batch` 方法)
|
||||
- Verify: `backend/tests/**`
|
||||
|
||||
- [ ] **Step 1: 创建批量分享请求 Schema**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/schemas.py
|
||||
|
||||
class ScheduleItemShareBatchRequest(BaseModel):
|
||||
shares: List[ShareTarget] = Field(..., description="List of users to share with")
|
||||
permission_view: bool = Field(True, description="Default view permission")
|
||||
permission_edit: bool = Field(False, description="Default edit permission")
|
||||
permission_invite: bool = Field(False, description="Default invite permission")
|
||||
|
||||
class ShareTarget(BaseModel):
|
||||
phone: str = Field(..., pattern=r"^\+861[3-9]\d{9}$")
|
||||
permission_view: Optional[bool] = None
|
||||
permission_edit: Optional[bool] = None
|
||||
permission_invite: Optional[bool] = None
|
||||
|
||||
class ScheduleItemShareBatchResponse(BaseModel):
|
||||
message: str
|
||||
success_count: int
|
||||
failure_count: int
|
||||
failures: List[ShareFailure] = []
|
||||
|
||||
class ShareFailure(BaseModel):
|
||||
phone: str
|
||||
reason: str
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现批量分享服务方法**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/service.py
|
||||
|
||||
async def share_batch(
|
||||
db: AsyncSession,
|
||||
current_user: User,
|
||||
schedule_item_id: UUID,
|
||||
shares: List[ShareTarget],
|
||||
default_permission_view: bool = True,
|
||||
default_permission_edit: bool = False,
|
||||
default_permission_invite: bool = False,
|
||||
) -> Tuple[int, int, List[Dict]]:
|
||||
"""批量分享日程给多个用户"""
|
||||
# 1. 验证当前用户是否有邀请权限
|
||||
# 2. 对每个 share 调用现有的 share_single 逻辑
|
||||
# 3. 返回 (成功数, 失败数, 失败详情列表)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 新增批量分享路由**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/router.py
|
||||
|
||||
@router.post("/{item_id}/share/batch", response_model=ScheduleItemShareBatchResponse)
|
||||
async def share_batch(
|
||||
item_id: UUID,
|
||||
request: ScheduleItemShareBatchRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: ScheduleItemService = Depends(),
|
||||
):
|
||||
"""批量分享日程给多个用户"""
|
||||
success_count, failure_count, failures = await service.share_batch(
|
||||
db=db,
|
||||
current_user=current_user,
|
||||
schedule_item_id=item_id,
|
||||
shares=request.shares,
|
||||
default_permission_view=request.permission_view,
|
||||
default_permission_edit=request.permission_edit,
|
||||
default_permission_invite=request.permission_invite,
|
||||
)
|
||||
return ScheduleItemShareBatchResponse(
|
||||
message="Batch share completed",
|
||||
success_count=success_count,
|
||||
failure_count=failure_count,
|
||||
failures=[ShareFailure(**f) for f in failures],
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写测试**
|
||||
|
||||
```python
|
||||
# backend/tests/v1/test_schedule_items.py
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_share_batch_success():
|
||||
"""测试批量分享成功"""
|
||||
# Arrange
|
||||
# Act
|
||||
# Assert
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_share_batch_partial_failure():
|
||||
"""测试批量分享部分失败(非好友用户应被拒绝)"""
|
||||
# Arrange
|
||||
# Act
|
||||
# Assert
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行测试验证**
|
||||
|
||||
```bash
|
||||
cd backend && uv run pytest tests/v1/test_schedule_items.py -v -k "share_batch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 前端 - 新增批量分享 API 方法
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/data/apis/calendar_api.dart`
|
||||
|
||||
- [ ] **Step 1: 新增批量分享 API 方法**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/data/apis/calendar_api.dart
|
||||
|
||||
class CalendarApi {
|
||||
// 现有单用户分享保留
|
||||
Future<void> share(...) { ... }
|
||||
|
||||
// 新增批量分享
|
||||
Future<ShareBatchResponse> shareBatch({
|
||||
required String eventId,
|
||||
required List<ShareTarget> shares,
|
||||
bool permissionView = true,
|
||||
bool permissionEdit = false,
|
||||
bool permissionInvite = false,
|
||||
}) async {
|
||||
final response = await _dio.post(
|
||||
'/api/v1/schedule-items/$eventId/share/batch',
|
||||
data: {
|
||||
'shares': shares.map((s) => s.toJson()).toList(),
|
||||
'permission_view': permissionView,
|
||||
'permission_edit': permissionEdit,
|
||||
'permission_invite': permissionInvite,
|
||||
},
|
||||
);
|
||||
return ShareBatchResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareTarget {
|
||||
final String phone;
|
||||
final bool? permissionView;
|
||||
final bool? permissionEdit;
|
||||
final bool? permissionInvite;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'phone': phone,
|
||||
if (permissionView != null) 'permission_view': permissionView,
|
||||
if (permissionEdit != null) 'permission_edit': permissionEdit,
|
||||
if (permissionInvite != null) 'permission_invite': permissionInvite,
|
||||
};
|
||||
}
|
||||
|
||||
class ShareBatchResponse {
|
||||
final String message;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final List<ShareFailure> failures;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 API 编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/data/apis/calendar_api.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 前端 - 创建分享页面状态管理
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart`
|
||||
- Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart`
|
||||
|
||||
- [ ] **Step 1: 创建 State**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart
|
||||
|
||||
enum CalendarShareStatus { initial, loading, success, failure }
|
||||
|
||||
class SelectedUser {
|
||||
final String id;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
final String phone;
|
||||
bool permissionView;
|
||||
bool permissionEdit;
|
||||
bool permissionInvite;
|
||||
}
|
||||
|
||||
class CalendarShareState {
|
||||
final CalendarShareStatus status;
|
||||
final List<FriendResponse> friends;
|
||||
final List<UserProfile> searchResults;
|
||||
final List<SelectedUser> selectedUsers;
|
||||
final String searchQuery;
|
||||
final String? errorMessage;
|
||||
|
||||
// 权限默认值(应用于新选中的用户)
|
||||
bool defaultPermissionView;
|
||||
bool defaultPermissionEdit;
|
||||
bool defaultPermissionInvite;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 Cubit**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart
|
||||
|
||||
class CalendarShareCubit extends Cubit<CalendarShareState> {
|
||||
final FriendsApi _friendsApi;
|
||||
final UsersApi _usersApi;
|
||||
final CalendarApi _calendarApi;
|
||||
|
||||
CalendarShareCubit({
|
||||
required FriendsApi friendsApi,
|
||||
required UsersApi usersApi,
|
||||
required CalendarApi calendarApi,
|
||||
}) : super(CalendarShareState.initial());
|
||||
|
||||
// 加载好友列表
|
||||
Future<void> loadFriends() async { ... }
|
||||
|
||||
// 搜索用户
|
||||
Future<void> searchUsers(String query) async { ... }
|
||||
|
||||
// 切换用户选中状态
|
||||
void toggleUser(String userId) { ... }
|
||||
|
||||
// 更新单个用户的权限
|
||||
void updateUserPermission(String userId, {bool? view, bool? edit, bool? invite}) { ... }
|
||||
|
||||
// 移除已选用户
|
||||
void removeUser(String userId) { ... }
|
||||
|
||||
// 设置默认权限(对新选用户生效)
|
||||
void setDefaultPermissions({bool? view, bool? edit, bool? invite}) { ... }
|
||||
|
||||
// 发送批量分享
|
||||
Future<void> share(String eventId) async { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 验证编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/presentation/cubits/calendar_share_cubit.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 前端 - 重构分享对话框 UI
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/presentation/widgets/user_select_list.dart`
|
||||
- Create: `apps/lib/features/calendar/presentation/widgets/user_select_item.dart`
|
||||
- Modify: `apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart`
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart`
|
||||
|
||||
### 4.1 用户选择列表组件
|
||||
|
||||
- [ ] **Step 1: 创建 UserSelectItem**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/widgets/user_select_item.dart
|
||||
|
||||
class UserSelectItem extends StatelessWidget {
|
||||
final UserBasicInfo user;
|
||||
final bool isSelected;
|
||||
final bool showPermissions; // 选中后才显示权限开关
|
||||
final SelectedUser? selectedUser; // 如果选中,包含权限状态
|
||||
final ValueChanged<bool> onToggle;
|
||||
final Function(PermissionType, bool) onPermissionChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Avatar(url: user.avatarUrl, name: user.username),
|
||||
title: Text(user.username),
|
||||
subtitle: showPermissions && isSelected
|
||||
? Row(
|
||||
children: [
|
||||
_PermissionChip('V', selectedUser?.permissionView ?? true, (v) => onPermissionChanged(PermissionType.view, v)),
|
||||
_PermissionChip('E', selectedUser?.permissionEdit ?? false, (v) => onPermissionChanged(PermissionType.edit, v)),
|
||||
_PermissionChip('I', selectedUser?.permissionInvite ?? false, (v) => onPermissionChanged(PermissionType.invite, v)),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
trailing: Checkbox(value: isSelected, onChanged: (v) => onToggle(v ?? false)),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 重构 CalendarShareDialog
|
||||
|
||||
- [ ] **Step 2: 重构对话框**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart
|
||||
|
||||
class CalendarShareDialog extends StatefulWidget {
|
||||
// ... existing props
|
||||
|
||||
@override
|
||||
State<CalendarShareDialog> createState() => _CalendarShareDialogState();
|
||||
}
|
||||
|
||||
class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
late final CalendarShareCubit _cubit;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cubit = context.read<CalendarShareCubit>();
|
||||
_cubit.loadFriends();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cubit,
|
||||
child: BlocBuilder<CalendarShareCubit, CalendarShareState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context, state),
|
||||
|
||||
// 搜索框
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索好友或输入手机号...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onChanged: (value) => _cubit.searchUsers(value),
|
||||
),
|
||||
|
||||
// 已选用户 Chips
|
||||
if (state.selectedUsers.isNotEmpty) ...[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: state.selectedUsers.map((user) {
|
||||
return Chip(
|
||||
label: Text('${user.username} (${_getPermissionSummary(user)})'),
|
||||
onDeleted: () => _cubit.removeUser(user.id),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
|
||||
// 用户列表
|
||||
Flexible(
|
||||
child: _buildUserList(state),
|
||||
),
|
||||
|
||||
// 发送按钮
|
||||
AppButton(
|
||||
text: '发送邀请 (${state.selectedUsers.length}人)',
|
||||
onPressed: state.selectedUsers.isEmpty
|
||||
? null
|
||||
: () => _handleShare(context, state),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserList(CalendarShareState state) {
|
||||
// 如果有搜索结果,显示搜索结果;否则显示好友列表
|
||||
final users = state.searchQuery.isNotEmpty
|
||||
? state.searchResults
|
||||
: state.friends.map((f) => f.friend).toList();
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
final selectedUser = state.selectedUsers.cast<SelectedUser?>().firstWhere(
|
||||
(s) => s?.id == user.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
final isSelected = selectedUser != null;
|
||||
|
||||
return UserSelectItem(
|
||||
user: user,
|
||||
isSelected: isSelected,
|
||||
showPermissions: isSelected,
|
||||
selectedUser: selectedUser,
|
||||
onToggle: (selected) {
|
||||
if (selected) {
|
||||
_cubit.addUser(user, state.defaultPermissionView, state.defaultPermissionEdit, state.defaultPermissionInvite);
|
||||
} else {
|
||||
_cubit.removeUser(user.id);
|
||||
}
|
||||
},
|
||||
onPermissionChanged: (type, value) {
|
||||
_cubit.updateUserPermission(user.id, type: type, value: value);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 移除顶部栏,简化 CalendarEventShareScreen**
|
||||
|
||||
```dart
|
||||
// apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart
|
||||
|
||||
class CalendarEventShareScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 直接显示分享对话框,不需要额外顶部栏
|
||||
Expanded(
|
||||
child: CalendarShareDialog(
|
||||
eventId: eventId,
|
||||
eventTitle: event.title,
|
||||
canInvite: event.canInvite,
|
||||
canEdit: event.canEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 验证编译**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze lib/features/calendar/presentation/widgets/calendar_share_dialog.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart`
|
||||
|
||||
- [ ] **Step 1: 编写 Widget 测试**
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
testWidgets('选择用户后显示权限开关', (tester) async {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
|
||||
testWidgets('已选用户显示在 Chips 中', (tester) async {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试**
|
||||
|
||||
```bash
|
||||
cd apps && flutter test test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Task 1 (后端批量接口)
|
||||
↓
|
||||
Task 2 (前端批量分享 API)
|
||||
↓
|
||||
Task 3 (分享状态管理 Cubit)
|
||||
↓
|
||||
Task 4 (分享对话框 UI)
|
||||
↓
|
||||
Task 5 (集成测试)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **后端兼容性**: 批量接口上线前,前端使用原有单用户接口
|
||||
2. **搜索防抖**: 搜索用户时需要 debounce 避免频繁请求
|
||||
3. **好友列表分页**: 如果好友数量大,需要分页加载
|
||||
4. **权限验证**: 前端权限不能超过当前用户的权限(由后端控制)
|
||||
@@ -0,0 +1,214 @@
|
||||
# Calendar Detail - Show Subscribers Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在日历详情页显示已订阅此日历的用户列表
|
||||
|
||||
**Architecture:**
|
||||
- 后端:在 `GET /api/v1/schedule-items/{id}` 响应中返回 `subscribers` 列表
|
||||
- 前端:在详情页渲染订阅者列表组件
|
||||
|
||||
**Tech Stack:** Flutter, FastAPI, PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端 - 返回订阅者列表
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/schemas.py`
|
||||
- Modify: `backend/src/v1/schedule_items/service.py`
|
||||
- Verify: `backend/tests/integration/v1/test_schedule_items_routes.py`
|
||||
|
||||
- [ ] **Step 1: 新增 SubscriberInfo Schema**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/schemas.py
|
||||
|
||||
class SubscriberInfo(BaseModel):
|
||||
"""订阅者信息"""
|
||||
user_id: UUID
|
||||
username: str
|
||||
avatar_url: str | None = None
|
||||
permission: int # 位标志: 1=view, 2=invite, 4=edit
|
||||
status: str # active, pending, paused, unsubscribed
|
||||
subscribed_at: datetime
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 ScheduleItemResponse**
|
||||
|
||||
```python
|
||||
class ScheduleItemResponse(BaseModel):
|
||||
# ... existing fields ...
|
||||
subscribers: List[SubscriberInfo] = [] # 新增
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 service.get_by_id 填充 subscribers**
|
||||
|
||||
```python
|
||||
# backend/src/v1/schedule_items/service.py
|
||||
|
||||
async def get_by_id(self, item_id: UUID) -> ScheduleItemResponse:
|
||||
item = await self._repository.get_by_id(item_id)
|
||||
if not item:
|
||||
raise ScheduleItemNotFoundError(item_id)
|
||||
|
||||
# 获取订阅者列表
|
||||
subscriptions = await self._repository.get_subscriptions_by_item_id(item_id)
|
||||
subscribers = []
|
||||
for sub in subscriptions:
|
||||
if sub.status == 'active': # 只返回活跃订阅者
|
||||
user = await self._user_repo.get_by_id(sub.subscriber_id)
|
||||
if user:
|
||||
subscribers.append(SubscriberInfo(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
avatar_url=user.avatar_url,
|
||||
permission=sub.permission,
|
||||
status=sub.status,
|
||||
subscribed_at=sub.created_at,
|
||||
))
|
||||
|
||||
return ScheduleItemResponse(
|
||||
# ... existing fields ...,
|
||||
subscribers=subscribers,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写集成测试**
|
||||
|
||||
```python
|
||||
# backend/tests/integration/v1/test_schedule_items_routes.py
|
||||
|
||||
async def test_get_item_returns_subscribers(self):
|
||||
"""测试获取日程时返回订阅者列表"""
|
||||
# Arrange: 创建日程,添加订阅者
|
||||
# Act: GET /api/v1/schedule-items/{id}
|
||||
# Assert: 响应包含 subscribers 字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 前端 - Model 更新
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
|
||||
|
||||
- [ ] **Step 1: 新增 Subscriber 模型**
|
||||
|
||||
```dart
|
||||
class Subscriber {
|
||||
final String userId;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
final int permission;
|
||||
final String status;
|
||||
final DateTime subscribedAt;
|
||||
|
||||
bool get canView => (permission & 1) != 0;
|
||||
bool get canEdit => (permission & 4) != 0;
|
||||
bool get canInvite => (permission & 2) != 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 ScheduleItemModel 中添加 subscribers 字段**
|
||||
|
||||
```dart
|
||||
class ScheduleItemModel {
|
||||
// ... existing fields ...
|
||||
final List<Subscriber> subscribers;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 更新 fromJson**
|
||||
|
||||
```dart
|
||||
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScheduleItemModel(
|
||||
// ... existing fields ...,
|
||||
subscribers: (json['subscribers'] as List<dynamic>?)
|
||||
?.map((s) => Subscriber.fromJson(s as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 前端 - 详情页渲染订阅者
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart`
|
||||
|
||||
- [ ] **Step 1: 在 _buildMetaSurface 或新方法中渲染订阅者**
|
||||
|
||||
```dart
|
||||
Widget _buildSubscribersSurface(ScheduleItemModel event) {
|
||||
if (event.subscribers.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: _colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: _colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailSubscribers,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
...event.subscribers.map((sub) => _buildSubscriberRow(sub)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubscriberRow(Subscriber subscriber) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
|
||||
child: Row(
|
||||
children: [
|
||||
Avatar(url: subscriber.avatarUrl, name: subscriber.username),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(child: Text(subscriber.username)),
|
||||
if (subscriber.canEdit) Icon(Icons.edit, size: 16),
|
||||
if (subscriber.canInvite) Icon(Icons.person_add, size: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 build 方法中添加**
|
||||
|
||||
```dart
|
||||
// 在 _buildExtraSurface 之后添加
|
||||
if (event.subscribers.isNotEmpty) [
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildSubscribersSurface(event),
|
||||
],
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 添加 l10n 文本**
|
||||
|
||||
```arb
|
||||
// apps/lib/l10n/app_zh.arb
|
||||
"calendarDetailSubscribers": "已订阅 ({count}人)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 验证
|
||||
|
||||
- [ ] 运行后端测试: `cd backend && uv run pytest -v -k "subscriber"`
|
||||
- [ ] 运行前端分析: `cd apps && flutter analyze`
|
||||
- [ ] 手动测试分享流程
|
||||
@@ -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=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 显示,不参与权限判断
|
||||
@@ -52,7 +52,7 @@ Base URL: `/api/v1/agent`
|
||||
|
||||
## 2) GET `/runs/{thread_id}/events`
|
||||
|
||||
订阅指定 thread 的实时事件流。
|
||||
订阅指定 thread 的实时事件流(按 `runId` 隔离当前 run)。
|
||||
|
||||
### Path
|
||||
|
||||
@@ -64,6 +64,7 @@ Base URL: `/api/v1/agent`
|
||||
|
||||
| 参数 | 类型 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `runId` | string | - | 目标 run ID(必填)。SSE 仅输出该 run 事件 |
|
||||
| `idle_limit` | integer | `300` | 最大空闲轮询次数(1-3600) |
|
||||
|
||||
### Headers
|
||||
@@ -81,6 +82,11 @@ Base URL: `/api/v1/agent`
|
||||
- usage 审计与成本回退策略见 `docs/protocols/agent/sse-events.md`(5) Usage 审计协议)
|
||||
- 空闲时会发送 keep-alive 注释行 `: keep-alive`
|
||||
|
||||
run 过滤语义:
|
||||
|
||||
- 服务端会读取 thread 的事件流游标,但仅向客户端发送 `event.runId == query.runId` 的事件。
|
||||
- SSE 连接终止条件为“目标 run 收到 `RUN_FINISHED` 或 `RUN_ERROR`”,其他 run 的 terminal 事件不会终止当前连接。
|
||||
|
||||
当前阶段执行说明:
|
||||
|
||||
- `chat` 模式采用两阶段:`router` -> `worker`。
|
||||
@@ -91,6 +97,7 @@ Base URL: `/api/v1/agent`
|
||||
|
||||
- `401` 未认证
|
||||
- `403` 非会话所有者
|
||||
- `422` `runId` 非法
|
||||
- `422` `Last-Event-ID` 非法
|
||||
- `429` 超过 SSE 连接数限制
|
||||
|
||||
@@ -279,6 +286,7 @@ Agent 路由的错误同样遵循统一 HTTP 错误契约,详见:
|
||||
|
||||
- `AGENT_RUN_INPUT_INVALID`
|
||||
- `AGENT_RUN_MESSAGES_INVALID`
|
||||
- `AGENT_INVALID_RUN_ID`
|
||||
- `AGENT_INVALID_LAST_EVENT_ID`
|
||||
- `AGENT_SSE_CONNECTION_LIMIT`
|
||||
- `AGENT_ATTACHMENT_EMPTY`
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
本文档描述 `GET /api/v1/agent/runs/{thread_id}/events` 的事件协议。
|
||||
|
||||
> 当前协议要求 SSE 订阅显式携带 `runId`,事件输出按 run 隔离。
|
||||
|
||||
---
|
||||
|
||||
## 1) 事件管道
|
||||
@@ -13,7 +15,7 @@
|
||||
3. 事件同时:
|
||||
- 持久化到数据库(用于 history)
|
||||
- 发布到 Redis Stream(用于 SSE)
|
||||
4. `/runs/{thread_id}/events` 从 Redis Stream 读取并输出 SSE
|
||||
4. `/runs/{thread_id}/events` 从 Redis Stream 读取并输出 SSE(仅输出目标 `runId` 事件)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,6 +32,7 @@ data: <json>
|
||||
|
||||
- `id` 可用于断点续流(`Last-Event-ID`)
|
||||
- `event` 与 JSON 内 `type` 一致(例如 `RUN_STARTED`)
|
||||
- 仅当 payload 中 `runId` 与 query `runId` 一致时才会对外发送
|
||||
- 空闲时可能出现 keep-alive 注释帧:
|
||||
|
||||
```text
|
||||
@@ -41,6 +44,15 @@ data: <json>
|
||||
|
||||
## 3) 事件类型(当前实现)
|
||||
|
||||
### 3.0 过滤与终止规则
|
||||
|
||||
- 请求参数 `runId` 为本次订阅目标 run。
|
||||
- 服务端只转发 `event.runId == runId` 的事件。
|
||||
- SSE 连接仅在目标 run 收到 terminal 事件后结束:
|
||||
- `RUN_FINISHED`
|
||||
- `RUN_ERROR`
|
||||
- 其他 run 的事件(包括 terminal)不会结束当前连接。
|
||||
|
||||
### 3.1 Run 生命周期
|
||||
|
||||
#### `RUN_STARTED`
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# 客户端缓存键作用域规范(Cache Key Scoping)
|
||||
|
||||
## 目标
|
||||
|
||||
防止同一设备多账号切换时出现缓存串读(A 账号数据在 B 账号展示)。
|
||||
|
||||
本规范适用于 `apps/lib/data/cache/**` 以及所有通过 `CachedRepository` 读写的业务缓存。
|
||||
|
||||
---
|
||||
|
||||
## 作用域模型
|
||||
|
||||
### 1) 作用域级别
|
||||
|
||||
- `anonymous`:未登录态(或未绑定用户上下文)
|
||||
- `user:<user_id>`:已登录用户态
|
||||
|
||||
### 2) 键格式
|
||||
|
||||
客户端持久缓存最终键必须按以下格式生成:
|
||||
|
||||
`cache:<scope>:<feature-key>`
|
||||
|
||||
其中 `<scope>` 可以附带会话代际后缀(推荐):
|
||||
|
||||
- `user:<user_id>:v<epoch>`
|
||||
- `anonymous:v<epoch>`
|
||||
|
||||
该后缀用于切号并发场景,确保旧会话异步回写落在旧命名空间。
|
||||
|
||||
示例:
|
||||
|
||||
- `cache:user:8ef4...:chat:history:first:default`
|
||||
- `cache:user:8ef4...:v12:chat:history:first:default`
|
||||
- `cache:user:8ef4...:v12:calendar:day:2026-03-29`
|
||||
- `cache:anonymous:v13:inbox:list:all`
|
||||
|
||||
---
|
||||
|
||||
## 责任边界
|
||||
|
||||
### 基础层(必须)
|
||||
|
||||
- `CachedRepository` 负责统一附加 `<scope>` 前缀。
|
||||
- Feature Repository 只声明业务键(`<feature-key>`),不得手工拼接 userId 前缀。
|
||||
|
||||
### 应用层(必须)
|
||||
|
||||
- 在认证状态变化时更新当前缓存作用域:
|
||||
- 登录成功 -> `user:<user_id>`
|
||||
- 登出/失效 -> `anonymous`
|
||||
|
||||
---
|
||||
|
||||
## 并发与切号安全
|
||||
|
||||
- 切号后,旧账号异步请求结果不得回写到新账号 UI 状态。
|
||||
- 推荐使用会话代际(epoch/token)保护异步回写。
|
||||
- 缓存分区与 UI 状态隔离必须同时存在:
|
||||
- 仅有分区,无代际保护:仍可能出现瞬时回流显示。
|
||||
- 仅有代际保护,无分区:仍可能读取到旧持久缓存。
|
||||
|
||||
---
|
||||
|
||||
## 兼容与迁移策略
|
||||
|
||||
### 向后兼容
|
||||
|
||||
- 旧无作用域键允许保留在本地存储中,不参与新读取路径。
|
||||
- 新版本只读取带 `cache:<scope>:` 前缀的键。
|
||||
|
||||
### 迁移方式
|
||||
|
||||
- 采用增量迁移(additive),不执行强制删除旧键。
|
||||
- 如需清理旧键,必须通过统一维护任务处理,不在功能逻辑中零散实现。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 同设备 A/B 账号来回切换,不出现跨账号历史/列表串读。
|
||||
2. 登录后首次读取命中当前用户作用域键;登出后读取命中匿名作用域键。
|
||||
3. Feature 仓库不再自行实现 userId 拼 key 逻辑。
|
||||
@@ -51,6 +51,7 @@ When creating/modifying/deprecating any code, this table must be updated in the
|
||||
|---|---|---:|---|
|
||||
| `AGENT_RUN_INPUT_INVALID` | agent | 422 | Run input payload invalid |
|
||||
| `AGENT_RUN_MESSAGES_INVALID` | agent | 422 | Run messages contract invalid |
|
||||
| `AGENT_INVALID_RUN_ID` | agent | 422 | SSE runId query invalid |
|
||||
| `AGENT_INVALID_LAST_EVENT_ID` | agent | 422 | SSE Last-Event-ID invalid |
|
||||
| `AGENT_SSE_CONNECTION_LIMIT` | agent | 429 | SSE connections exceed per-user limit |
|
||||
| `AGENT_ATTACHMENT_EMPTY` | agent | 422 | Attachment payload empty |
|
||||
@@ -114,6 +115,7 @@ When creating/modifying/deprecating any code, this table must be updated in the
|
||||
| `SCHEDULE_ITEM_PAGE_INVALID` | schedule_items | 400 | Pagination `page` must be greater than or equal to 1 |
|
||||
| `SCHEDULE_ITEM_PAGE_SIZE_INVALID` | schedule_items | 400 | Pagination `page_size` out of allowed range |
|
||||
| `SCHEDULE_ITEM_SHARE_FORBIDDEN` | schedule_items | 403 | Current user cannot share this schedule item |
|
||||
| `SCHEDULE_ITEM_FORBIDDEN` | schedule_items | 403 | Current user does not have permission to edit this schedule item |
|
||||
| `SCHEDULE_ITEM_SHARE_PERMISSION_EXCEEDED` | schedule_items | 403 | Requested share permission exceeds inviter permission |
|
||||
| `SCHEDULE_ITEM_SUBSCRIPTION_ALREADY_ACTIVE` | schedule_items | 400 | Recipient already has active subscription |
|
||||
| `SCHEDULE_ITEM_INVITE_ALREADY_SUBSCRIBED` | schedule_items | 400 | Recipient already accepted calendar invite |
|
||||
@@ -172,6 +174,7 @@ Exit code policy:
|
||||
|
||||
- `AGENT_RUN_INPUT_INVALID`
|
||||
- `AGENT_RUN_MESSAGES_INVALID`
|
||||
- `AGENT_INVALID_RUN_ID`
|
||||
- `AGENT_INVALID_LAST_EVENT_ID`
|
||||
- `AGENT_SSE_CONNECTION_LIMIT`
|
||||
- `AGENT_ATTACHMENT_EMPTY`
|
||||
|
||||
@@ -162,6 +162,12 @@ interface UiBadgeNode extends UiBaseNode {
|
||||
}
|
||||
```
|
||||
|
||||
`label` contract:
|
||||
- Backend SHOULD return stable i18n tokens for status badges: `ui.status.info|success|warning|error|pending`
|
||||
- Frontend is responsible for localizing these tokens by current locale
|
||||
- Backward compatibility: frontend SHOULD still tolerate legacy uppercase labels (`INFO/SUCCESS/...`) during migration
|
||||
- Unknown token fallback: frontend SHOULD keep original label (no semantic remap to other statuses)
|
||||
|
||||
#### 4. Button Node
|
||||
|
||||
```typescript
|
||||
@@ -455,7 +461,7 @@ interface UiIconSpec {
|
||||
},
|
||||
{
|
||||
"type": "badge",
|
||||
"label": "ERROR",
|
||||
"label": "ui.status.error",
|
||||
"status": "error"
|
||||
}
|
||||
]
|
||||
@@ -729,7 +735,7 @@ UiSchemaRenderer (for frontend rendering)
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{"type": "text", "content": "日程已创建", "role": "title"},
|
||||
{"type": "badge", "label": "SUCCESS", "status": "success"}
|
||||
{"type": "badge", "label": "ui.status.success", "status": "success"}
|
||||
],
|
||||
"justify": "space-between",
|
||||
"align": "center"
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Frontend Cache + SWR + Boot Prewarm Implementation Plan (Updated)
|
||||
|
||||
> This document replaces the previous version and removes outdated path references.
|
||||
|
||||
## Current Architecture Baseline
|
||||
|
||||
- Shared infrastructure is under `apps/lib/data/` only:
|
||||
- cache: `apps/lib/data/cache/`
|
||||
- network: `apps/lib/data/network/`
|
||||
- storage: `apps/lib/data/storage/`
|
||||
- Feature business repositories/models live under each feature:
|
||||
- calendar: `apps/lib/features/calendar/data/repositories/`, `apps/lib/features/calendar/data/models/`
|
||||
- messages: `apps/lib/features/messages/data/repositories/`, `apps/lib/features/messages/data/models/`
|
||||
- contacts: `apps/lib/features/contacts/data/repositories/`, `apps/lib/features/contacts/data/models/`
|
||||
- chat history cache repo: `apps/lib/features/chat/data/repositories/chat_history_repository.dart`
|
||||
|
||||
## Scope
|
||||
|
||||
1. Keep cache infra generic and reusable.
|
||||
2. Keep TTL policy defined per feature repository (not centralized in shared cache).
|
||||
3. Keep boot prewarm bounded by timeout with cache-first UX.
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task A: Cache Infra Stability
|
||||
|
||||
**Files:**
|
||||
- `apps/lib/data/cache/cache_store.dart`
|
||||
- `apps/lib/data/cache/cache_policy.dart`
|
||||
- `apps/lib/data/cache/cached_repository.dart`
|
||||
|
||||
**Checks:**
|
||||
- `flutter test apps/test/data/cache/cached_repository_test.dart`
|
||||
- `flutter test apps/test/data/cache/hybrid_cache_store_test.dart`
|
||||
- `flutter test apps/test/data/cache/shared_prefs_cache_store_test.dart`
|
||||
|
||||
### Task B: Feature Repositories Use Local TTL Policies
|
||||
|
||||
**Files:**
|
||||
- `apps/lib/features/todo/data/repositories/todo_repository.dart`
|
||||
- `apps/lib/features/messages/data/repositories/inbox_repository.dart`
|
||||
- `apps/lib/features/contacts/data/repositories/friend_repository.dart`
|
||||
- `apps/lib/features/settings/data/repositories/user_profile_cache_repository.dart`
|
||||
- `apps/lib/features/chat/data/repositories/chat_history_repository.dart`
|
||||
- `apps/lib/features/calendar/data/repositories/calendar_repository.dart`
|
||||
|
||||
**Checks:**
|
||||
- `flutter test apps/test/data/repositories/shared_repositories_test.dart`
|
||||
- `flutter test apps/test/features/chat/data/repositories/chat_history_repository_test.dart`
|
||||
- `flutter test apps/test/features/settings/data/repositories/user_profile_cache_repository_test.dart`
|
||||
|
||||
### Task C: Auth Boot Prewarm Gate
|
||||
|
||||
**Files:**
|
||||
- `apps/lib/app/services/app_prewarm_orchestrator.dart`
|
||||
- `apps/lib/app/router/app_router.dart`
|
||||
- `apps/lib/features/auth/presentation/screens/auth_boot_screen.dart`
|
||||
- `apps/lib/app/di/injection.dart`
|
||||
|
||||
**Checks:**
|
||||
- `flutter test apps/test/app/services/app_prewarm_orchestrator_test.dart`
|
||||
- `flutter test apps/test/app/router/app_router_redirect_test.dart`
|
||||
|
||||
### Task D: Regression Safety
|
||||
|
||||
**Checks:**
|
||||
- `flutter analyze`
|
||||
- Run all tests from Tasks A/B/C together before finalizing.
|
||||
|
||||
## Notes
|
||||
|
||||
- Any old path references from previous plan versions are obsolete.
|
||||
- Notification/reminder data-interaction services are intentionally removed for separate rewrite.
|
||||
Reference in New Issue
Block a user