diff --git a/docs/api-phase1.md b/docs/api-phase1.md index 82124e0..e3c9e2b 100644 --- a/docs/api-phase1.md +++ b/docs/api-phase1.md @@ -113,10 +113,24 @@ POST /app/auth/register | `targetType=PHONE` | 维持原状:`code` **必填** | | 邮箱规范化 | 去空格 + 转小写(`batch1@Example.com ` → `batch1@example.com`);格式不合法直接 `400 邮箱格式不正确` | | 密码强度 | 8–64 位,且**同时包含字母与数字**(不强制特殊字符:低端机输入成本高,拦机器注册靠频控) | -| 注册频控 | 同 IP 24h ≤ **5** 个、同设备 24h ≤ **3** 个,超出返回 `429`(`TOO_MANY_REQUESTS`) | +| 注册频控 | 同 IP / 同设备 24h 默认上限分别为 5 / 3 个,超出返回 `429`(`TOO_MANY_REQUESTS`) | | 邮箱验证状态 | 免码注册时 `app_user_credential.verified = 0`(保留"待验证"),**不阻塞登录**;只有**验证码登录**会把 `verified` 置 1,密码登录不改变该状态 | -频控口径落库在 `app_user.register_ip` / `register_device_id` 上(按 24h 计数,不新建计数表)。 +频控口径落库在 `app_user.register_ip` / `app_user.register_device_id` 上(按 24h 计数,不新建计数表)。 + +注册频控阈值存储在 `sys_config`,可在后台「系统配置」修改: + +```text +app.register.max-per-ip-per-day +app.register.max-per-device-per-day +``` + +运行时优先读取 `sys_config`。键缺失或值不是整数时,回退 `application.yml` +中的 `fruitshorts.auth.register-max-per-ip-per-day` / +`fruitshorts.auth.register-max-per-device-per-day`,缺省为 5 / 3; +值小于等于 0 表示对应维度不限制。 +开发种子配置为 0 / 0,生产建议恢复为 5 / 3。 + **残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量; 真要防住需要接图形/行为验证码或风控服务,本阶段未做。验证码通道目前是 `LogOtpSender`(只写日志不发信), 所以邮箱**可达性**未验证 —— 这也是"不阻塞注册"的前提。 diff --git a/docs/plans/2026-10-16-K1.md b/docs/plans/2026-10-16-K1.md new file mode 100644 index 0000000..51cf06b --- /dev/null +++ b/docs/plans/2026-10-16-K1.md @@ -0,0 +1,95 @@ +# K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整 —— 计划(2026-10-16) + +> 依据:`docs/plans/INBOX.md` 19:40 条目。用户指令:「现在你要做的事对接客户端接口及 UI 调整, +> 仍然严格按照 specs 执行」→ 本轮做客户端 `ReelShort`(Flutter),目标是一条完整看剧闭环: +> **首页点进剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏**,全链路走真接口。 +> 契约以 `FruitServer/docs/api-phase1.md` §1.4 / §1.5 为准。 + +## 一、勘察更正(先说清,免得后面对不上) + +条目里写「客户端只调了 6 个接口、没有详情和票据」—— 这条**不准确**,实测: + +| 接口 | 现状 | +| --- | --- | +| `GET /app/dramas/{id}` | **已接**:`CatalogApi.dramaDetail` + `CatalogController.requireDetail`,详情页在用 | +| `GET /app/episodes/{id}/play` | **半接**:`CatalogApi.playTicket` + `CatalogController.resolvePlay` 存在,但**没有任何页面调用**;详情下发的 `playUrl`(免费集已带签名)被直接使用 | +| `/app/watch/*` | **没接**:`WatchHistoryRepository` 是 `SharedPreferences` 本地实现,`demoProfileId` 假身份 | +| `/app/favorites/*` | **没接**:收藏走本地 `EngagementController`(SharedPreferences),点赞也是本地(点赞属 K3,不动) | + +所以本轮真正要做的是:**播放票据接上并带过期重取**、**进度从本地换远程**、**收藏换服务端**、 +**三态 UI**,以及把详情页已有的加载/失败态补齐(空态、续看进度条)。 + +## 二、方案(分层:api → repository → logic → view) + +### 1. 新增 api 类(`lib/core/data/remote/`) + +| 文件 | 方法 | 对应接口 | +| --- | --- | --- | +| `playback_api.dart`(新增) | `ticket(episodeId)` | `GET /app/episodes/{id}/play` | +| `watch_api.dart`(新增) | `saveProgress` / `progressOfDrama` / `recent` | `POST /app/watch/progress`、`GET /app/watch/progress?dramaId=`、`GET /app/watch/recent?limit=` | +| `library_api.dart`(新增) | `toggleFavorite` / `favorites` / `removeFavorite` | `POST /app/favorites/toggle`、`GET /app/favorites`、`DELETE /app/favorites/{dramaId}` | + +票据调用从 `CatalogApi` **搬**到 `PlaybackApi`(条目要求新增 playback_api), +`CatalogRepository.resolvePlay` 随之移到新的 `PlaybackRepository`——这是本轮唯一"动到既有目录层"的地方,最小接线。 + +### 2. repository(保留抽象,测试注入假实现) + +- `playback_repository.dart`(新增):`ticketFor(episodeId, {forceRefresh})`。 + `HttpPlaybackRepository` 内含**短时缓存**:默认 TTL 10 分钟(签名有效期契约是 15 分钟,留余量), + **时钟与 TTL 可注入** → 过期自动重取,测试用假时钟钉死。 +- `watch_history_repository.dart`(**改签名**):服务端按 `(user, drama)` 存一条续看记录, + 所以接口从 `(profileId, dramaId, episodeId)` 变成 + `progressFor(dramaId)` / `save({episodeId, position, duration, finished})` / `recent(limit)`; + `HttpWatchHistoryRepository` + `InMemoryWatchHistoryRepository`(测试用); + `demoProfileId` 删除;`clampResumePosition` 保留(续看钳位规则不变)。 +- `favorite_repository.dart`(新增):`toggle` / `list` / `remove`;HTTP + 内存实现。 + +### 3. controller / logic + +- `favorite_controller.dart`(新增):`savedIds` + `cards` + `loading/error`, + `toggle(dramaId)` **乐观更新 → 失败回滚**(`DELETE`/`toggle` 任一失败都回到原状态)。 +- `DramaDetailLogic` / `MineLogic` / `PlayerLogic`:改用新的 `WatchHistoryRepository`, + 续看点从 `WatchProgress(episodeId, positionMs)` 来(不再是本地 episodeId 字符串)。 +- `PlayerLogic`: + - 起播前 `ticketFor(episode)`,把签名地址写回 session 的剧集对象;**地址原样交给播放器** + (`AppConfig.mediaUrl` 只补 host,不动 path/query —— 新增测试钉住 `?e=&s=` 原样保留); + - 票据拿不到 / `accessType=COIN_REQUIRED|NO_MEDIA` → 由**后端返回值**驱动 UI(锁定/无片源提示 + 重试); + - 进度上报**节流 10 秒**(原实现是 5 秒),切集/退出/切后台各补一次,`finished` 在播完时上报。 + +### 4. UI + +- 详情页:加载中 / 失败(可重试)/ 空剧集三态;续看位置在"继续观看"按钮与对应剧集行上给进度条。 +- 播放页:票据加载中 / 失败(可重试)/ 锁定(`accessType` 驱动)三态;收藏按钮真实状态 + 回滚 + + 失败提示(未登录先引导登录,沿用现有 `ensureSignedIn`)。 +- 我的页:最近观看卡片三态(服务端续看位置驱动)、收藏列表三态,去掉 `demoProfileId` 假身份。 +- 文案:新增键写进 `app_en.arb` / `app_zh.arb` / `app_id.arb`,`flutter gen-l10n` 重新生成。 + +## 三、影响面清单 + +新增:`playback_api.dart`、`watch_api.dart`、`library_api.dart`、`watch_progress.dart`(模型)、 +`playback_repository.dart`、`favorite_repository.dart`、`favorite_controller.dart`。 + +改:`watch_history_repository.dart`(签名)、`catalog_api.dart`(去掉 playTicket)、 +`catalog_repository.dart`(去掉 resolvePlay)、`catalog_controller.dart`(同步)、 +`play_ticket.dart`(补 dramaId/number/previewSeconds)、`app.dart`(注入三个仓库 + FavoriteController)、 +`drama_detail_{logic,view,binding}.dart`、`mine_{logic,view,binding}.dart`、 +`player_{logic,view,binding}.dart`、`discover_{logic,binding}.dart`(续看点来源)、 +`app_{en,zh,id}.arb` + 生成的 4 个 l10n 文件。 + +测试:改 `test/support/fake_watch_history.dart`、`test/playback/watch_history_test.dart`、 +`test/catalog/catalog_controller_test.dart`、`test/support/fake_catalog.dart`、 +`test/{discover,mine,drama_detail,player}/…`(续看点改签名);新增 +`playback_ticket_expiry_test.dart`、`progress_throttle_test.dart`、`watch_history_remote_test.dart`、 +`favorite_controller_test.dart`、`player_ticket_gate_test.dart`、`integration/live_watch_flow_test.dart`。 + +## 四、风险 / 待确认 + +1. **改动面比"只加接口"大**:`WatchHistoryRepository` 的签名必须改(服务端是"每剧一条续看记录", + 没有 profile 概念),这会连带改 4 个 logic + 6 个测试文件。这是接口模型决定的,不是顺手重构。 +2. **未登录的边界**:`/app/watch/*` 与 `/app/favorites/*` 需要登录态。客户端策略: + 未登录时**不发**这些请求(浏览/试看仍可匿名),收藏按钮沿用"先引导登录"。 +3. **票据过期无法从响应判断**(契约里没有 expiresAt 字段),所以用**客户端 TTL(10 分钟)**保守失效重取; + 若后端后续补 `expiresAt`,直接换成按字段判断即可(口径已写进计划)。 +4. **`/app/watch/recent` 只给 dramaId/episodeId/位置**,没有剧名封面 —— K1 里"我的"页用 + 已加载的首页/发现卡片按 id 映射显示;映射不到就不显示(K2 的记录页再做成独立请求)。 +5. 联调测试需要登录态:新增的 live 测试会**注册一个一次性邮箱账号**,跑完在库里清掉(回执写明)。 diff --git a/docs/plans/2026-10-17-K10.md b/docs/plans/2026-10-17-K10.md new file mode 100644 index 0000000..ac111bf --- /dev/null +++ b/docs/plans/2026-10-17-K10.md @@ -0,0 +1,90 @@ +# 2026-10-17 K10 统一空列表占位(EmptyState) + +## 一、目标与边界 + +新建统一 `EmptyState` 组件,替换任务书列出的 10 处纯文字空态,保留现有空态 +文案、按钮行为和错误态逻辑。只改空态视觉、三语 l10n 与测试,不改数据、接口、 +列表加载/失败逻辑或页面信息架构。 + +- 客户端分支:`feature/flutter-k10-empty-states`,基线 `develop 19babb8`。 +- 文档分支:`docs/k10-empty-states`,FruitServer 基线 K10 任务书 `3f84bc6`。 +- 不引入图片、SVG、字体或第三方依赖。 +- 不跑真机,设备侧深色/浅色、小屏和页面状态由用户验收。 + +## 二、组件设计 + +`lib/core/widgets/empty_state.dart`: + +1. 默认居中,最大宽度 280,外层 Padding 24。 +2. 默认图形 96x96、圆角 28,背景 `AppColors.blue` 透明度 0.11;图标 42。 +3. 深色图标色 `AppColors.blueSoft`,浅色图标色 `AppColors.blue`。 +4. compact 图形 64x64、圆角 18、图标 28,用于评论弹窗。 +5. 标题 16/w600(compact 14)、最多 2 行;可选副标题 13、`muted`、最多 2 行。 +6. 可选主色 TextButton,最小点击区域 44x44;保留调用方动作。 +7. 外层 `Semantics(label: title)`;图形、标题和副标题文本使用 + `ExcludeSemantics`,避免重复朗读,操作按钮仍保持可访问。 +8. 稳定 Key:`empty-state-art`、`empty-state-icon`、`empty-state-title`、 + `empty-state-action`。 + +新增通用副标题 `emptyHint`: + +- zh:有新内容时会显示在这里。 +- en:New content will appear here. +- id:Konten baru akan muncul di sini. + +## 三、10 处替换与图标 + +| 位置 | 图标 | 行为 | +|---|---|---| +| 首页 noStories | `movie_outlined` | 保留重试 | +| 首页分类空 | `grid_view_rounded` | 无新增动作 | +| 首页搜索空 | `search_off_rounded` | 无新增动作 | +| 收藏 noFavorites | `favorite_border_rounded` | 原按钮不变 | +| 通知 noNotices | `notifications_none_rounded` | 原下拉刷新不变 | +| 详情 noEpisodesYet | `video_library_outlined` | 保留重试 | +| 我的-片单 noSavedShows | `bookmark_border_rounded` | 无新增动作 | +| 我的-最近剧目 noStories | `history_rounded` | 无新增动作 | +| 下载 noDownloads | `download_outlined` | 原清空按钮不变 | +| 观看记录 noWatchRecords | `history_rounded` | 原清空/刷新逻辑不变 | +| 评论 noComments | `chat_bubble_outline_rounded` | compact | + +首页错误态继续使用 `error_outline_rounded` 和 `catalogError`;分类、收藏、通知、 +记录、评论等失败态继续使用原错误图标/重试,不允许用 `EmptyState` 冒充失败。 + +## 四、影响文件 + +- `lib/core/widgets/empty_state.dart`(新增) +- `lib/pages/home/home_view.dart` +- `lib/pages/discover/widgets/home_drama_grid.dart` +- `lib/pages/favorites/favorites_page.dart` +- `lib/pages/notices/notices_page.dart` +- `lib/pages/drama_detail/drama_detail_view.dart` +- `lib/pages/mine/mine_view.dart` +- `lib/pages/downloads/downloads_page.dart` +- `lib/pages/watch_records/watch_records_page.dart` +- `lib/pages/player/widgets/drama_comments_sheet.dart` +- `lib/l10n/app_en.arb`、`app_zh.arb`、`app_id.arb` 与生成文件 +- 组件测试及收藏/下载/记录/通知/评论页面空态测试 +- `FruitServer/docs/plans/2026-10-17-K10.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md` + +## 五、TDD 与门禁 + +1. 先新增 `EmptyState` 组件测试和五处页面空态测试,确认组件不存在时红灯。 +2. 实现组件并替换 10 处空态后跑定向测试绿灯。 +3. 运行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +4. 构建前检查并发 `flutter run/build`;当前有用户 iOS `flutter run` 会话, + 不主动终止,若仍未结束则等待其退出后再构建。 +5. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 六、风险与待确认 + +- 首页当前把 `catalog.error` 与空列表放在同一分支,需先拆出错误态,再单独用 + `EmptyState` 渲染 noStories,避免错误态被误认为空态。 +- 部分空态位于 Sliver、固定高度滚动区或弹窗内,需保持可滚动和居中。 +- 组件外层统一读标题,若操作按钮位于同一 Semantics 容器内,必须确保按钮自身 + 语义未被 `ExcludeSemantics` 吞掉。 +- 最大宽度 280 与 compact 64 图标容器需在 320px 宽度和浅/深色主题下无溢出。 diff --git a/docs/plans/2026-10-17-K11.md b/docs/plans/2026-10-17-K11.md new file mode 100644 index 0000000..412fb96 --- /dev/null +++ b/docs/plans/2026-10-17-K11.md @@ -0,0 +1,63 @@ +# 2026-10-17 K11 首页网格 3 列 + 分类首位「全部」 + +## 一、目标与边界 + +在 K9 首页「全部剧目」网格上做最小修订:每行从 2 个改为 3 个,分类首个仍绑定 +`CategoryController.allCode`,文案改为「全部 / All / Semua」。 + +- 客户端分支:`feature/flutter-k11-home-grid-3col`,基线 `develop 0466b4b`。 +- 文档分支:`docs/k11-home-grid-3col`,FruitServer 基线任务书 `2a06190`。 +- 不改轮播、空态、三态、数据源、接口、分页或其他页面。 +- 不跑真机;设备侧 3 列布局、分类切换和详情跳转由用户验收。 + +## 二、实现方案 + +1. `home_drama_grid.dart`: + - `crossAxisCount: 3` + - `mainAxisSpacing: 14` + - `crossAxisSpacing: 10` + - `childAspectRatio: 2 / 3` 不变 +2. 3 列下卡片仍只保留标题最多 2 行、类型最多 1 行;由现有 `Text` 的 + `maxLines` / `ellipsis` 保证不溢出。 +3. `discover_view.dart` 分类第一项 code 保持 `CategoryController.allCode`, + label 从 `l10n.forYou` 改为 `l10n.all`。 +4. 已核对三份 ARB 已存在 `all` key: + - en:All + - zh:全部 + - id:Semua + +因此直接复用现有 l10n,不重复插入 JSON key;热门、最新和服务端分类顺序不变。 + +## 三、测试 + +- 更新 K9 网格断言:`crossAxisCount == 3`、`crossAxisSpacing == 10`、 + `mainAxisSpacing == 14`、比例仍为 2/3。 +- 新增首个分类 chip 文案断言为 `All`;切换分类时仍能更新网格。 +- K9/K10 既有测试保留,不删除用例;只更新本次行为明确变化的参数断言。 +- 组件/页面空态测试保持通过。 + +## 四、影响文件 + +- `ReelShort/lib/pages/discover/widgets/home_drama_grid.dart` +- `ReelShort/lib/pages/discover/discover_view.dart` +- `ReelShort/test/discover/home_all_dramas_grid_test.dart` +- `FruitServer/docs/plans/2026-10-17-K11.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md` + +## 五、门禁与 Git + +1. 先更新测试取得与 3 列/全部文案相关的红灯。 +2. 实现后运行定向测试。 +3. 运行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +4. 当前有 Android `flutter run` 会话,不主动终止;构建前检查进程,必要时使用 + 独立工作区副本构建,避免争用原项目 `build/`。 +5. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 六、风险与待确认 + +- 3 列后单卡宽度变窄,长标题和类型必须继续 ellipsis,不允许改变卡片结构。 +- 现有 `all` l10n 已具备三语,新增重复 key 会破坏 ARB;本批复用现有 key。 +- 用户正在运行的 Flutter `run` 不终止;APK 构建需隔离原 `build/`。 diff --git a/docs/plans/2026-10-17-K12.md b/docs/plans/2026-10-17-K12.md new file mode 100644 index 0000000..8498240 --- /dev/null +++ b/docs/plans/2026-10-17-K12.md @@ -0,0 +1,79 @@ +# 2026-10-17 K12 首页去标题 + 空态视觉重做 + +## 一、目标与边界 + +在 K11 首页基础上做两项视觉修订: + +1. 删除首页「全部剧目」标题,分类 chips 后直接显示网格。 +2. 将 `EmptyState` 从单个实心圆角块改为轻盈的双层同心圆。 + +只改首页间距、`EmptyState` 视觉与对应测试,不改接口、数据、轮播、三态或其他页面。 + +- 客户端分支:`feature/flutter-k12-home-empty-polish`,基线 `develop b5a8656`。 +- 文档分支:`docs/k12-home-empty-polish`,FruitServer 基线任务书 `87a37e9`。 +- 不引入图片、SVG、字体或动画;不跑真机。 + +## 二、首页去标题 + +- 删除 `Key('home-all-dramas-title')` 的 Text。 +- 删除标题与 chips 之间、标题与网格之间的多余间距。 +- chips 与网格间距调整为 14px。 +- 网格、数据、空态和分类切换逻辑保持不变。 + +## 三、EmptyState 双层同心圆 + +默认: + +- 外层直径 84,blue 5.5% 填充,1px blue 11% 描边。 +- 内层直径 56,blue 11% 填充。 +- 图标 27px,深色 `blueSoft` / 浅色 `blue`。 +- 标题 15/w600,与图形间距 14。 +- 副标题 12.5 muted,行高 1.45,与标题间距 6。 +- TextButton 13 主色,点击区至少 44,与副标题间距 8。 +- 上下 Padding 28,最大宽度 260。 + +compact: + +- 外层 60、内层 40、图标 21。 +- 图形与标题间距 10,上下 Padding 16。 + +保留外层 Key `empty-state-art`,新增内层 Key `empty-state-art-inner`;继续使用 +`empty-state-icon/title/action`。外层 Semantics 仍只读标题。 + +## 四、测试 + +- 更新组件测试:外/内层尺寸、图标尺寸、标题 15、副标题 12.5、按钮 44、 + 最大宽 260、compact 尺寸与深浅主题颜色。 +- 首页测试:`home-all-dramas-title` 不存在;chips 存在后直接能找到 + `home-all-dramas-grid`。 +- K9/K10/K11 既有用例保留,不删除测试文件;只更新本次明确变化的视觉/锚点断言。 +- 既有壳层测试中若用「All Dramas」作为页面定位锚点,改用当前稳定锚点 + `All` chip,不降低导航流程覆盖。 + +## 五、影响文件 + +- `ReelShort/lib/core/widgets/empty_state.dart` +- `ReelShort/lib/pages/discover/discover_view.dart` +- `ReelShort/test/empty_state/empty_state_test.dart` +- `ReelShort/test/discover/home_all_dramas_grid_test.dart` +- `ReelShort/test/widget_test.dart`(仅稳定锚点更新) +- `FruitServer/docs/plans/2026-10-17-K12.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md` + +## 六、门禁与 Git + +1. 先更新组件与首页测试,取得标题仍存在、空态仍是旧尺寸的红灯。 +2. 实现后跑定向测试。 +3. 运行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +4. 当前已确认真机会话停止且无并发构建;构建前再次检查。 +5. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 七、风险与待确认 + +- 删除标题后,部分既有测试用「All Dramas」文本定位页面;需改为稳定的 + `All` chip 或网格 Key,保持原测试流程。 +- 双层圆使用固定尺寸和 Stack,小屏需保证 84/60 不溢出。 +- 副标题 12.5 与按钮 13 是视觉规格;测试按 double 精确断言。 diff --git a/docs/plans/2026-10-17-K13.md b/docs/plans/2026-10-17-K13.md new file mode 100644 index 0000000..177c750 --- /dev/null +++ b/docs/plans/2026-10-17-K13.md @@ -0,0 +1,121 @@ +# 2026-10-17 K13 运行时服务器地址设置(仅 debug) + +## 一、目标与边界 + +解决 debug APK 不带 `--dart-define` 时固定回退到 +`http://jiangdeMacBook-Air.local:8081` 的问题。增加仅 debug 可用的运行时地址 +覆盖,保存后下次启动生效。 + +优先级: + +```text +SharedPreferencesAsync dev.api_base_url + > --dart-define(API_BASE_URL) + > AppConfig 现有默认地址 +``` + +- 客户端分支:`feature/flutter-k13-dev-server-setting`,基线 `b762289`。 +- 文档分支:`docs/k13-dev-server-setting`,FruitServer 基线 `1a5951c`。 +- release 不读取 SharedPreferences 覆盖,也不显示设置入口。 +- 不做环境列表、远程配置、接口或后端改动。 +- 不跑真机。当前有真机 `flutter run` 时,构建使用隔离副本,不终止用户会话。 + +## 二、运行时解析 + +新增 `lib/core/config/server_endpoint.dart`: + +1. 常量 key:`dev.api_base_url`。 +2. `normalize`: + - `trim()`; + - 只接受 `http://` 或 `https://`; + - `Uri.host` 必须非空; + - 去掉末尾所有 `/`。 +3. `resolve`: + - 本地值有效时使用本地值; + - 否则使用有效 dart-define; + - 否则使用现有默认值。 +4. `load({debug, preferences})`: + - debug=false 时立即返回,不创建/读取 SharedPreferences; + - debug=true 时读取并规范化,合法值写入 `AppConfig` 的运行时覆盖。 +5. `save` 与 `clear`: + - save 前规范化,非法值返回 false 且不写; + - clear 删除 key。 + +`AppConfig` 增加运行时覆盖读写入口和编译期/默认地址 getter;`apiBaseUrl` 优先 +返回已加载的运行时值。`main()` 在 `runApp` 前 +`await ServerEndpoint.load(debug: kDebugMode)`,确保 `LumaDramaApp` 创建 +`ApiClient` 前已加载完成。 + +## 三、Debug 设置入口 + +`MineLogic.showLanguage` 增加 `showServerSetting` 参数。我的页设置弹窗中在语言 +列表下方增加「服务器地址」行: + +- 仅 `MyPage.debugShowSetting == true` 时显示,默认值 `kDebugMode`,测试可注入; +- 副标题显示当前 `AppConfig.apiBaseUrl`; +- 点击打开编辑弹窗,预填当前地址; +- 保存合法地址后关闭编辑弹窗并提示“重启 App 生效”; +- 非法地址在输入框显示校验错误,不保存; +- 提供「恢复默认」,清除本地 key,并提示重启生效; +- release 默认不显示,也不触发本地读取。 + +新增三语文案: + +- `serverAddress` +- `serverAddressHint` +- `saveAddress` +- `restartToApply` +- `restoreDefault` +- `invalidServerAddress` + +## 四、测试 + +新增 `test/config/server_endpoint_test.dart`: + +- 本地覆盖 > dart-define > 默认; +- 非法本地值忽略并回落; +- trim、末尾斜杠、http/https; +- `ftp://`、无 scheme、空 host 被拒绝; +- debug=false 不读取覆盖; +- save/clear 对 SharedPreferencesAsync 的行为。 + +新增/扩展设置弹窗测试: + +- debug 注入 true:显示服务器地址行,可打开编辑弹窗; +- 注入 false:不显示入口; +- 非法地址显示错误且不写入。 + +既有测试只更新必要锚点/新增断言,不删除用例。 + +## 五、影响文件 + +- `ReelShort/lib/core/config/server_endpoint.dart`(新增) +- `ReelShort/lib/core/config/app_config.dart` +- `ReelShort/lib/main.dart` +- `ReelShort/lib/pages/mine/mine_view.dart` +- `ReelShort/lib/pages/mine/mine_logic.dart` +- `ReelShort/lib/l10n/app_en.arb`、`app_zh.arb`、`app_id.arb` 与生成文件 +- `ReelShort/test/config/server_endpoint_test.dart`(新增) +- 我的设置弹窗测试 +- `FruitServer/docs/plans/2026-10-17-K13.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md` + +## 六、门禁与 Git + +1. 先写运行时解析与入口可见性测试,确认红灯。 +2. 实现后跑定向测试。 +3. 运行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +4. 当前真机 run 会话不终止;构建在独立副本中进行,原 `build/` 不受影响。 +5. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 七、风险与待确认 + +- `AppConfig.apiBaseUrl` 当前是纯 getter;增加运行时静态状态后,测试必须显式 + 清理,避免用例间污染。 +- release 的“完全不读取”由 `ServerEndpoint.load(debug: kDebugMode)` 和主入口 + 的 debug 参数共同保证;测试使用可注入 debug 参数验证。 +- 保存和恢复默认都只在下次启动生效,当前 ApiClient 不在运行中重建。 +- 设置入口只在 debug 显示,不修改发布包的信息架构。 diff --git a/docs/plans/2026-10-17-K16.md b/docs/plans/2026-10-17-K16.md new file mode 100644 index 0000000..640c39c --- /dev/null +++ b/docs/plans/2026-10-17-K16.md @@ -0,0 +1,40 @@ +# 2026-10-17 K16 注册限流可配置 + +## 范围 + +将 C 端注册的 IP/设备 24 小时限流阈值从硬编码改为后台 `sys_config` 配置: + +```text +app.register.max-per-ip-per-day +app.register.max-per-device-per-day +``` + +运行时优先读取后台配置;键缺失或非整数时回退 `@Value` 注入值(默认 5/3); +值小于等于 0 时跳过对应 guard。注册接口、错误码和错误文案不变。 + +## 实现 + +1. `AppAuthService` 注入 `ConfigService`,从 `sys_config` 优先读取两个键; + 再用 `@Value` 注入 yml 值作为缺失/非法时的 fallback。 +2. `sql/data-phase1.sql` 幂等写入开发默认 0/0 和说明。 +3. `application.yml` 只声明 fallback(默认 5/3),运行时不以后台固化值为准。 +4. `docs/api-phase1.md` §1.1 补充后台配置键、缺省/非法回退和 0=不限制口径。 +5. 测试 mock `ConfigService`,覆盖自定义、0、缺省/非数字回退。 +6. 确认后台「系统设置」页面可访问 `/admin/configs`;若现有表单未展示新键, + 记录为待后续 Web 表单补充,不把配置固化回 yml。 + +## 验证与交付 + +- `mvn test` +- `mvn -pl fruitshorts-app-api -am package -DskipTests` +- 停止当前 8081 服务,启动新 jar,`/healthz` 通过 +- 使用新设备号连续注册至少 4 次,确认不再 429(接口自检) +- 后台把 IP 上限临时改为 2:前两次成功,第三次返回 429;恢复 0 后继续注册成功 +- `feature/backend-register-rate-config-fix` 以 `--no-ff` 合并回 `develop` 并 push +- PROGRESS 记录命令、原始输出和解读 + +## 风险 + +- 配置 0 会关闭注册滥用保护,仅用于当前开发环境;生产需在后台恢复 5/3。 +- 旧 8081 服务重启期间会有短暂不可用。 +- 限流仍基于客户端可伪造 IP/设备号,本次不放宽其他风控。 diff --git a/docs/plans/2026-10-17-K2.md b/docs/plans/2026-10-17-K2.md new file mode 100644 index 0000000..9b49d76 --- /dev/null +++ b/docs/plans/2026-10-17-K2.md @@ -0,0 +1,170 @@ +# K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权管理 + +> 依据:`docs/plans/INBOX.md` 2026-10-17 `[待处理] K2` 条目。 +> 客户端基线:`ReelShort` `develop` / `00280ca`。 +> 接口唯一权威:`FruitServer/docs/api-phase1.md` §0、§1.3、§1.5、§1.6。 +> 本批只做 K2,完成后停下写回执,不连做 K3/K4。 + +## 一、范围与完成口径 + +### K2-A 分类 + +- 接 `GET /app/categories`,分类 id 全程按字符串处理。 +- 发现页分类条改为服务端分类;保留现有分类条样式。 +- 分类内容只从 `GET /app/discover/feed` 的 `sections[]` 中读取 + `code == "category-{id}"` 的分区,不在客户端拼装分类请求。 +- 分类在发现 feed 中没有对应分区时显示空态。 +- 保留 `hot` / `new` 入口与既有样式;其内容同样只按发现 feed 中对应 `code` + 分区读取,缺失时显示空态。 + +### K2-B 观看记录页 + +- 接 `GET /app/watch/recent?limit=20`、`DELETE /app/watch/progress/{dramaId}`、 + `DELETE /app/watch/progress`。 +- 最近记录先去重 `dramaId`,再按 id 补 `GET /app/dramas/{id}` 卡片数据。 +- 详情补取并发上限 4,同页按 `dramaId` 只请求一次。 +- 单条详情失败只影响该条:显示“加载失败”占位与单项重试,其余记录正常渲染。 +- 独立“观看记录”页:封面、剧名、集号、进度、更新时间;点击详情并续看; + 单条删除、清空均有二次确认;删除采用乐观更新,失败回滚并提示。 +- 加载 / 空 / 失败可重试三态;未登录使用现有登录引导。 + +### K2-C 收藏列表页 + +- 复用 `GET /app/favorites`、`DELETE /app/favorites/{dramaId}`、 + `POST /app/favorites/toggle`,不增加分页参数。 +- 独立“我的收藏”页,复用卡片数据模型与列表卡片视觉;点击进详情。 +- 取消收藏采用乐观更新,失败回滚并提示。 +- 加载 / 空 / 失败可重试三态;未登录使用现有登录引导。 + +### K2-D 下载授权管理 + +- 接契约 §1.6 全部 8 个接口: + `GET /app/downloads/policy`、`PUT /app/downloads/settings`、 + `POST /app/downloads`、`GET /app/downloads`、 + `GET /app/downloads/{episodeId}/url`、`POST /app/downloads/{episodeId}/progress`、 + `DELETE /app/downloads/{episodeId}`、`DELETE /app/downloads`。 +- 详情页与播放页增加下载入口;后端拒绝时直接展示后端 `message`。 +- 独立“我的下载”页:策略卡、授权列表、单条删除、清空、仅 Wi-Fi、最高清晰度。 +- 授权状态与任务状态完全使用后端字符串,客户端不新增状态枚举、不自行判断付费/配额。 +- 加载 / 空 / 失败可重试三态;未登录使用现有登录引导。 +- 明确不做:文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验。 + `url` 与 `progress` 本批只提供并测试 API 方法,不驱动实际下载任务。 + +## 二、实施顺序与 TDD + +1. 基线:从 `ReelShort/develop` 建 `feature/flutter-k2-library`。 +2. K2-A:先写分类解析、`category-{id}` 映射、无分区空态和筛选状态测试, + 确认红灯后实现;再跑该块门禁。 +3. K2-B:先写控制器测试覆盖详情去重、并发上限 4、单条失败降级、 + 删除/清空乐观更新与回滚;确认红灯后实现 API、repository、controller、页面; + 再跑门禁。 +4. K2-C:先写独立收藏页取消收藏失败回滚、三态与登录引导测试, + 确认红灯后实现页面与导航;再跑门禁。 +5. K2-D:先写 DTO 字符串 ID/时间解析、策略/列表渲染、授权失败透传、 + 设置更新、删除/清空和未登录测试;确认红灯后实现; + `ApiClient` 仅补契约所需的 `PUT` 能力;再跑门禁。 +6. 三份 ARB 一次补齐并生成 l10n;全量跑 + `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +7. 门禁全绿后合并回 `develop` 并 push;不动 `main`。 +8. 在 `PROGRESS.md` 与 INBOX K2 条目追加回执;真机验证交用户执行。 + +## 三、预计影响文件 + +### 数据层 + +- 修改:`lib/core/data/remote/api_client.dart`(补 `put`) +- 修改:`lib/core/data/models/watch_progress.dart`(补 `updatedAt`) +- 修改:`lib/core/data/remote/watch_api.dart`(补清空) +- 修改:`lib/core/data/repositories/watch_history_repository.dart`(补清空) +- 新增:`lib/core/data/models/content_category.dart` +- 新增:`lib/core/data/remote/category_api.dart` +- 新增:`lib/core/data/repositories/category_repository.dart` +- 新增:`lib/core/data/services/category_controller.dart` +- 新增:`lib/core/data/models/watch_record.dart` +- 新增:`lib/core/data/services/watch_records_controller.dart` +- 新增:`lib/core/data/models/download_policy.dart` +- 新增:`lib/core/data/models/download_license.dart` +- 新增:`lib/core/data/remote/download_api.dart` +- 新增:`lib/core/data/repositories/download_repository.dart` +- 新增:`lib/core/data/services/download_controller.dart` + +### 页面与共享 UI + +- 修改:`lib/app.dart`(新仓库/控制器注入) +- 修改:`lib/core/widgets/auth_gate.dart`(页面级登录引导复用) +- 修改:`lib/pages/discover/discover_logic.dart` +- 修改:`lib/pages/discover/discover_view.dart` +- 修改:`lib/pages/home/home_view.dart`(我的页入口接线) +- 修改:`lib/pages/mine/mine_view.dart`(观看记录、收藏、下载入口) +- 修改:`lib/pages/drama_detail/drama_detail_view.dart`(下载入口) +- 修改:`lib/pages/player/player_view.dart`(当前集下载入口) +- 新增:`lib/pages/watch_records/watch_records_binding.dart` +- 新增:`lib/pages/watch_records/watch_records_view.dart` +- 新增:`lib/pages/favorites/favorites_binding.dart` +- 新增:`lib/pages/favorites/favorites_view.dart` +- 新增:`lib/pages/downloads/downloads_binding.dart` +- 新增:`lib/pages/downloads/downloads_view.dart` +- 视复用需要新增列表卡片/状态组件,预计位于 `lib/core/widgets/` 或各页面 `widgets/`。 + +### 文案 + +- 修改:`lib/l10n/app_en.arb` +- 修改:`lib/l10n/app_zh.arb` +- 修改:`lib/l10n/app_id.arb` +- 重新生成:`lib/l10n/app_localizations*.dart` + +### 测试 + +- 修改:`test/catalog/catalog_controller_test.dart` 及分类筛选相关测试 +- 修改:`test/discover/discover_logic_test.dart` +- 修改:`test/mine/mine_logic_test.dart` +- 修改:`test/library/favorite_controller_test.dart` +- 新增:分类 API/控制器与 section 映射测试 +- 新增:观看记录 API、并发加载、单项降级、删除/清空回滚和页面三态测试 +- 新增:收藏独立页、取消收藏回滚、三态和登录引导测试 +- 新增:下载 DTO/API/controller/page 测试 +- 新增:未登录 `AuthGate` 页面测试 + +## 四、契约缺口(本批登记,不改契约/后端) + +1. `GET /app/watch/recent` 未返回剧名与封面,客户端只能先取最近记录, + 再对去重后的 `dramaId` 调详情补齐卡片;代价是 N+1 请求。 + 建议后续在 `ProgressRespVO` 补卡片字段(需 specs 负责人改契约)。 +2. `GET /app/favorites` 没有分页参数并返回全量数组,与 + `TC-RECORD-04` 的“分页正确”不一致。本批按全量列表实现, + 不自造 `page/size`。 +3. `api-phase1.md` 没有搜索接口,本批不做搜索,也不以本地模糊匹配冒充搜索接口。 + +## 五、风险与控制 + +1. 记录页 N+1 详情请求可能放大流量和延迟:严格去重、并发上限 4、页内缓存, + 单项失败降级,不在失败后自动无限重试。 +2. 页面销毁时异步详情仍可能返回:controller 的 `notify` 做 disposed 保护, + 不把迟到结果写回已退出页面。 +3. `updatedAt` 为后端 `LocalDateTime` 字符串,解析失败只能显示占位, + 不能让整条记录解析失败。 +4. 下载契约里授权与任务状态均为字符串,客户端只原样展示/映射现有文案, + 不根据未知值猜测业务状态。 +5. 下载地址字段 `playUrl` 后端当前未填充,本批不做实际下载;不能据此宣称 + “可离线播放”。 +6. 本机可能缺少 Android SDK,`flutter build apk --debug` 若失败将保留原始输出, + 回执标记“未验证”,不静默跳过。 +7. 真机/模拟器可达性由用户验证;开发机 `curl` 不作为设备证据。 + +## 六、待确认(不阻塞本批) + +1. 发现页 `hot` / `new` 入口保留,但当前后端 `discoverFeed()` 只生成 + `category-{id}` 分区。若后续要展示热门/最新内容,需要后端在发现 feed 下发 + 对应分区;在此之前按契约显示空态。 +2. 收藏分页与观看记录卡片字段待契约负责人后续补足;本批按 INBOX 降级方案执行。 +3. 下载真实落盘、断点续传与离线播放需要存储、权限、加密播放方案,另立批次。 + +## 七、完成定义 + +- 四块均有“红 → 绿”测试证据。 +- `dart analyze lib test` 零告警,`flutter test` 全过。 +- `flutter build apk --debug` 通过,或保留缺 SDK 的原始失败输出并标记未验证。 +- 三语言 ARB 齐全且生成物已更新。 +- 独立记录、收藏、下载页和分类条均接契约接口;未登录入口可完成登录引导。 +- 不改 `main`、不改 specs、不改 FruitServer 后端、不做搜索。 +- `PROGRESS.md` 与 INBOX K2 回执包含验证步骤、预期结果、提交号与原始门禁输出。 diff --git a/docs/plans/2026-10-17-K3.md b/docs/plans/2026-10-17-K3.md new file mode 100644 index 0000000..d0ee5b1 --- /dev/null +++ b/docs/plans/2026-10-17-K3.md @@ -0,0 +1,92 @@ +# 2026-10-17 K3 客户端第三批:互动 + +## 一、目标与边界 + +本轮只完成 Flutter 客户端互动能力的真实接口接入: + +- K3-A 评论:匿名游标列表、顶层评论与一层回复、发布、敏感词三档、删除、三态与重试。 +- K3-B 点赞:剧集与评论点赞、乐观更新、失败回滚、未登录拦截、防连点。 +- K3-C 举报:举报评论与短剧、固定原因单选、可选描述、成功与失败提示。 +- K3-D 分享回归:沿用现有系统分享,核对标题、简介与 footer 的三语文案。 + +不做 K4、拉黑、举报用户、后台治理、图片/表情/@提及、无限嵌套、我的评论列表和并发压测。 +后端代码、后台 Web、specs 仓库均不改;接口只使用 `FruitServer/docs/api-phase1.md` §1.7。 + +## 一之一、实测基线 + +- ReelShort:`develop` / `feature/flutter-k3-interaction` 均为 `45f5f25`,功能分支从 K2 合并点派生。 +- FruitServer:`develop` 为 `a0156ee`,比 `origin/develop` 多 1 个仅包含 INBOX 的派单提交。 +- 本计划文件在开工前的 FruitServer 工作区中已存在且尚未提交,本轮仅增量补充,不覆盖既有内容。 + +## 二、契约与数据口径 + +| 能力 | 接口 | 客户端处理 | +| --- | --- | --- | +| 评论列表 | `GET /app/comments?dramaId=&episodeId=&rootId=&cursor=&size=` | 首屏 20 条,按 `nextCursor` 续拉,用 `hasMore` 控制加载更多 | +| 发布评论 | `POST /app/comments` | 顶层传 `dramaId/content`,回复额外传 `parentId` | +| 删除评论 | `DELETE /app/comments/{id}` | 仅 `mine=true` 可操作,二次确认,失败恢复列表 | +| 点赞切换 | `POST /app/likes/toggle` | `DRAMA` / `COMMENT`,以响应 `{liked,count}` 为最终态 | +| 举报 | `POST /app/reports` | `COMMENT` / `DRAMA`,`reasonCode` 必填 | +| 系统分享 | 无后端接口 | 保持 `PlatformShareService` | + +评论字段按 `CommentRespVO` 解析,ID 全部使用字符串。所有新增用户可见文案进入 +`lib/l10n/app_{en,zh,id}.arb` 并重新生成。 + +## 三、已确认的契约缺口与降级 + +1. `AppDramaDetailVO` / `AppDramaCardVO` 没有 `likeCount` / `liked`。 + 降级:不展示剧集点赞数字,只维护当前会话内的 liked 状态。 + 建议:后端详情与卡片补 `likeCount` / `liked`,需 specs 负责人变更契约。 +2. §1.7 没有定义 `reasonCode` 枚举,后端当前不校验。 + 降级:客户端固定 `SPAM / ABUSE / PORN / AD / OTHER` 五项,文案进三语 arb。 + 建议:后端补 `reasonCode` 枚举与后台工单映射。 +3. §1.7 没有拉黑接口。 + 降级:本批不做拉黑,不提供入口。 + +## 四、预计影响文件 + +FruitServer 文档: + +- `docs/plans/2026-10-17-K3.md` +- `docs/plans/PROGRESS.md` +- `docs/plans/INBOX.md` + +ReelShort 客户端: + +- `lib/core/data/remote/comment_api.dart` +- `lib/core/data/remote/interaction_api.dart` +- `lib/core/data/repositories/comment_repository.dart` +- `lib/core/data/repositories/interaction_repository.dart` +- `lib/core/data/services/comment_controller.dart` +- 互动相关模型、Provider 注入与页面/组件 +- `lib/pages/player/widgets/drama_comments_sheet.dart` +- `lib/pages/player/player_view.dart` +- 详情页举报入口及互动弹窗 +- `lib/l10n/app_{en,zh,id}.arb` 与生成文件 +- 对应 `test/**/*_test.dart` + +实际文件以现有 K1/K2 分层和注入方式为准,避免无关重构。 + +## 五、TDD 与执行顺序 + +1. 只读核对 `ReelShort` 与 `FruitServer` 的 `develop` 基线、工作区状态与提交。 +2. 从 `develop` 建 `feature/flutter-k3-interaction`。 +3. K3-A:先写评论分页、发布、敏感词、删除、失败回滚和未登录测试并确认红灯,再实现并转绿。 +4. K3-B:先写剧集/评论点赞成功、计数、乐观更新、回滚和未登录测试并确认红灯,再实现并转绿。 +5. K3-C:先写举报成功、重复举报 message 透传和未登录测试并确认红灯,再实现并转绿。 +6. K3-D:补充分享文案回归检查,确认三语资源完整。 +7. 每块执行定向测试和 `dart analyze lib test`。 +8. 全量执行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +9. 分块提交,`--no-ff` 合并回 `develop` 并 push;文档单独提交并 push。 +10. 在 `PROGRESS.md` 与 INBOX 写回执,列出用户真机验证步骤,并明确设备侧取证未由 Codex 执行。 + +## 六、风险与待确认 + +- 评论游标的实际响应字段以已实现后端 VO 为准;若与任务书 `nextCursor/hasMore` 不一致,先停止并在 INBOX 顶部登记待协助,不自行扩契约。 +- 剧集点赞没有初始查询接口,页面重建后会话内状态可能丢失;本批按降级方案不伪造初始值。 +- 匿名列表可见,但发布、点赞、举报必须登录;需确保 AuthGate 在发起业务请求前拦截。 +- 举报重复与举报自己依赖后端 message,客户端只原样展示,不自行判断。 +- 敏感词 `REVIEW` 的 pending 评论仅自己可见,客户端必须保留发布成功返回内容并显示“审核中”。 +- 现有互动控制器为本地演示数据,移除时不得破坏 K2 的分类、记录、收藏、下载和播放进度逻辑。 +- 全量构建可能受本机 Flutter/Android SDK 环境影响;若失败会记录原始输出,不以开发机接口请求代替设备证据。 +- 当前没有待确认的产品问题;若执行中出现契约与实现不一致,按任务书先停并登记。 diff --git a/docs/plans/2026-10-17-K5.md b/docs/plans/2026-10-17-K5.md new file mode 100644 index 0000000..fd2cfc9 --- /dev/null +++ b/docs/plans/2026-10-17-K5.md @@ -0,0 +1,82 @@ +# 2026-10-17 K5 客户端视觉:克莱因蓝主题 + 印尼市场适配 + +## 一、目标与边界 + +本轮只完成 Flutter 客户端的主题色、蓝色收敛、印尼语 locale 和回归测试: + +- K5-A:建立克莱因蓝 token 与浅色/深色显式 `ColorScheme`,主色统一 `#002FA7`。 +- K5-B:清理页面层的旧蓝字面量,统一使用 `AppColors` / `ColorScheme`;不改语义色。 +- K5-C:`LocaleController` 支持 `id`;未显式选择时跟随系统,非 `en/zh/id` 回退 `id`; + 两个现有语言入口增加 Bahasa Indonesia。 +- K5-D:补主题 token、对比度、硬编码扫描、locale 回退与语言切换测试, + 回归深色/浅色、三态和小屏布局。 + +不做 K4、支付、Google 登录、新接口、下载落盘、App 图标/启动图或信息架构调整; +不改后端、`ReelShortWeb`、specs。 + +## 二、用户裁定色值 + +| Token | 值 | 用途 | +| --- | --- | --- | +| `blue` | `#002FA7` | Klein Blue / IKB 主色 | +| `bluePressed` | `#00227A` | 按下/激活态与大面积蓝色背景 | +| `blueSoft` | `#5B7BE0` | 深色背景上的高亮/次级文字与图标 | +| `onPrimary` | `#FFFFFF` | 主色上的文字/图标 | +| `ink` | `#0B0C10` | 保持现值 | +| `bar` | `#101114` | 保持现值 | +| `card` | `#17191F` | 保持现值 | +| `muted` | `#B7BDC9` | 保持现值 | + +对比度口径:正文 ≥ 4.5:1,大字/图标 ≥ 3:1;错误红、成功绿、点赞红等语义色不改。 + +## 三、预计影响文件 + +- `lib/core/theme/app_colors.dart` +- `lib/app.dart` +- `lib/core/config/locale_controller.dart` +- `lib/pages/mine/mine_logic.dart` +- `lib/pages/home/widgets/home_quick_drawer.dart` +- `lib/pages/home/home_view.dart` +- `lib/pages/discover/discover_view.dart` +- `lib/pages/drama_detail/drama_detail_view.dart` +- `lib/pages/player/player_view.dart` +- `lib/pages/player/player_logic.dart` +- `lib/pages/player/widgets/seek_progress_bar.dart` +- `lib/pages/player/widgets/comment_tile.dart` +- `lib/pages/player/widgets/drama_comments_sheet.dart` +- `lib/pages/mine/mine_view.dart` +- `lib/pages/watch_records/watch_records_page.dart` +- `lib/pages/downloads/downloads_page.dart` +- `lib/pages/auth/auth_view.dart` +- `lib/core/widgets/auth_gate.dart` +- `lib/l10n/app_{en,zh,id}.arb` 与生成文件 +- `test/theme/*`、`test/config/*`、相关页面 widget 测试 + +实际改动以静态扫描结果为准;不改业务逻辑和页面布局。 + +## 四、TDD 顺序 + +1. 写主题 token/对比度、硬编码蓝色扫描、locale `id` 持久化与未知语言回退测试,确认红灯。 +2. K5-A:新增色值 token 和浅色/深色 `ColorScheme`,补主题测试转绿。 +3. K5-B:按扫描结果替换页面蓝色字面量,补语言入口和 widget 回归。 +4. K5-C:实现 `id` locale、系统语言跟随、未知系统语言回退 `id`,两个入口增加 + Bahasa Indonesia。 +5. K5-D:全量扫描、深色/浅色与小屏 widget 回归。 +6. 门禁:`dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +7. `--no-ff` 合并回 `develop` 并 push;FruitServer 仅提交 `docs/plans`。 + +## 五、风险与处理 + +- 克莱因蓝在浅色背景上作为文字可能对比不足:正文继续使用现有着色, + 主色主要用于按钮/图标/选中态;主色背景统一配 `onPrimary`。 +- `blueSoft` 仅用于深色背景高亮,避免在浅色背景当正文色造成低对比度。 +- 现有 `_orange` 等历史命名可能实际承载蓝色,替换时按语义改名,避免误导。 +- 直接替换主题色可能影响 FilledButton、Slider、Switch 等 Material 控件: + 通过显式 `ColorScheme` 和 widget 测试统一验证。 +- 语言入口两处需同步,避免“我的”和快捷抽屉行为不一致。 +- 真机、设备截图与系统语言切换证据由用户执行,Codex 不跑真机。 + +## 六、待确认 + +- 当前无产品歧义;按用户裁定的方案 A 色值执行。 +- specs 无成文色板,若后续提供 Stitch 视觉稿,再按新稿更新 token 与页面映射。 diff --git a/docs/plans/2026-10-17-K6.md b/docs/plans/2026-10-17-K6.md new file mode 100644 index 0000000..834490d --- /dev/null +++ b/docs/plans/2026-10-17-K6.md @@ -0,0 +1,102 @@ +# 2026-10-17 K6 客户端 UI:登录页 + 底部导航优化 + +## 一、目标与边界 + +本批只优化 Flutter 登录页和首页底部导航,基于 K5 克莱因蓝主题: + +- K6-A:登录页品牌区、autofill、密码显隐、键盘 next/done 提交、 + 提交中禁用、主题语义错误色、小屏键盘可用性。 +- K6-B:底部导航背景/边框/选中态统一使用 K5 token,安全区适配, + 点击区域 ≥48pt,切换动效 ≤200ms。 +- K6-C:新增可注入 `Haptics` 封装,只对关键操作提供 selection/light/medium 反馈。 +- K6-D:新增 widget 测试并保持既有 Key、导航逻辑与沉浸隐藏行为。 + +不做忘记密码、Google/Apple 登录、隐私勾选、新 tab、页面信息架构调整; +不改接口、业务校验、注册/登录流程、后端、Web 或 specs。 + +## 二、设计口径 + +- 主色:`AppColors.blue = #002FA7`。 +- 按下态/大面积激活背景:`AppColors.bluePressed = #00227A`。 +- 深色底高亮:`AppColors.blueSoft = #5B7BE0`。 +- 主色上的文字/图标:`AppColors.onPrimary = #FFFFFF`。 +- 错误提示使用 `ColorScheme.error/onError` 对应语义色。 +- 底栏背景使用现有 `AppColors.bar`,顶部边框使用主题 outline 或现有暗色 token; + 页面不新增色值字面量。 + +## 三、预计影响文件 + +- `lib/pages/auth/auth_view.dart` +- `lib/pages/home/home_view.dart` +- `lib/core/platform/haptics.dart` +- `lib/app.dart`(Haptics Provider) +- 收藏、点赞、评论、举报、下载、记录相关页面回调 +- `lib/l10n/app_{en,zh,id}.arb` 与生成文件 +- `test/auth/auth_page_test.dart` +- `test/widget_test.dart` +- 必要时补首页导航专用测试文件 + +不修改 `HomeLogic` 的 tab 顺序、选中规则和 Key;不动播放页沉浸状态来源。 + +## 四、交互与可用性 + +### 登录页 + +- 品牌区显示 App 名和印尼市场副标题,全部走 l10n 三语。 +- 邮箱 `TextInputAction.next`,提交后焦点进入密码框。 +- 密码 `TextInputAction.done`,提交后直接执行现有 `_submit()`。 +- 邮箱 `AutofillHints.email`,密码 `AutofillHints.password`。 +- 密码显隐按钮语义标签走 l10n,所有点击区域 ≥44pt。 +- `busy` 时禁用两个输入框、显隐按钮、提交和模式切换。 +- 使用 `resizeToAvoidBottomInset` + ListView/SafeArea,保证小屏键盘下 + submit 可滚动到可见区域。 + +### 底部导航 + +- 保持 4 个 tab 的数量、顺序、Key 和回调不变。 +- 选中态采用克制的背景胶囊(现有方案收敛)或短指示条,二选一即可; + 使用 `bluePressed` 胶囊 + `blueSoft` 高亮文字/图标。 +- 底栏高度 + SafeArea bottom 适配 home indicator。 +- 每个 tab 的可点击区域至少 48pt;动效控制在 200ms 内。 +- `logic.videoImmersive == true` 时保持 `bottomNavigationBar: null`。 + +### 点击震动反馈 + +- 使用 Flutter 内置 `HapticFeedback`,统一封装为 `Haptics` 抽象 + + `FlutterHaptics` 默认实现,通过 Provider 注入,测试使用 fake。 +- tab 真正切换:`selectionClick()`。 +- 登录/注册提交、收藏/取消、点赞/取消、评论发送、举报提交、下载授权申请: + `lightImpact()`。 +- 清空记录、清空下载、删除记录/授权:`mediumImpact()`。 +- 全部 `unawaited`;平台不可用时默认实现静默忽略;普通列表/滚动/页面切换不触发。 + +## 五、TDD 顺序 + +1. 新增登录页密码显隐、autofill、next/done 焦点、busy 禁用、错误语义色测试。 +2. 新增底栏 4 Key、tab 切换选中态、沉浸隐藏、最小点击区域和硬编码色扫描测试。 +3. 新增 fake Haptics 测试,覆盖 tab selection、主操作 light、危险操作 medium、 + 普通列表不触发与默认实现异常静默。 +4. 先运行定向测试确认红灯,再实现登录页、底栏与 Haptics 转绿。 +5. 补 320×568 小屏与键盘弹出回归。 +6. 全量门禁:`dart analyze lib test`、`flutter test`、 + `flutter build apk --debug`。 + +## 六、风险与处理 + +- 登录页已有业务逻辑较长:仅提取品牌区和表单展示组件,不改 + `AuthLogic`、接口调用与校验规则。 +- 键盘焦点流转依赖 `FocusNode`:显式创建并释放,测试使用 + `tester.testTextInput.receiveAction(TextInputAction.next/done)`。 +- busy 状态下测试请求不能立即返回:使用可控 Fake Auth 或延迟 Future, + 验证输入和按钮禁用,再释放请求避免测试悬挂。 +- 底栏新增动画可能影响既有 `pumpAndSettle`:统一使用 ≤200ms, + 不引入第三方动画库。 +- Haptics 注入遗漏会让页面测试缺少 Provider:所有直接 pump 页面的测试 + 统一注入 fake;生产默认实现吞掉平台不可用异常,不影响业务。 +- 安全区/小屏可能压缩按钮高度:使用最小点击区约束与 + `SafeArea(minimum: ...)`,保持现有 Key 可定位。 + +## 七、待确认 + +- specs 无 UI 设计稿,按“视觉统一 + 可用性”执行;不引入图片资源。 +- 当前无产品歧义;若真机出现键盘安全区差异,由用户提供设备侧截图后再做最小修正。 diff --git a/docs/plans/2026-10-17-K7.md b/docs/plans/2026-10-17-K7.md new file mode 100644 index 0000000..7a23bc7 --- /dev/null +++ b/docs/plans/2026-10-17-K7.md @@ -0,0 +1,91 @@ +# 2026-10-17 K7 首页轮播图 + 站内信通知 + +## 一、目标与边界 + +本批只完成 Flutter 客户端两项能力: + +- K7-A:增强现有 `CoverCarousel`,继续使用 `feed.banners` 和 3D 翻转风格, + 增加自动轮播、交互暂停、指示器、失败占位和语义。 +- K7-B:新增匿名站内信分页页,接入 `GET /app/notices`,在我的页增加通知入口。 +- K7-C:补轮播/通知/响应式与 locale 时间测试。 + +不做已读未读、红点、FCM、通知设置、`/app/config noticeEnabled`、首页铃铛、 +新 banner 接口、后端、Web 或 specs 修改。 + +## 二、契约口径 + +### Banner + +- 数据源:`GET /app/home/feed` / `GET /app/discover/feed` 的 `banners[]`。 +- 无独立接口;空列表继续 `SizedBox.shrink()`。 +- 点击继续沿用 `linkType=DRAMA` + `linkValue` 的现有 `onOpen`。 + +### Notices + +- `GET /app/notices?pageIndex=1&pageSize=20`,匿名。 +- 响应:`{total, list:[{id,title,content,type,publishAt,expireAt}]}`。 +- ID 全部按字符串;`total` 是数字。 +- `list.length` 用于判断当前页是否有数据;是否加载更多按 + “已加载数量 < total”判断。 +- `type` 原样展示,不建立客户端枚举映射。 + +## 三、预计影响文件 + +轮播: + +- `lib/pages/discover/widgets/cover_carousel.dart` +- `lib/pages/discover/discover_view.dart` +- `lib/core/widgets/remote_image.dart`(可选失败图标参数) +- `test/discover/cover_carousel_test.dart` + +通知: + +- `lib/core/data/models/notice.dart` +- `lib/core/data/remote/notice_api.dart` +- `lib/core/data/repositories/notice_repository.dart` +- `lib/core/data/services/notices_controller.dart` +- `lib/pages/notices/notices_page.dart` +- `lib/pages/mine/mine_view.dart` +- `lib/app.dart`(NoticeRepository Provider) +- `test/notice/**` +- `lib/l10n/app_{en,zh,id}.arb` 与生成文件 + +## 四、实现口径 + +### 轮播 + +- `Timer.periodic` 4.5s;单张不创建定时器;`dispose` 取消。 +- 使用 `TickerMode.of(context)`/`Visibility` 语义监听页面可见性; + 不可见时取消,恢复时重新启动。 +- pointer down 暂停,up/cancel 恢复;拖拽时沿用现有 3D 翻转。 +- 指示器采用底部圆点,使用 `AppColors.onPrimary` + `muted` 低透明度。 +- 语义标签进入三语 arb。 +- 图片失败使用现有 `RemoteImage` 并显示图标占位。 + +### 通知 + +- `NoticesController` 管理首屏、分页、加载更多、刷新、错误。 +- `NoticesPage` 深色页,保留 K5/K6 主题与 Haptics 不触发滑动振动。 +- 列表项标题/类型/时间/两行摘要;点击打开详情 Dialog。 +- 详情 `content` 用可滚动 `Text` 保留 `\n`,长内容不溢出。 +- `publishAt` / `expireAt` 使用现有 `formatLocalizedDateTime`。 +- 我的页 `_menuCard` 增加通知入口,匿名可进入。 + +## 五、TDD 顺序 + +1. 轮播:自动切页、单张无 Timer、指示器、触摸暂停、点击回调、失败占位。 +2. 通知 API/model:分页字段、字符串 ID、时间解析。 +3. Controller:首屏、追加、加载更多条件、空/失败/刷新。 +4. NoticesPage:三态、列表、分页、详情、长内容、小屏。 +5. 我的页入口与三语文案。 +6. 门禁:`dart analyze lib test`、`flutter test`、 + `flutter build apk --debug`。 + +## 六、风险与待确认 + +- 自动轮播 Timer 与 3D AnimationController 并存,需避免 dispose 后 setState。 +- 页面用 `IndexedStack` 保持状态,需通过 `TickerMode` 判断真实可见性。 +- `total` 可能在分页期间变化,以本次响应 total 与已加载数量比较。 +- 详情长内容必须可滚动,避免小屏 overflow。 +- `noticeEnabled` 属 `/app/config`,本批不接;通知入口暂时常驻,已登记后续。 +- 无独立 banner 接口和已读接口,不自行扩展契约。 diff --git a/docs/plans/2026-10-17-K8.md b/docs/plans/2026-10-17-K8.md new file mode 100644 index 0000000..6de6ca2 --- /dev/null +++ b/docs/plans/2026-10-17-K8.md @@ -0,0 +1,63 @@ +# 2026-10-17 K8 登录页视觉重设计 + +## 一、目标与边界 + +将现有登录页从表单堆叠改为深色、克莱因蓝品牌的单页视觉,保留 K6 已完成的 +AuthLogic、校验、接口、登录/注册流程、autofill、键盘流转、busy 状态和 Haptics。 + +- 客户端分支:`feature/flutter-k8-auth-visual`,基线 `develop 6cc5bfc`。 +- 文档分支:`docs/k8-auth-visual`,FruitServer 只改 `docs/plans/`。 +- 不改后端、Web、specs、接口契约、其他页面或业务逻辑。 +- 不新增图片、字体、第三方依赖;品牌图形使用 Material Icons。 +- 不跑真机,设备侧视觉、键盘与小屏验收由用户执行。 + +## 二、视觉与交互实现 + +1. 页面背景使用 `AppColors.ink`,顶部叠加克莱因蓝渐变光晕,颜色透明度不超过 + 0.18,并忽略指针事件。 +2. 品牌区使用 48px 克莱因蓝圆角方块、白色剧集图标、App 名和现有 + `authBrandTagline`。 +3. 表单放入 20px 圆角、1px `outlineVariant` 边框、20px 内边距的 + `AppColors.card` 卡片。 +4. 输入框使用 52px 高、14px 圆角、`AppColors.bar` 填充,带邮箱/锁前缀图标; + 聚焦时显示 1.5px 主色边框并高亮图标;密码显隐按钮保留。 +5. 主按钮全宽 52px、26px 圆角、`AppColors.blue` 背景、白色 16/w700 文字, + pressed 态使用 `bluePressed`,loading 为 18px 白色环,busy 时输入、切换与 + 提交均禁用。 +6. 登录/注册改为双段 pill,实际可见标签为 `signIn` / `signUp`;点击调用现有 + `toggleMode()`。保留既有 `auth-switch-mode` 兼容入口,避免破坏既有测试。 +7. 错误提示使用红色 10% 透明背景、12px 圆角、图标和主题 error 色文字的胶囊。 +8. 首帧使用 220ms 的 `TweenAnimationBuilder` 完成淡入和上滑。 +9. 保留 `auth-scroll`、`auth-email`、`auth-password`;新增 + `auth-brand`、`auth-card`、`auth-mode-sign-in`、`auth-mode-sign-up`、 + `auth-submit`、`auth-error-capsule` 等稳定 Key。 + +## 三、影响文件 + +- `ReelShort/lib/pages/auth/auth_view.dart` +- `ReelShort/test/auth/auth_visual_test.dart`(新增) +- `ReelShort/test/auth/auth_page_test.dart`(既有用例保持不变) +- `FruitServer/docs/plans/2026-10-17-K8.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md`(完成后标记 K8 已处理) + +## 四、TDD 与门禁 + +1. 先新增视觉测试并运行,确认分段控件、品牌、错误胶囊和尺寸断言红灯。 +2. 实现登录页视觉后,定向 auth 测试绿灯。 +3. 运行 `dart analyze lib test` 零告警。 +4. 运行 `flutter test` 全过。 +5. 构建前确认没有并发 `flutter run/build`;运行 + `flutter build apk --debug`。 +6. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。提交作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 五、风险与待确认 + +- 旧测试按文字链接和 `auth-switch-mode` TextButton 检查交互;本次通过保留 + 兼容入口满足旧用例,同时可见交互必须由新 pill 分段控件完成。 +- 320px 小屏和键盘弹出时,卡片、错误胶囊及按钮需保持可滚动且不溢出。 +- 当前检测到 Android `flutter run` 会话;构建门禁前必须复查,若仍占用 + `build/` 则等待其结束,不能并发构建。 +- specs 无登录页设计稿,颜色与尺寸以本 INBOX 条目为准,无需新增契约。 diff --git a/docs/plans/2026-10-17-K9.md b/docs/plans/2026-10-17-K9.md new file mode 100644 index 0000000..3f303b1 --- /dev/null +++ b/docs/plans/2026-10-17-K9.md @@ -0,0 +1,70 @@ +# 2026-10-17 K9 首页剧目列表改造(2 列全部剧目) + +## 一、目标与边界 + +首页(发现 tab)移除「继续观看」区块,在分类 chips 下方以懒构建的 2 列网格展示 +当前分类的全部剧目;保留 header、轮播、分类、三态、空态和底部导航。 + +- 客户端分支:`feature/flutter-k9-home-grid`,基线 `develop 99f64ab`。 +- 文档分支:`docs/k9-home-grid`,FruitServer 基线 K9 任务书 `b434f65`。 +- 只改首页(发现页)、三语 l10n、对应 widget 测试与 `docs/plans/`。 +- 不改接口、数据模型、CategoryController 过滤逻辑、轮播、通知、观看记录、 + 收藏、下载或底部导航。 +- 不新增图片、依赖或分页;继续使用 `RemoteImage` 懒加载。 +- 不跑真机,设备侧滚动、布局、分类切换和详情跳转由用户验收。 + +## 二、实现方案 + +1. 删除 `_continueHeader`、`_dramaContent`、`_continueTile`、`_ShowcaseCard` + 与 `_cardsFor`,首页不再构建或保留「继续观看」文案和代码。 +2. 将页面滚动容器改为 `CustomScrollView`,依次放置: + carousel `SliverToBoxAdapter`、分类与标题 `SliverToBoxAdapter`、状态 + `SliverToBoxAdapter` 或剧目 `SliverGrid`、底部留白 `SliverToBoxAdapter`。 +3. 剧目网格使用 `SliverGridDelegateWithFixedCrossAxisCount`: + `crossAxisCount: 2`、`crossAxisSpacing: 12`、`mainAxisSpacing: 16`、 + `childAspectRatio: 2 / 3`;左右由 `SliverPadding` 保持 16。 +4. 卡片封面使用 2:3 比例、14px 圆角、`RemoteImage` 和现有底部深色渐变; + 标题最多 2 行,类型最多 1 行。点击调用现有 + `logic.openCard(..., embedded, onDramaSelected, onAction)`。 +5. 数据直接使用 `CategoryController.visibleDramas`;`For You` 使用其全量 + 去重结果。分类切换后由 ChangeNotifier 驱动 sliver 内容更新。 +6. 保留 `categories.loading` 首载进度、`categories.error` 重试、 + `noCategoryDramas` / `noSearchResults` 空态文案。 +7. l10n 新增 `allDramas`: + `全部剧目` / `All Dramas` / `Semua Drama`,并重新生成 l10n。 + +## 三、影响文件 + +- `ReelShort/lib/pages/discover/discover_view.dart` +- `ReelShort/lib/l10n/app_en.arb` +- `ReelShort/lib/l10n/app_zh.arb` +- `ReelShort/lib/l10n/app_id.arb` +- `ReelShort/lib/l10n/app_localizations*.dart`(生成物) +- `ReelShort/test/discover/home_all_dramas_grid_test.dart`(新增) +- `ReelShort/test/widget_test.dart`(仅更新作废的「继续观看」断言) +- `FruitServer/docs/plans/2026-10-17-K9.md` +- `FruitServer/docs/plans/PROGRESS.md` +- `FruitServer/docs/plans/INBOX.md` + +## 四、测试与门禁 + +1. 先新增 K9 首页 widget 测试并取得红灯。 +2. 实现后验证:无「继续观看」、有「全部剧目」、2 列 sliver、左右/行列间距、 + 2:3 封面、分类切换更新、空态与失败态。 +3. 运行 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`; + 构建前检查无并发 `flutter run/build`。 +4. 完成后 `merge --no-ff` 回客户端 `develop` 并 push;FruitServer 文档同样 + 合并推送。作者与提交者统一为 + `Zhou Weidong <502333560@qq.com>`。 + +## 五、风险与待确认 + +- 既有 `test/widget_test.dart` 有两个壳层用例断言首页出现 + `Continue Watching`。K9 明确要求该文案彻底消失,因此原断言与 + `find.text('Continue Watching'), findsNothing` 不可能同时成立。 + 处理方式:保留两个既有用例及完整导航流程,只把已作废的文案断言更新为 + `All Dramas`,不删除任何测试用例。 +- 首页 feed 可能一次返回较多剧目;必须使用 `SliverGrid` 懒构建,不能嵌套 + `GridView(shrinkWrap: true)`。 +- 2:3 卡片在小屏需要保持标题 2 行和类型 1 行不溢出;测试覆盖 320px 宽度。 +- 不改 `CategoryController` 的数据语义;筛选仍由现有分区 code 决定。 diff --git a/docs/plans/INBOX.md b/docs/plans/INBOX.md index 3d20d07..1e42f43 100644 --- a/docs/plans/INBOX.md +++ b/docs/plans/INBOX.md @@ -15,11 +15,1933 @@ > 6. **禅道同步**:任务「开始/完成」时同步更新禅道任务状态。 > 7. **验收归用户**:每批做完**停在干净交接点**,由用户亲自验证;回执必须含「用户可照做的验证步骤 + 每步预期结果」。 > 8. **门禁(不达标不提交)**:后端 = `mvn test` 全过(核心接口须有契约测试);Web = `pnpm build:pro` 通过 + eslint 零错误;Flutter = `dart analyze lib test` 零告警 + `flutter test` 全过。 -> 9. **分支**:在 `develop` 提交并 `git push origin develop`;**绝不动 `main`**(dev→main 走 PR,由用户操作)。 +> 9. **分支(按 `GIT_WORKFLOW.md`)**:从 `develop` 拉 `feature/<模块>-<简述>`,小步提交;完成后 `git checkout develop && git merge --no-ff feature/...` 再 `git push origin develop`;**绝不动 `main`**(dev→main 走 PR,由用户操作)。 +> 10. **设备验证必须「设备侧取证」(2026-10-16 新增,血的教训)**:真机 / 模拟器 / 小程序等目标设备的验证,**证据必须产自设备侧**(设备侧集成测试、设备日志、设备截图、设备侧抓到的连接)。**禁止**用"在开发机上 `curl` 某地址返回 200"证明设备可达 —— 本机 curl 本机网卡地址必然 200,这是**假阳性**(已实际误导过一次:`10.157.242.1` 是开发机 VPN 网卡地址,手机根本不可达,导致真机「剧目列表加载失败」)。**拿不到设备侧证据的,一律标「未验证」**,不得写成"已验证"。 --- -## [已处理·待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一 +## [已处理·待用户验收] 2026-10-17 | K16 注册限流改为可配置(支持不限制) + +**用户指令(2026-10-17)**:「注册 429 限流做成可配置的,不限制或限制数量」。 + +> 只改后端注册风控配置化,不改注册业务逻辑与接口形态。 + +### 一、现状(实测) + +- `fruitshorts-module-user/.../AppAuthService.java:305-307`:常量硬编码 `MAX_REGISTER_PER_IP_PER_DAY = 5`、`MAX_REGISTER_PER_DEVICE_PER_DAY = 3`。 +- `guardRegisterRisk(...)` 在数值达到阈值时抛 429;无注解注入,无法配置。 +- `fruitshorts-app-api/src/main/resources/application.yml` 已有 `fruitshorts.interaction.*` 等配置段。 + +### 二、本批范围(后台可配置,走 `sys_config`) + +1. **读取方式**:`AppAuthService` 注入 `ConfigService`,读取: + - `app.register.max-per-ip-per-day` + - `app.register.max-per-device-per-day` + - 值 **≤0 = 不限制**(跳过对应 guard);键缺失或非数字时回退默认 **5 / 3**;错误码与文案不变。 +2. **种子配置**:`sql/data-phase1.sql` 增加两键的幂等种子(`INSERT ... ON CONFLICT (config_key) DO NOTHING`): + - 开发默认值 0(不限制),remark 写明「0=不限制;生产建议 IP 5 / 设备 3」。 +3. **后台配置**:无需新建页面——已有 `ReelShortWeb/src/views/systemManagement/SettingsView.vue` 使用 `/admin/configs`;只需确认菜单可进入「系统配置」并能看到这两个键。 +4. **契约说明**:`docs/api-phase1.md` §1.1 注册风控段补充:两键可在后台「系统配置」修改、0=不限制、缺省回退 5/3。 +5. **测试**:`ConfigService` 注入 mock 覆盖 0 / 缺省 / 自定义三种;`mvn test` 全绿。 +6. **打包并重启 8081**: + - `mvn -pl fruitshorts-app-api -am package -DskipTests`; + - 停旧 8081 进程并使用新 jar 启动,`/healthz` 通过; + - 验证:确认库中两键为 0 后,用新设备号连续注册 ≥4 次不再 429(接口自检,不作为设备侧证据)。 +7. **不使用 application.yml 固化限流值**(已下发口径更正)。 + +### 三、约束 + +- 不改客户端;不改注册接口路径与返回结构;不动 main。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`;从干净 develop 拉 `feature/backend-register-rate-config`,完成后 `git merge --no-ff` 回 develop 并 push。 +- 留痕写入 `FruitServer/docs/plans/PROGRESS.md`。 + +### 四、K16 首版回执(历史,已被返工回执替代) + +**结论**:注册 IP/设备限流已改为读取后台 `sys_config`,开发两键为 0/0, +支持 0=不限制;后台系统设置页可修改。注册接口与返回结构未变。 + +- 最终后端实现 `f1aaaee`,缺省回退测试 `f4003ed`,最终合并 + `5b7be2e`,已推送 `78fb995..5b7be2e develop`。 +- `AppAuthService` 注入 `ConfigService`,读取 + `app.register.max-per-ip-per-day` 和 + `app.register.max-per-device-per-day`;非法/缺失回退 5/3。 +- `sql/data-phase1.sql` 已增加幂等种子(0/0);当前开发库已写入并确认 ID 3/4。 +- 新增 Web 提交 `54db082`,合并 `5c087f4`;系统设置页可编辑两项, + `pnpm build:pro` 通过。 +- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。 +- 新 jar 已启动,`/healthz` 返回 200;当前 8081 PID `80642`。 +- 同一新设备号连续注册 4 次均 HTTP 200、`code=0`,不再出现 429。 + +命令、日志和服务自检原始输出见 +`docs/plans/PROGRESS.md`「32. K16 注册限流后台配置化」。 + +### 五、K16 返工回执(2026-10-17) + +**结论**:已按最新口径改为“后台 `sys_config` 优先,`@Value` fallback”, +后台「系统配置」可按 IP/设备修改;开发库两键均为 0。 + +- 返工提交 `35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底`。 +- `AppAuthService` 读取 `app.register.max-per-ip-per-day` / + `app.register.max-per-device-per-day`;缺失或非数字时回退 + `fruitshorts.auth.register-max-*` 的 `@Value` 注入值。 +- `sql/data-phase1.sql` 幂等种子已执行;查询结果为 IP 0、设备 0。 +- `/admin/configs` 实测返回两键,现有系统设置页可修改。 +- 临时把 IP 上限改为 2:前两次注册成功,第三次返回 `code=429`; + 恢复 0 后再次注册成功。 +- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。 +- 新 jar 已重启,8081 PID `94657`,`/healthz` 返回 HTTP 200。 + +原始输出与解读见 `docs/plans/PROGRESS.md`「33. K16 返工」。 + +--- + +## [待处理] 2026-10-17 | K14 客户端文档:README + 任务清单 + 文件说明 + +**用户指令(2026-10-17)**:「现在更新客户端 readme 任务清单 及文件说明」。 + +> 只改 `ReelShort` 仓库文档,不改代码、不改 specs、不改后端/Web。 +> 内容必须基于当前实际代码与已交付事实,**禁止编造**;不写任何密钥/账号。 + +### 一、现状(实测) + +- `ReelShort/README.md` 仅 50 行,是原型阶段说明,已明显过时: + - 仍写“本地演示评论”“互动数据保存在本机”,实际 K3 已接后端评论/点赞/举报; + - 未提分类、通知、下载授权、克莱因蓝主题、印尼语默认、运行时服务器地址设置; + - 未写门禁命令、iOS SPM 兼容注意事项、USB 调试方式。 +- `ReelShort` 无 `docs/` 目录,没有任务清单与文件说明。 +- `lib` 现有 105 个 Dart 文件,目录:`core/{config,data/{models,remote,repositories,services},formatting,platform,theme,widgets}`、`l10n`、`pages/{auth,discover,downloads,drama_detail,favorites,home,interaction,mine,notices,player,watch_records}`。 + +### 二、本批范围(3 份文档) + +#### K14-A 重写 `ReelShort/README.md` + +至少包含: + +1. 项目定位:面向印尼市场的短剧 App(Flutter)。 +2. 技术栈:Flutter 3.47 / Dart 3.13、Provider、dio、video_player、shared_preferences、flutter_localizations(en/zh/id)。 +3. 目录结构概览(core 三层 + pages + l10n + test)。 +4. 已接后端接口清单(按契约分组,**以代码实际调用为准**):账号、首页/发现/分类/详情、播放、观看记录/收藏、下载授权 8 接口、互动(评论/点赞/举报)、公告。 +5. 运行方式: + - Android USB:`adb reverse tcp:8081 tcp:8081` + `flutter run -d --dart-define=API_BASE_URL=http://127.0.0.1:8081`; + - iOS:`flutter run -d --dart-define=API_BASE_URL=http://:8081`; + - **debug 下可在「我的 → 设置 → 服务器地址」运行时修改**(K13),换网络不必重编。 +6. 环境注意: + - Xcode 16.2 / Swift 6.0.3 与 `shared_preferences_foundation 2.5.6` 的固定原因(2.6.0 需 Swift 6.1); + - iOS SPM 报 `Missing package product` 时的恢复步骤(清 DerivedData、`flutter pub get`、`xcodebuild -resolvePackageDependencies`); + - Android `libdartjni.so` 加载失败时卸载重装。 +7. 门禁命令:`dart analyze lib test`、`flutter test`、`flutter build apk --debug`;测试命名约定。 +8. 当前边界:下载不落盘/不续传/不离线播放;无搜索接口;通知无已读态。 + +#### K14-B 新增 `ReelShort/docs/TASKS.md` + +表格形式,列:批次 / 范围 / 状态 / 关键提交或契约章节 / 备注。至少覆盖: + +- K1 详情+播放+进度+收藏;K2 分类/记录/收藏页/下载授权;K3 评论/点赞/举报/分享;K5 克莱因蓝主题+印尼语;K6 登录页+底部导航+震动;K7 轮播+站内通知;K8 登录页视觉;K9 首页网格;K10 空态;K11 3 列+全部首位;K12 去标题+空态重做;K13 运行时服务器地址。 +- 待办:K4 账号剩余(Google 登录需凭据)、B1 支付/金币、搜索(契约缺失)、下载落盘(另立批次)。 +- 状态以 `develop` 实际合并情况为准,不确定的标注「需核对」,不要编造提交号。 + +#### K14-C 新增 `ReelShort/docs/FILES.md` + +- 目录树(到两级),每目录一句话职责。 +- 关键文件表:`lib/core/config/app_config.dart`、`server_endpoint`(如已实现)、`core/data/remote/*`、`repositories/*`、`services/*`、`core/theme/*`、`core/widgets/empty_state.dart`、`pages/*/`、`l10n/*.arb`。 +- `test/` 结构说明(按模块目录)。 +- 只写实际存在的文件;生成物(`*.g.dart`、`app_localizations*.dart`)标注为生成、勿手改。 + +### 三、约束 + +- 中文书写,简洁准确;不使用 AI 腔(“赋能/一站式/旨在”等)。 +- 不泄露任何密钥、账号、服务器凭据。 +- 只新增/修改 `README.md`、`docs/TASKS.md`、`docs/FILES.md`;不删既有内容以外的文件。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 四、门禁与 Git + +- 文档改动仍需 `dart analyze lib test` 与 `flutter test` 通过(确认未误改代码)。 +- 从干净 `develop` 拉 `docs/k14-client-docs`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 在 `FruitServer/docs/plans/PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」,完成后停住写回执。 + +### 五、K14 完成回执(2026-10-17) + +**结论**:客户端 README 已重写,并新增任务清单与文件说明。只改 ReelShort 的 +三份文档,未改代码、specs、后端或 Web。 + +- 文档分支 `docs/k14-client-docs` 从 `f95325d` 完成并 `--no-ff` 合并; + 主提交 `5a28845`,接口统计修正 `213c86e`,最终合并 `9fe62d1`,已推送 + `f95325d..9fe62d1 develop`。 +- `README.md` 已覆盖印尼市场定位、Flutter 3.47.7/Dart 3.13.5 技术栈、 + 实际调用的 26 个 `/app/**` 路径(31 个方法级调用)、Android USB/iOS/运行时地址、iOS SPM、 + shared_preferences 固定、Android libdartjni、门禁和当前边界。 +- 新增 `docs/TASKS.md`:K1-K13 状态表、K4/支付/搜索/下载落盘待办; + 未写不确定提交号,未确认项标「需核对」。 +- 新增 `docs/FILES.md`:目录树、关键文件职责、test 结构和生成物说明。 +- 仓库 `.gitignore` 排除了 `/docs/`,本次仅对 TASKS/FILES 使用 + `git add -f`,没有修改 `.gitignore`。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:20 +309 ~1: All tests passed!`;本轮仅文档,不需要 APK 构建。 + +原始命令、输出与内容核对见 `docs/plans/PROGRESS.md` +「30. K14 客户端文档」。 + +--- + +## [待处理] 2026-10-17 | K13 运行时服务器地址设置(仅 debug) + +**用户反馈(2026-10-17,已重复 3 次)**:每次 Codex 门禁构建出的 APK 不带 `--dart-define`,App 就回退到 `http://jiangdeMacBook-Air.local:8081`,真机不可用。 + +> 这是开发效率问题,不是业务功能。只做 debug 工具,不改 release 行为、不改接口。 + +### 一、现状(实测) + +- `lib/core/config/app_config.dart`:`apiBaseUrl = dart-define(API_BASE_URL) 非空 ? 它 : 默认值`;默认值在 iOS/Android 真机上是 `http://jiangdeMacBook-Air.local:8081`(常量)。 +- 构建参数由 `--dart-define` 决定,编译期固化;Xcode/门禁构建不带参数时必然回退。 +- 客户端已有 `SharedPreferencesAsync` 使用先例(`LocaleController`)。 + +### 二、本批范围 + +1. **新增运行时配置** `lib/core/config/server_endpoint.dart`(命名可调整): + - 读取 `SharedPreferencesAsync` 的 `dev.api_base_url`; + - 优先级:**运行时设置 > `--dart-define(API_BASE_URL)` > 现有默认值**; + - 启动时在 `main()` 里 `await` 加载完成后再创建 `ApiClient`(若现有结构不允许,可在 `AppConfig` 初始化时同步设置); + - URL 规范化:去首尾空格、去末尾 `/`;仅接受 `http://` 或 `https://` 开头,非法值忽略并保留原值。 +2. **设置入口(仅 `kDebugMode`)**: + - 位置:我的页 → 设置(现有 `showLanguage` 弹窗内增加「服务器地址」一行),或登录页底部一行小字入口;二选一,保持克制; + - 交互:弹窗显示当前实际 baseUrl,可编辑并保存;保存后提示「重启 App 生效」(文案走 l10n,三语); + - 提供「恢复默认」操作,清除 `dev.api_base_url`。 +3. **release 行为不变**:`kDebugMode == false` 时完全不显示入口,也不读取本地覆盖(仍用编译期配置)。 +4. **测试**: + - 优先级:本地设置 > dart-define > 默认; + - URL 规范化与非法输入拒绝; + - debug 下入口可见、release 下不可见(可用 `debugShowSetting` 之类的可注入开关测试); + - 不得删改既有测试致绿。 + +### 三、明确不做 + +- 不做多环境切换 UI(dev/staging/prod 列表)、不做远程配置下发。 +- 不改后端、不改接口、不改其他页面业务逻辑。 +- 不在 release 包暴露任何服务器地址修改能力。 + +### 四、门禁与 Git + +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。 +- 从干净 `develop` 拉 `feature/flutter-k13-dev-server-setting`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K13.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 五、K13 完成回执(2026-10-17) + +**结论**:新增 debug-only 运行时服务器地址覆盖。启动优先级为本地设置 > +`--dart-define(API_BASE_URL)` > 现有默认;release 不读取本地覆盖且不显示入口。 +未改接口、后端或其他页面业务逻辑;未跑真机。 + +- 客户端分支 `feature/flutter-k13-dev-server-setting` 从 `b762289` 完成并 + `--no-ff` 合并;功能提交 `907562a`,合并提交 `f95325d`,已推送 + `b762289..f95325d develop`。 +- 新增 `ServerEndpoint`,使用 `SharedPreferencesAsync` key + `dev.api_base_url`;URL 会 trim、校验 http(s) 和 host,并去掉末尾 `/`。 +- `main()` 在 `runApp` 前 debug 加载覆盖;ApiClient 创建时读取已解析地址。 +- 我的页设置弹窗新增服务器地址行、编辑/保存、非法值提示、重启生效提示和 + 恢复默认;入口可通过 `debugShowSetting` 注入,默认 `kDebugMode`。 +- 新增三语文案;既有语言设置流程保留。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:19 +309 ~1: All tests passed!`;不带 `dart-define` 的 Android debug + APK 在隔离副本中构建成功。 + +用户真机验证步骤与逐步预期、原始输出见 +`docs/plans/PROGRESS.md`「29. K13 运行时服务器地址设置」。 +设备侧修改地址、重启和网络连通性验收由用户执行。 + +--- + +## [待处理] 2026-10-17 | K12 首页去标题 + 空态视觉重做 + +**用户指令(2026-10-17)**:「有了全部分类后 全部剧目不要显示了 而且现在空状态UI太丑了」。 + +> 在 K9/K10/K11 基础上做视觉修订,不改接口、数据与业务逻辑。 + +### 一、现状(实测) + +- 首页 `lib/pages/discover/discover_view.dart`:分类 chips 下方有 `Text(l10n.allDramas)`(`Key('home-all-dramas-title')`),再往下才是 `_HomeDramaGrid`。 +- 空态组件 `lib/core/widgets/empty_state.dart`:96×96 圆角 28 **实心方块** + 42px 图标 + 16px 标题 + 13px 副标题 + TextButton。用户反馈“太丑”。 + +### 二、本批范围 + +#### K12-A 去掉首页「全部剧目」标题 + +- 删除 `Key('home-all-dramas-title')` 的 `Text` 及其下间距;分类 chips 与网格间距调整为 12–14px。 +- 网格、数据、空态逻辑不变;相关测试中“标题存在”的断言改为“不存在”,不得删测试文件。 + +#### K12-B 空态视觉重做(`empty_state.dart`) + +目标:轻盈、精致、留白多,去掉“大色块 + 图标”的模板感。 + +图形结构改为**双层同心圆**: + +- 外层圆:直径 84(compact 60),填充 `blue` 透明度 **0.05–0.06**,并加 1px 描边 `blue` 透明度 0.10–0.12; +- 内层圆:直径 56(compact 40),填充 `blue` 透明度 **0.10–0.12**; +- 中央图标:26–28px(compact 20–22),颜色深色主题 `blueSoft`、浅色主题 `blue`; +- 外层与内层同心居中,用 `Stack` 或两层 `Container` 嵌套实现。 + +文字与间距: + +- 标题 15px / w600 / `onSurface`,与图形间距 14(compact 10); +- 副标题 12.5px / `muted` / 行高 1.45,与标题间距 6; +- 操作按钮:保留 `TextButton`,字号 13、主色、点击区 ≥44pt,与副标题间距 8; +- 整体 `Padding` 上下 28(compact 16),最大宽度 260; +- 不新增图片资源、不加动画。 + +#### K12-C 测试 + +- 更新 `empty_state` 相关断言:存在 `empty-state-art` 与内层圆(可用新 Key `empty-state-art-inner`)、图标尺寸、标题/副标题/按钮; +- 首页测试:`home-all-dramas-title` 不存在;chips 后直接出现 `home-all-dramas-grid`; +- 既有测试不得删改致绿。 + +### 三、门禁与 Git + +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(构建前确认无并发 `flutter run`/`build`)。 +- 从干净 `develop` 拉 `feature/flutter-k12-home-empty-polish`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K12.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 五、K12 完成回执(2026-10-17) + +**结论**:首页「全部剧目」标题已删除,chips 与网格间距为 14px;空态改为 +双层同心圆视觉。未改接口、数据、轮播、三态或其他页面;未跑真机。 + +- 客户端分支 `feature/flutter-k12-home-empty-polish` 从 `b5a8656` 完成并 + `--no-ff` 合并;功能提交 `2dd4627`,合并提交 `b762289`,已推送 + `b5a8656..b762289 develop`。 +- 外层圆 84(compact 60),blue 5.5% + 11% 描边;内层圆 56(compact 40), + blue 11%;图标 27(compact 21)。 +- 标题 15/w600、副标题 12.5、按钮 13;上下 padding 28(compact 16), + 最大宽 260。 +- 新增 `empty-state-art-inner`、`empty-state-padding` Key;外层 + `empty-state-art` 及图标/标题/按钮 Key 保留。 +- 首页删除 `home-all-dramas-title`,chips 后直接进入网格;既有壳层中的 + `All Dramas` 锚点改为稳定网格 Key,测试流程和用例均保留。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:17 +302 ~1: All tests passed!`;`flutter build apk --debug` 成功。 + +用户真机验证步骤与逐步预期、原始输出见 +`docs/plans/PROGRESS.md`「28. K12 首页去标题与空态重做」。 +设备侧视觉、深浅主题与小屏验收由用户执行。 + +--- + +## [待处理] 2026-10-17 | K11 首页网格 3 列 + 分类首位「全部」 + +**用户指令(2026-10-17)**:「现在改首页,一行显示 3 个,而且我要的全部是显示在分类的第一个」。 + +> 在 K9 已实现的 2 列网格基础上做最小修订,不改接口、不改业务逻辑、不动后端。 + +### 〇、开工前必读 + +1. K9 计划与实现(`lib/pages/discover/widgets/home_drama_grid.dart`、`discover_view.dart`) +2. K5 主题 token +3. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测) + +- `lib/pages/discover/widgets/home_drama_grid.dart`:`SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 16, crossAxisSpacing: 12, childAspectRatio: 2/3)`。 +- `lib/pages/discover/discover_view.dart:273`:分类 chips 第一项为 `(code: CategoryController.allCode, label: l10n.forYou)`(文案「为你推荐」)。 +- 其余(轮播、空态、三态)保持 K9/K10 现状。 + +### 二、本批范围 + +1. **网格改 3 列**:`crossAxisCount: 3`;`mainAxisSpacing: 14`、`crossAxisSpacing: 10`,`childAspectRatio` 保持 `2/3`;3 列下标题最多 2 行、分类 1 行,避免溢出。 +2. **分类首位改「全部」**:chips 第一项仍使用 `allCode`,文案改为新增 l10n `all`(中文「全部」/ English「All」/ Bahasa Indonesia「Semua」);保留「热门 / 最新 / 服务端分类」顺序不变。 +3. **同步测试**: + - 更新/新增断言 `crossAxisCount == 3`; + - 断言分类 chips 第一项文案为「全部」(按 locale); + - 既有 K9/K10 测试不得删改致绿。 +4. **不做**:不改轮播、空态、三态、数据源、接口、其他页面;不新增分页。 + +### 三、门禁与 Git + +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(当前有 Android 真机 `flutter run` 在跑,构建前先确认没有并发构建占用 `build/`)。 +- 从干净 `develop` 拉 `feature/flutter-k11-home-grid-3col`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K11.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 五、K11 完成回执(2026-10-17) + +**结论**:首页网格已改为 3 列,分类首项仍绑定 `CategoryController.allCode`, +文案改为「全部 / All / Semua」。未改轮播、空态、三态、数据源、接口或其他页面; +未跑真机。 + +- 客户端分支 `feature/flutter-k11-home-grid-3col` 从 `0466b4b` 完成并 + `--no-ff` 合并;功能提交 `f0db631`,合并提交 `b5a8656`,已推送 + `0466b4b..b5a8656 develop`。 +- `crossAxisCount: 3`、`mainAxisSpacing: 14`、`crossAxisSpacing: 10`, + `childAspectRatio: 2 / 3` 保持不变。 +- 三份 ARB 原本已有 `all` key,分别为 All / 全部 / Semua,本轮直接复用, + 未插入重复 JSON key。 +- K9 网格测试更新为 3 列并断言「全部」位于「热门」之前;两个既有首页壳层 + 用例只把旧分类文案断言改为「All/全部」,导航流程和用例均保留。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。 + +用户真机验证步骤与逐步预期、原始输出见 +`docs/plans/PROGRESS.md`「27. K11 首页 3 列与全部分类」。 +设备侧 3 列布局、分类切换与详情跳转验收由用户执行。 + +--- + +## [待处理] 2026-10-17 | K10 统一空列表占位(EmptyState) + +**用户指令(2026-10-17)**:「做空列表的占位图,所有的列表,要求简单大方、符合审美」。 + +> 依赖 K5 主题 token;**不引入图片资源**(用内置 `Icons` + 柔和圆形底),保证离线、体积与风格统一。 +> 只改空态呈现,不改列表数据、接口与业务逻辑。 + +### 〇、开工前必读 + +1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md` +2. K5 主题 token(`AppColors` / `ColorScheme`) +3. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测,10 处空态) + +| 页面/组件 | 代码位置 | 现有文案 | +|---|---|---| +| 首页 | `lib/pages/home/home_view.dart:56` | `noStories` | +| 首页剧目网格 | `lib/pages/discover/widgets/home_drama_grid.dart:52-53` | `noCategoryDramas` / `noSearchResults` | +| 收藏 | `lib/pages/favorites/favorites_page.dart:107` | `noFavorites` | +| 通知 | `lib/pages/notices/notices_page.dart:115` | `noNotices` | +| 详情剧集 | `lib/pages/drama_detail/drama_detail_view.dart:130` | `noEpisodesYet` | +| 我的-片单 | `lib/pages/mine/mine_view.dart:676` | `noSavedShows` | +| 我的-剧目 | `lib/pages/mine/mine_view.dart:736` | `noStories` | +| 下载 | `lib/pages/downloads/downloads_page.dart:151` | `noDownloads` | +| 观看记录 | `lib/pages/watch_records/watch_records_page.dart:97` | `noWatchRecords` | +| 评论弹窗 | `lib/pages/player/widgets/drama_comments_sheet.dart:225` | `noComments` | + +现状均为纯文字(部分带按钮),风格不统一。 + +### 二、本批范围 + +#### K10-A 新建统一组件 `lib/core/widgets/empty_state.dart` + +规格(简单大方): + +- 布局:垂直居中,`Padding` 24;默认最大宽度 280,避免宽屏拉伸。 +- 图形占位:96×96 圆角 28 的容器,背景 `AppColors.blue` 透明度 0.10–0.12;中央 `Icons` 40–44px,颜色 `AppColors.blueSoft`(深色主题)/ `AppColors.blue`(浅色主题按 `Theme.brightness` 取)。 +- 标题:15–16px、w600、`onSurface`;居中,最多 2 行。 +- 副标题(可选):13px、`muted`、居中,最多 2 行。 +- 操作按钮(可选):`TextButton`/`OutlinedButton`,主色文字,最小点击区域 44pt;用于「重试」「去发现」等。 +- `compact` 变体:图形 64×64、图标 28、字号缩小,用于评论弹窗等内嵌区域。 +- 语义:外层 `Semantics(label: title)`;纯装饰图形 `excludeFromSemantics`。 +- 参数建议:`icon`、`title`、`subtitle?`、`actionLabel?`、`onAction?`、`compact=false`、`key`。 + +#### K10-B 替换 10 处空态 + +- 按上表逐一替换为 `EmptyState`,图标对应语义: + - 首页/剧目:`movie_outlined`;分类:`grid_view_rounded`;搜索:`search_off_rounded`; + - 收藏:`favorite_border_rounded`;下载:`download_outlined`;记录:`history_rounded`; + - 通知:`notifications_none_rounded`;评论:`chat_bubble_outline_rounded`;剧集:`video_library_outlined`; + - 首页加载失败仍用错误态(`error_outline_rounded`),不要用空态冒充失败。 +- 保留现有空态文案 key;如需副标题,优先复用已有 l10n,新增文案必须 en/zh/id 三份齐全(例如通用 `emptyHint`)。 +- 保留现有按钮行为(重试/去发现等);评论弹窗使用 `compact`。 +- 空态出现在原有滚动容器内时,保持可滚动与居中,不破坏三态切换。 + +#### K10-C 测试 + +- 组件单测:标题/副标题/按钮渲染、`compact` 尺寸、图标存在、Semantics。 +- 页面测试:至少覆盖 收藏/下载/记录/通知/评论 五处空态渲染为 `EmptyState`(可通过 `find.byType(EmptyState)` + 文案);既有测试不得删改致绿。 +- 深色/浅色、小屏不溢出。 + +### 三、明确不做 + +- 不引入图片/SVG/字体资源;不做动画插画。 +- 不改列表数据、接口、三态逻辑、页面结构。 +- 不改错误态与加载态,只统一空态视觉。 + +### 四、门禁与 Git + +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。 +- 从干净 `develop` 拉 `feature/flutter-k10-empty-states`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K10.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 五、K10 完成回执(2026-10-17) + +**结论**:新增统一 `EmptyState` 组件,任务书表格中的 10 处空态已全部替换; +错误/加载态保持原逻辑,未引入图片、SVG、字体或接口改动。未跑真机。 + +- 客户端分支 `feature/flutter-k10-empty-states` 从 `19babb8` 完成并 + `--no-ff` 合并;功能提交 `6d08330`,合并提交 `0466b4b`,已推送 + `19babb8..0466b4b develop`。 +- 默认图形 96x96、圆角 28,blue 11% 背景,图标 42;compact 为 64x64、 + 图标 28;标题 16/w600,副标题 13,操作按钮至少 44pt。 +- 深色主题图标 `blueSoft`,浅色主题图标 `blue`;外层 Semantics 只读标题, + 图形和文本不重复朗读,操作按钮保持可访问。 +- 10 处替换完成:首页、首页分类/搜索、收藏、通知、详情剧集、我的两处、 + 下载、观看记录、评论 compact。 +- 新增三语 `emptyHint`;原有空态 l10n key、页面文案和操作行为保留。 +- 新增 `test/empty_state/empty_state_test.dart` 4 个组件用例;收藏、下载、 + 记录、通知、评论五处页面测试已断言 `EmptyState`,评论还校验 compact。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。 + +用户真机验证步骤与逐步预期、原始输出见 +`docs/plans/PROGRESS.md`「26. K10 统一空列表占位」。 +设备侧深色/浅色、小屏与各页面空态验收由用户执行。 + +--- + +## [待处理] 2026-10-17 | K9 首页剧目列表改造(2 列全部剧目) + +**用户指令(2026-10-17)**:「优化首页,要有全部剧目的列表,一行 2 个,不用显示继续观看的」。 + +> 只改首页(发现页)信息流布局与卡片,不改接口、不改业务逻辑、不动后端。 +> 依赖 K5 主题 token 与现有 `RemoteImage` / 卡片视觉。 + +### 〇、开工前必读 + +1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md` +2. 当前首页 `lib/pages/discover/discover_view.dart`(649 行) +3. K5 主题 token、K7 轮播实现 +4. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测) + +- 首页结构(`discover_view.dart`):顶部 header → 轮播(home feed banners)→ 分类 chips(为你推荐/热门/最新/服务端分类)→「继续观看」标题 + 横向卡片列表(高 196、卡片宽 132,`_dramaContent` / `_continueHeader` / `_continueTile`)。 +- 数据源:`CategoryController.visibleDramas`(当前选中分类的剧目);`_cardsFor(...)` 生成展示卡片;分类切换已有过滤逻辑。 +- 「继续观看」区块使用 l10n `continueWatching`;横向列表不展示播放进度,本质是分类剧目横滑。 + +### 二、本批范围(首页单页改造) + +1. **移除「继续观看」区块**:删除 `_continueHeader` 调用与其标题;`_continueTile` 若不再被使用则一并删除(不留死代码)。 +2. **新增「全部剧目」2 列网格**: + - 位置:分类 chips 下方,占据原「继续观看」位置; + - 标题:新增 l10n `allDramas`(中文「全部剧目」/ English「All Dramas」/ Bahasa Indonesia「Semua Drama」); + - 网格:每行 2 个,左右边距 16、列间距 12、行间距 16;封面竖版比例 2:3,圆角 14; + - 卡片内容:封面、标题(最多 2 行)、分类/类型(最多 1 行),沿用现有深色渐变与文字层级; + - 数据:当前选中分类下的全部剧目(沿用 `visibleDramas`,去重后全量渲染);「为你推荐」显示全部; + - 点击:沿用 `logic.openCard(...)` 进详情; + - 长列表性能:外层改为 `CustomScrollView` + `SliverGrid`(或等价的懒构建方案),不要用 `GridView(shrinkWrap: true)` 嵌套整页;图片沿用 `RemoteImage` 懒加载。 +3. **保留**:header、轮播、分类 chips、底部导航、现有三态(加载/失败重试/空态)与空态文案(无分类剧目/无搜索结果)。 +4. **明确不做**:不动接口与数据模型、不改观看记录/收藏/下载、不改轮播与通知、不新增分页(feed 一次返回全量)、不改底部导航。 + +### 三、测试与门禁 + +- 新增/更新 widget 测试:首页不再出现「继续观看」;出现 `allDramas` 标题;网格为 2 列(断言 `SliverGridDelegateWithFixedCrossAxisCount.crossAxisCount == 2`);分类切换后网格内容更新;空态与失败态保持。 +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过;构建前确认无并发 `flutter run`/`build` 占用 `build/`。 +- 真机验收由用户执行:首页滚动、2 列布局、分类切换、点击进详情。 + +### 四、Git 与留痕 + +- 从干净 `develop` 拉 `feature/flutter-k9-home-grid`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K9.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 五、K9 完成回执(2026-10-17) + +**结论**:首页「继续观看」已移除,分类下方改为 `CustomScrollView + SliverGrid` +的 2 列「全部剧目」列表;分类切换、空态、失败态、全量测试与 APK 构建均完成。 +未改接口、数据模型、轮播、通知或其他页面;未跑真机。 + +- 客户端分支 `feature/flutter-k9-home-grid` 从 `99f64ab` 完成并 + `--no-ff` 合并;功能提交 `cf03178`,合并提交 `2c381be`,已推送 + `99f64ab..2c381be develop`。 +- `discover_view.dart` 从 649 行降到 487 行;新增 + `widgets/home_drama_grid.dart` 167 行。已删除 `_continueHeader`、 + `_dramaContent`、`_continueTile`、`_ShowcaseCard` 与 `_cardsFor`。 +- `SliverGridDelegateWithFixedCrossAxisCount` 为 2 列、列距 12、行距 16; + 左右边距 16;封面 2:3、圆角 14,标题最多 2 行、类型最多 1 行。 +- 新增三语 `allDramas`:`全部剧目` / `All Dramas` / `Semua Drama`。 +- 新增 `test/discover/home_all_dramas_grid_test.dart` 5 个用例,覆盖标题、 + 无继续观看、2 列参数、2:3 封面、分类切换、空态、失败态和 320px 小屏。 +- 既有 `test/widget_test.dart` 的 5 处「Continue Watching」可见文案断言原位 + 更新为「All Dramas」,所有既有用例与导航流程保留,未删除测试用例。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:18 +297 ~1: All tests passed!`;`flutter build apk --debug` 成功。 + +用户真机验证步骤与逐步预期、原始输出见 +`docs/plans/PROGRESS.md`「24. K9 首页 2 列全部剧目」。 +设备侧滚动、布局、分类切换和详情跳转验收由用户执行。 + +--- + +## [已处理·待用户验收] 2026-10-17 | K8 登录页视觉重设计 + +**用户指令(2026-10-17)**:「接下来做登录页面 ui 优化,现在这个 ui 不好看」。 + +> K6 已完成功能层优化(品牌区、autofill、密码显隐、键盘流转),本批只做**视觉重设计**,不改业务逻辑与接口。 +> 依赖 K5 主题 token(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)。 +> specs 无设计稿;以下为 PM 按「深色 + 克莱因蓝 + 印尼市场」给出的可执行视觉规格,按此实现。 + +### 〇、开工前必读 + +1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md` +2. K5 主题 token(`lib/core/theme/app_colors.dart`、`app_theme.dart`) +3. 当前登录页 `lib/pages/auth/auth_view.dart`(356 行,K6 后版本) +4. 本 INBOX 条目 + +### 一、本批范围(登录页单页视觉重设计) + +目标:从「表单堆叠」升级为有品牌感的深色登录页,保持现有逻辑与 Key 不变。 + +**布局结构(自上而下)** + +1. 全屏背景:`AppColors.ink`,顶部叠加一层克莱因蓝径向/线性渐变光晕(透明度 ≤0.18,不抢文字对比度)。 +2. 品牌区:44–52px 克莱因蓝圆角方块(内含白色播放/剧集图形图标,用 `Icons.` 即可,不引入图片资源)+ App 名 + 印尼市场 tagline(现有 `authBrandTagline`)。 +3. 表单卡片:`AppColors.card` 背景、圆角 20、1px 边框(用 K5 `outlineVariant`)、内边距 20;卡片内为标题、副标题、输入框、错误提示、主按钮。 +4. 主按钮:全宽、高 52、圆角 26、`AppColors.blue`、`onPrimary` 文字 16/w700;按下态 `bluePressed`;loading 时 18px 白色环且禁用输入。 +5. 登录/注册切换:改为**分段控件(Segmented)或 pill 双段**(登录 | 注册),选中段主色底/白字,未选中透明/`muted`;点击切换现有 `toggleMode()`,不再用文字链接。 +6. 底部辅助:保留当前模式说明或语言提示一行小字(`muted`,可选);不新增协议勾选。 + +**输入框规格** + +- 填充式背景 `AppColors.bar`,圆角 14,高 52,无外边框;聚焦时 1.5px `AppColors.blue` 边框 + 前缀图标变主色; +- 前缀图标:邮箱 `mail_outline_rounded`、密码 `lock_outline_rounded`;密码后缀显隐按钮保留; +- 标签与错误文案沿用现有 l10n;错误提示改为浅红底胶囊(红 10% 透明度 + 圆角 12 + 图标),不再裸文本。 + +**动效与可用性** + +- 首帧淡入 + 上滑(≤220ms,`TweenAnimationBuilder` 即可,不引第三方库); +- 小屏(iPhone SE)+ 键盘弹出不遮挡:沿用 `auth-scroll` 滚动与 `viewInsets` padding; +- 所有点击区域 ≥44pt;对比度:正文 ≥4.5:1; +- 保留 Key:`auth-scroll`、`auth-email`、`auth-password`;如新增控件需有稳定 Key 便于测试。 + +**明确不做** + +- 不改 `AuthLogic`、校验规则、`AuthController`、接口调用、登录/注册流程; +- 不做忘记密码、Google/Apple 登录、协议勾选、第三方 SDK; +- 不新增图片/字体资源;不改其他页面。 + +### 二、测试与门禁 + +- 新增/更新 widget 测试:分段切换仍改变模式、邮箱→密码→提交焦点流转、密码显隐、busy 禁用、错误胶囊展示、品牌区文案存在;既有 `test/auth/auth_page_test.dart` 不得删改用例。 +- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(注意当前有真机构建在跑,避免并发写 `build/`,先确认无构建进程)。 +- 真机验收由用户执行:视觉、键盘、小屏、深色对比度。 + +### 三、Git 与留痕 + +- 从当前干净 `develop`(`6cc5bfc`)拉 `feature/flutter-k8-auth-visual`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K8.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 四、K8 完成回执(2026-10-17) + +**结论**:登录页视觉重设计、交互回归、全量测试和 Android debug 构建均完成。 +未改 AuthLogic、校验、接口、登录/注册流程或其他页面;未跑真机。 + +- 客户端分支 `feature/flutter-k8-auth-visual` 已从 `6cc5bfc` 完成并 + `--no-ff` 合并;修复提交 `45c3c64`,合并提交 `99f64ab`,已推送 + `6cc5bfc..99f64ab develop`。 +- 深色背景、≤0.18 克莱因蓝光晕、48px 品牌方块、20px 圆角卡片、52px + 输入框与主按钮、pill 分段切换、错误胶囊、220ms 淡入上滑均按规格实现。 +- 既有 `auth-scroll/auth-email/auth-password/auth-switch-mode` Key、autofill、 + next→done→提交、busy 禁用、密码显隐与 Haptics 保留;新增视觉控件 Key。 +- 新增 `test/auth/auth_visual_test.dart` 6 个用例;既有 + `test/auth/auth_page_test.dart` 9 个用例未修改且全过。 +- `dart analyze lib test` 零告警;`flutter test` 为 + `00:19 +292 ~1: All tests passed!`;`flutter build apk --debug` 成功。 + +用户真机验证步骤与逐步预期、原始命令输出见 +`docs/plans/PROGRESS.md`「23. K8 登录页视觉重设计」。 +设备侧截图、键盘与小屏验收由用户执行。 + +--- + +## [待处理] 2026-10-17 | K7 首页轮播图 + 站内信通知 + +**用户指令(2026-10-17)**:「现在做首页轮播图及站内信通知」。 + +> **依赖 K5/K6**:轮播与通知页的配色、控件、震动反馈沿用 K5 主题与 K6 `Haptics`。 +> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.9,**契约里没有的接口一律不许自造**。 + +### 〇、开工前必读 + +1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md` +2. `FruitServer/docs/api-phase1.md` §1.3(feed banners)/ §1.9.2(notices) +3. K5 主题 token、K6 `Haptics` 封装 +4. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测) + +- **轮播图已有基础实现**:发现页 `lib/pages/discover/discover_view.dart` 的 `_carousel` 使用 `lib/pages/discover/widgets/cover_carousel.dart`(3D 翻转风格,手动滑动)。 + - 数据来自 `GET /app/discover/feed` 的 `banners[]`;**契约无独立 banner 接口**。 + - 现状缺口:无自动轮播、无页码/圆点指示器、单张时无特殊处理、图片失败无占位。 +- **站内信完全未接**:客户端没有 `notice_api` / 通知页;`GET /app/notices` 契约已实现。 + - 契约:`GET /app/notices?pageIndex=1&pageSize=20`(匿名),返回 `{total, list:[{id,title,content,type,publishAt,expireAt}]}`,`pageSize` 最大 50,空数据返回 `total=0,list=[]`。 + - 契约**没有已读/未读接口**,本批不做未读红点。 +- 我的页菜单 `lib/pages/mine/mine_view.dart` 的 `_menuCard` 已有「观看历史 / 我的收藏 / 下载 / 设置」列表,可新增「通知」入口。 +- 客户端尚未接 `GET /app/config`,本批**不接** `noticeEnabled` 开关(登记为后续项)。 + +### 二、本批范围(2 块 + 测试验收,按顺序做) + +#### K7-A 首页轮播图增强 + +- 数据源保持不变:`feed.banners`,不新增接口、不改后端。 +- 在现有 `CoverCarousel` 上做最小增强,**保留 3D 翻转风格**: + - 自动轮播:间隔 4.5s;用户拖拽/触摸期间暂停,松手后恢复;`items.length <= 1` 时不启动定时器; + - 页面不可见/组件 dispose 时取消定时器(避免后台耗电与 `setState after dispose`); + - 指示器:底部圆点或 `当前/总数`(二选一,克制);颜色用 K5 主题 token,未选中用 `muted` 低透明度; + - 图片加载失败显示占位底色 + 图标(复用 `RemoteImage` 的错误处理,不白屏); + - 点击行为沿用现有 `onOpen`(跳详情),不改变跳转逻辑; + - 无障碍:指示器与轮播容器提供语义标签(走 l10n)。 +- `items` 为空时保持现状 `SizedBox.shrink()`,不占位。 + +#### K7-B 站内信通知 + +- 新增数据层:`notice.dart`(model)、`notice_api.dart`、`notice_repository.dart`、`notices_controller.dart`,ID 按字符串解析。 +- 新增独立页 `lib/pages/notices/notices_page.dart`: + - 分页:首屏 `pageSize=20`,`list.length < total` 时展示「加载更多」并用 `pageIndex+1` 续拉; + - 三态:加载 / 空(「暂无通知」)/ 失败(可重试);支持下拉刷新; + - 列表项:标题、类型(`type` 原样展示或统一「公告」,不自造枚举映射)、`publishAt` 按 locale 格式化、内容摘要(最多 2 行); + - 点击打开详情弹窗(标题 + 发布时间 + 完整 `content`,保留换行);关闭按钮无障碍标签走 l10n。 +- 入口:我的页 `_menuCard` 新增「通知」ListTile(图标 `notifications_none_rounded`,主题色),点击打开 `NoticesPage`;不新增首页铃铛,避免改首页信息架构。 +- 匿名可访问,不要求登录;不做未读态、不做通知设置。 +- 文案全部进 en/zh/id 三份 arb。 + +#### K7-C 测试与验收 + +- 轮播:`pump` 时间推进后自动切页、单张不启动定时器、指示器数量与当前页、触摸暂停、点击回调。 +- 通知:首屏加载、分页追加、空态、失败重试、详情展示、时间格式化(id locale)。 +- 深色主题、小屏、长标题/长内容不溢出。 +- 真机验收由用户执行:轮播自动切换与指示器、通知列表/详情、三态与下拉刷新。 + +### 三、明确不做 + +- 不新增 banner 接口、不改 `/app/notices` 契约、不改后端/Web/specs。 +- 不做已读/未读、通知红点、推送(FCM)、通知设置。 +- 不接 `/app/config` 的 `noticeEnabled`(登记后续)。 +- 不新增首页铃铛入口、不改首页信息架构。 + +### 四、规范约束 + +- 颜色只用 K5 token;震动仅用于关键操作(K6 `Haptics`),轮播滑动不加震动。 +- 新增文案三语齐全;新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。 +- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。 + +### 五、门禁 + +1. `dart analyze lib test` 零告警;`flutter test` 全过。 +2. `flutter build apk --debug` 通过。 +3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。 +4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。 + +### 六、Git 与留痕 + +- 从当前干净 `develop` 拉 `feature/flutter-k7-carousel-notices`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K7.md`(范围、影响文件、契约缺口、风险、待确认)。 +- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 七、K7 完成回执(2026-10-17) + +**结论**:K7-A 轮播增强、K7-B 站内通知、K7-C 测试验收均完成并停止。 +未新增接口,未改后端/Web/specs。 + +#### 7.1 首页轮播 + +- 保持 `feed.banners` 数据源和 `CoverCarousel` 3D 翻转风格。 +- 4.5s 自动轮播;单张不启动 Timer。 +- 触摸/拖拽期间暂停,松手恢复。 +- `active=false` 或 `TickerMode` 不可见时暂停;dispose 取消 Timer。 +- 底部圆点指示器使用 `onPrimary` / `muted` K5 token,带三语语义标签。 +- 图片失败复用 `RemoteImage` 并显示图标占位。 +- 点击继续调用原有 `onOpen`。 + +#### 7.2 站内通知 + +- 新增 `notice.dart`、`notice_api.dart`、`notice_repository.dart`、 + `notices_controller.dart` 和 `NoticesPage`。 +- `GET /app/notices?pageIndex&pageSize=20`,匿名访问。 +- ID 按字符串解析,`total` 按数字解析。 +- 首屏、加载更多、空态、失败重试、下拉刷新均已接入; + 已加载数量 `< total` 时才展示加载更多。 +- 列表展示标题、原始 `type`、locale 时间、两行摘要。 +- 详情弹窗展示完整 `content`,保留换行并支持长内容滚动。 +- 我的页新增“通知”入口;匿名可进入,无登录闸。 +- `noticeEnabled` 属 `/app/config`,本批未接,登记后续。 + +真实 8081 匿名接口验证: + +``` +$ curl -sS 'http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=20' +{"code":0,"message":"ok","data":{"total":0,"list":[]}} +``` + +#### 7.3 门禁原始结果 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test test/discover/cover_carousel_test.dart test/notice +All tests passed! + +$ flutter test +00:14 +284 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.9s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过。 + +新增覆盖: + +- 自动轮播、单张无 Timer、交互暂停、active/TickerMode、圆点、图片占位、onOpen。 +- 通知 API 字符串 ID、controller 分页/空/失败/刷新。 +- NoticesPage 首屏、加载更多、加载/空/失败三态、详情换行、印尼时间。 +- 320×568 长标题/长内容不溢出;我的页匿名通知入口。 + +#### 7.4 Git + +- 功能提交:`b870956665f5268070c691533cc2a334f2aa7f59` + `feat(flutter): 增加轮播自动播放与站内通知` +- `--no-ff` 合并:`3e8703b`,已推送 + `b7b1c50..3e8703b develop -> develop` +- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。 + +#### 7.5 用户真机验证步骤与预期 + +1. 打开发现页顶部轮播,等待 4.5 秒。 + +预期:自动翻到下一张,圆点同步变化;单张 banner 不会自动循环。 + +2. 按住/拖动轮播超过 4.5 秒后松手。 + +预期:按住期间不自动翻页;松手后恢复自动轮播;拖拽仍保持 3D 翻转。 + +3. 暂时断开图片源或使用失败封面。 + +预期:显示深色占位和图片失败图标,不白屏。 + +4. 点击当前 banner。 + +预期:按原有 DRAMA 跳转逻辑进入详情。 + +5. 进入“我的 → 通知”,未登录也可访问。 + +预期:显示通知列表;空数据时显示“暂无通知”;断网时显示失败及重试。 + +6. 有超过 20 条通知时点击“加载更多”。 + +预期:下一页追加到列表,无重复;已加载数量达到 total 后按钮消失。 + +7. 下拉刷新,再点击一条长通知。 + +预期:刷新后列表回到第一页;详情显示标题、发布时间和完整正文; +正文换行保留且长内容可滚动。 + +8. 切换 Bahasa Indonesia 查看通知时间。 + +预期:时间按印尼 Material 格式显示。 + +设备侧截图与真机结果由用户执行。 + +#### 7.6 已知边界 + +- 无已读/未读、红点、FCM、通知设置。 +- 不接 `/app/config noticeEnabled`,通知入口当前常驻。 +- 无独立 banner 接口,未新增首页铃铛。 + +--- + +## [已处理·待用户验收] 2026-10-17 | K6 客户端 UI:登录页 + 底部导航优化 + +**用户指令(2026-10-17)**:「接下来优化登录页面 UI 及底部导航 UI」。 + +> **依赖 K5 完成**:必须基于 K5 的克莱因蓝主题(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)与统一 `ColorScheme` 再做,不得沿用旧 `#2F7BFF`。 +> specs 无 UI 设计稿(已确认),本批以「视觉统一 + 可用性」为目标,不自行改变信息架构。 + +### 〇、开工前必读 + +1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md` +2. K5 条目与 K5 计划文档(色值 token) +3. specs `docs/02-技术架构.md`(印尼首发)、`docs/19-术语表.md` +4. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测) + +- 登录页 `lib/pages/auth/auth_view.dart`(246 行):AppBar + ListView,两个 `TextField`、`FilledButton`、错误行、登录/注册切换链接。 + - 密码**无显隐切换**;无 `autofillHints`;无键盘 `next/done` 流转;错误提示写死 `#FF6B6B`。 + - 主色仍直接引用 `AppColors.blue`,无品牌区;小屏键盘遮挡只靠 ListView 自然滚动。 +- 底部导航在 `lib/pages/home/home_view.dart` 的 `_HomeTabs`(约 182–260 行):自定义 `Container` + `Row` + 4 个 `_TabButton`。 + - 高度 63,背景 `#0D0D11`、上边框 `#242329` 均为硬编码;只有选中/未选中图标,无指示条;无统一主题 token。 + - 现有 Key:`home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`,**测试依赖,不得改名**。 + - 播放沉浸模式(`videoImmersive`)下隐藏导航,需保留。 + +### 二、本批范围(3 块 + 测试验收,按顺序做) + +#### K6-A 登录页 UI 优化 + +- 品牌区:登录页顶部增加 App 名与一句印尼市场定位副标题(文案走 l10n 三语),使用 K5 主题色;不引入图片资源。 +- 表单体验: + - 邮箱 `autofillHints: [AutofillHints.email]`、密码 `[AutofillHints.password]`; + - 密码框增加显隐切换(图标按钮,语义标签走 l10n); + - 键盘动作:邮箱 `next` → 密码 `done` → 提交;提交中禁用输入与按钮; + - 错误提示统一样式,颜色取主题语义错误色,不再硬编码 `#FF6B6B`。 +- 布局与可用性:小屏 + 键盘弹出不遮挡输入与提交按钮(`resizeToAvoidBottomInset` + 滚动/SafeArea);所有点击区域 ≥44pt;主按钮用 `blue`,按下态用 `bluePressed`。 +- **不改**登录/注册业务逻辑、接口调用、字段校验规则与切换逻辑;**不做**忘记密码(契约无接口)、Google/Apple 登录(属 K4)、隐私协议勾选(无契约要求)。 + +#### K6-B 底部导航 UI 优化 + +- 统一使用 K5 主题 token:背景、边框、选中/未选中色全部来自 `AppColors` / `ColorScheme`,移除 `#0D0D11`、`#242329` 等硬编码。 +- 视觉:选中项为主色图标 + 主色标签 + 明确的指示条/背景胶囊(二选一,保持克制);未选中为 `muted`;图标 22–24、标签 10–11,间距一致。 +- 可用性:整体高度与底部安全区适配(含 iPhone home indicator),每个 tab 点击区域 ≥48pt;切换动效 ≤200ms,不引入第三方动画库。 +- **保持**:4 个 tab 的数量、顺序、导航逻辑与全部 Key 不变;播放沉浸模式仍隐藏导航。 +- **不做**:新增 tab、改信息架构、改页面内容。 + +#### K6-C 点击震动反馈 + +> specs 无触觉反馈规范;此为用户 2026-10-17 明确指令,按下方口径执行。客户端当前 `HapticFeedback` 调用数为 0。 + +- 使用 Flutter 内置 `HapticFeedback`(`package:flutter/services.dart`),**不引入第三方插件**。 +- 统一封装 `lib/core/platform/haptics.dart`(如 `Haptics` 抽象 + 默认实现),页面通过依赖注入调用,便于 widget 测试替换为 fake;禁止在页面里散落直接调用静态 `HapticFeedback.*`。 +- 触发范围(克制,只做关键操作): + - 底部导航 tab 切换:`selectionClick()`; + - 主操作按钮成功触发时:登录/注册提交、收藏/取消收藏、点赞/取消点赞、评论发送、举报提交、下载授权申请:`lightImpact()`; + - 危险操作确认(清空观看记录、清空下载、删除记录/授权):`mediumImpact()`。 +- 不在普通列表项点击、滚动、页面切换上震动;不新增「震动开关」设置项(避免超出本批)。 +- 调用不阻塞 UI:`unawaited(...)`;异常不得影响业务(触觉不可用时静默忽略,禁止空 catch 之外的吞错写法)。 +- 真机验收:iPhone 上逐项可感知;iOS 低电量模式会自动禁用触觉,验收时确认未处于低电量模式。 + +#### K6-D 测试与验收 + +- widget 测试至少覆盖: + - 底部导航 4 个 Key 存在、选中态随 tab 切换、沉浸模式下隐藏; + - 登录页密码显隐切换、提交中按钮禁用、错误提示展示、键盘 next/done 焦点流转(可用 `tester.testTextInput`); + - 震动反馈:用 fake `Haptics` 断言 tab 切换触发 `selectionClick`、主操作触发 `lightImpact`、危险操作确认触发 `mediumImpact`,且普通列表点击不触发; + - 无新增硬编码色值(沿用 K5 的扫描测试口径)。 +- 深色主题、小屏(iPhone SE 级别)、键盘弹出三种场景不溢出。 +- 真机验收由用户执行:登录页视觉与交互、底部导航选中态与安全区。 + +### 三、规范约束 + +- 颜色只允许来自 K5 的 `AppColors` / `ColorScheme`;页面禁止硬编码色值。 +- 新增文案必须 en/zh/id 三份 arb 齐全并重新生成;禁止硬编码文案。 +- 保持现有 Key 与业务逻辑;函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层。 +- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。 + +### 四、门禁 + +1. `dart analyze lib test` 零告警;`flutter test` 全过。 +2. `flutter build apk --debug` 通过。 +3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。 +4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。 + +### 五、Git 与留痕 + +- 从 K5 合并后的干净 `develop` 拉 `feature/flutter-k6-auth-nav-ui`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K6.md`(范围、影响文件、风险、待确认)。 +- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 六、K6 完成回执(2026-10-17) + +**结论**:K6-A 登录页、K6-B 底部导航、K6-C 点击震动反馈与 K6-D 测试验收均已完成 +并停止。未做 K4,未接新接口,未改后端/Web/specs,未跑真机。 + +#### 6.1 登录页 + +- 新增 App 品牌区、印尼市场副标题与管理入口一致的三语文案。 +- 邮箱/密码 autofill;密码显隐按钮与三语语义标签。 +- 键盘 next → done → 提交,提交中禁用输入、显隐、提交和模式切换。 +- 错误提示使用 `ColorScheme.error`,不再硬编码错误红。 +- submit、显隐按钮、模式切换点击区均 ≥44pt;小屏键盘场景可滚动到 submit。 +- 登录/注册接口、校验规则和模式切换逻辑未改。 + +#### 6.2 底部导航 + +- 背景 `AppColors.bar`、边框 `ColorScheme.outlineVariant`,移除旧深色边框/背景硬编码。 +- 选中项使用 `bluePressed` 胶囊、`onPrimary` 图标、`blueSoft` 标签; + 未选中使用 `muted`。 +- 160ms 内置动画;64pt 容器 + SafeArea 底部适配;每个 tab 点击区 ≥48pt。 +- 4 个 tab 数量、顺序、回调和 Key 不变: + `home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`。 +- `videoImmersive` 为 true 时导航继续隐藏。 + +#### 6.3 点击震动反馈 + +- 新增 `lib/core/platform/haptics.dart`:`Haptics` 抽象 + + `FlutterHaptics` 默认实现;通过 `LumaDramaApp` Provider 注入,测试使用 fake。 +- 页面没有直接调用静态 `HapticFeedback.*`;静态扫描限定封装文件内部调用。 +- 触发范围: + - tab 真正切换:`selectionClick()`;重复点击当前 tab 不触发。 + - 登录/注册提交、收藏/取消收藏、剧集/评论点赞、评论发送、举报提交、 + 下载授权申请:`lightImpact()`。 + - 清空观看记录、清空下载、删除记录、删除下载授权:`mediumImpact()`。 +- 所有页面调用均为 `unawaited`;平台异常由默认实现静默忽略,不影响业务。 +- 普通列表点击、滚动、页面跳转不触发。 + +#### 6.4 门禁原始结果 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:14 +263 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.6s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。 + +新增测试覆盖: + +- 登录品牌、autofill、密码显隐、next/done、busy 禁用、语义错误色、 + 44pt 点击区和小屏键盘滚动。 +- 底栏 4 Key、选中态切换、主题背景/边框、48pt 点击区、160ms 动效和沉浸隐藏。 +- Fake Haptics 的 selection/light/medium 触发、重复 tab 不重复触发、 + 普通列表点击不触发。 +- 静态调用扫描与平台触觉异常静默测试。 +- Klein Blue 旧色值扫描继续为零。 + +#### 6.5 Git + +- 登录页/底部导航提交:`e609e3a` + `feat(flutter): 优化登录页与底部导航` +- 震动反馈提交:`8a481d5` + `feat(flutter): 增加关键操作震动反馈` +- `--no-ff` 合并提交:`0cedaab` + `merge: 合并 K6 登录导航与震动反馈` +- 已推送:`e52d818..0cedaab develop -> develop` +- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。 + +#### 6.6 用户真机验证步骤与预期 + +前置:iPhone 关闭低电量模式并确认系统触觉正常;安装最新 debug APK。 +以下证据必须由用户在设备侧取得,Codex 未执行真机验证。 + +1. 打开登录页,检查品牌名、印尼市场副标题、输入框和主按钮。 + +预期:三语显示正确;按钮为克莱因蓝白字;密码眼睛按钮可切换显隐; +错误提示为系统语义红色;所有点击区易按。 + +2. 小屏手机打开键盘,使用邮箱键盘 next,密码键盘 done。 + +预期:焦点从邮箱进入密码;done 触发登录提交;键盘弹出时 submit 可滚动到可见区域。 + +3. 使用延迟网络或错误账号观察提交过程。 + +预期:提交时两个输入框、眼睛、submit 和模式切换全部禁用;加载指示可见; +完成后恢复或展示后端错误,不崩溃。 + +4. 在浅色/深色系统主题与小屏下检查底部 4 个 tab。 + +预期:4 个 Key 对应入口均存在且顺序不变;选中胶囊和指示清晰; +底部有安全区留白;无文字/图标重叠。 + +5. 逐一点击非当前 tab。 + +预期:每次真正切换触发轻微 selection 触觉;重复点击当前 tab 不再触发。 + +6. 登录提交、收藏/取消、剧集或评论点赞、发送评论、提交举报、申请下载授权。 + +预期:关键操作触发 lightImpact;触觉不影响请求速度或业务结果。 + +7. 清空观看记录、清空下载、删除记录或下载授权并确认。 + +预期:确认后触发更明显的 mediumImpact;取消确认不触发。 + +8. 点击普通列表项(如“我的”中的语言入口)并滚动列表。 + +预期:不产生震动;页面切换和滚动保持无触觉。 + +9. 临时开启 iOS 低电量模式后重复上述操作。 + +预期:系统可能禁用触觉,但业务、页面和无异常行为保持正常; +正式验收请关闭低电量模式。 + +设备侧截图、触觉感知结果与系统低电量测试由用户执行。 + +#### 6.7 iOS Xcode SPM 构建修复 + +- `Runner.xcscheme` 新增 “Bootstrap Flutter SPM Package” PreAction: + 按 `FLUTTER_ROOT → PATH → $HOME/flutter → /opt/homebrew` 兜底解析 + Flutter,执行 `flutter pub get`,找不到时明确报错并非 0 退出。 +- 原有 “Run Prepare Flutter Framework Script” 保留,并改为同一兜底链解析 + Flutter root 后执行 `xcode_backend.sh prepare`。 +- `xmllint --noout` 通过;`xcodebuild -list` 可识别 Runner scheme 与全部 + SPM source packages。 +- 在已有 SPM 包时完整执行: + `xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build` + 结果为 `BUILD SUCCEEDED`;日志确认执行 PreAction,输出 + `Resolving dependencies...`、`Got dependencies!`。 +- 删除 `ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage` + 后立即运行同一 `xcodebuild`,Xcode 在 “Resolve Package Graph” 阶段先失败, + 尚未运行 PreActions: + `Could not resolve package dependencies ... folder doesn’t exist`。 + 因此 **scheme-only 修复无法覆盖“包从未生成”的首次冷启动**; + 该冷启动自动生成场景标记为未通过验证。 +- 当前可靠口径:新克隆、`flutter clean` 或手动删除 ephemeral 后,先执行一次 + `flutter pub get`,再从 Xcode GUI 运行;正常构建期间 PreAction 会继续刷新包。 +- 修复提交:`cc037a2956848128c4899df1f5092fea44d3620b` + `fix(ios): 刷新 Flutter SPM 包后再准备框架` +- 合并:`ac706a4`,已推送 ReelShort `develop`。 + +#### 6.8 导航白线与观看历史缺陷修复 + +- 底部导航白线:M3 默认深色 `outlineVariant` 实测亮度为 `1.0`。 + 已新增显式 token: + `outlineDark=#3A3C45`、`outlineVariantDark=#242329`、 + `outlineLight=#8A8E99`、`outlineVariantLight=#D6D9E2`; + light/dark ColorScheme 均显式赋值,底栏边框使用主题 + `outlineVariant`。 +- 新增测试断言导航边框不是白色、深色亮度 `<0.2`, + 并覆盖 light/dark ColorScheme token。 +- 观看历史诊断: + - 只读查询 `UKZBA9IBE` 得到两条正常记录:剧 1/集 1、剧 5/集 7; + - 真实 8081 recent 返回结构正常,客户端诊断账号解析出 2 条记录并成功补详情; + - 未复现用户设备上的 401/100000、JSON 异常或详情失败; + - 当前代码缺陷是 `WatchRecordsController` 把 recent 异常统一折叠为 + `progressError`,后端原文无法定位。 +- 修复:controller 对 recent/详情异常打印真实类型、异常与堆栈; + `ApiException.message` 透传到页面错误态,页面新增稳定 retry Key。 +- 临时真实联调诊断文件已删除,不进入提交。 +- 用户设备上的具体错误仍待设备侧日志确认,未宣称根因已完全复现。 + +定向门禁: + +``` +$ flutter test test/theme test/home/home_nav_ui_test.dart test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart +00:01 +21: All tests passed! +``` + +全量门禁: + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:13 +266 ~1: All tests passed! + +$ flutter build apk --debug +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +Git: + +- 修复提交:`732483465ced2b28438585a4b2f1573e9a0529cb` + `fix(flutter): 修复导航白线与观看记录错误透传` +- `--no-ff` 合并:`b7b1c50`,已推送 `ac706a4..b7b1c50 develop -> develop` + +#### 6.9 已知边界 + +- 未增加震动开关;触觉遵循系统设置。 +- 不做忘记密码、Google/Apple 登录、隐私勾选或新 tab。 +- Xcode 首次包图解析早于 scheme PreActions;完全无 SPM 包时仍需先运行 + `flutter pub get`,本轮未通过 scheme 实现冷启动自动生成。 +- 观看历史缺陷未在用户真机上复现;需用户更新后抓 + `[WatchRecordsController] recent failed` / `detail failed` 日志与页面原文。 +- K4 未开始。 + +--- + +## [已处理·待用户验收] 2026-10-17 | K5 客户端视觉:克莱因蓝主题 + 印尼市场适配 + +**用户指令(2026-10-17)**:「改 UI 及 App 整体颜色,克莱因蓝,针对印尼市场,认真阅读文档严格执行」。 + +> **本批只做 K5 视觉与本地化,不做 K4,不接新接口。** +> **颜色口径**:specs 全文没有成文色板或设计稿(已检索 `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`、`docs/01~20`、`docs/plans/*`、`FruitServer/docs/*`); +> 颜色值来自用户 2026-10-17 明确裁定的 **方案 A**,按下方色值执行,不得自行另选。 + +### 〇、开工前必读(按顺序) + +1. `AGENTS.md`(目标市场:首发印尼) +2. `CODING_STANDARDS.md`(Dart/Flutter 红线、门禁) +3. `GIT_WORKFLOW.md`(Git Flow) +4. specs `docs/02-技术架构.md`(印尼首发)、`docs/15-成本清单与预算.md`(印尼语/Android 主导)、`docs/19-术语表.md`、`docs/16-测试用例库.md` +5. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测,直接用) + +- 主题集中在 `lib/core/theme/app_colors.dart`:`blue #2F7BFF`、`blueSoft #8FB4FF`、`ink/bar/card/muted`。 +- `lib/app.dart` 浅色与深色两套 `ColorScheme.fromSeed` 都硬编码 `seedColor: 0xFF2F7BFF`,另有 `scaffoldBackgroundColor`。 +- **42 处硬编码蓝色**散落 14 个文件,分布:`mine_view.dart` 13、`discover_view.dart` 5、`player_view.dart` 4、`comment_tile.dart` 3、`drama_detail_view.dart` 3、`home_view.dart` 2、`auth_view.dart` 2、`app.dart` 2,其余 7 个文件各 1。 +- `lib/core/config/locale_controller.dart` **只支持 `en` / `zh`**,选 `id` 会落成 null(跟随系统)——印尼首发缺陷。 +- `app.dart` 的 `localeResolutionCallback` 在系统语言不支持时回退 `supported.first`(当前为 en),与「首发印尼」不符。 +- 三语 arb 齐全:`app_en.arb` / `app_zh.arb` / `app_id.arb` 各 243 个 key。 + +### 二、本批范围(4 块,按顺序做) + +#### K5-A 主题色系统(克莱因蓝) + +色值(用户裁定,方案 A): + +| Token | 值 | 用途 | +|---|---|---| +| `blue` | `#002FA7` | 主色(Klein Blue / IKB) | +| `bluePressed` | `#00227A` | 按下/激活态 | +| `blueSoft` | `#5B7BE0` | 深色背景上的高亮/次级文字与图标 | +| `onPrimary` | `#FFFFFF` | 主色上的文字/图标 | +| `ink/bar/card/muted` | 保持现值 | 深色底不改成蓝色,不破坏现有暗色风格 | + +- `app_colors.dart` 增加上述 token,并把 `blue` 改为 `#002FA7`;`blueSoft` 若与现有语义冲突,明确注释用途。 +- `app.dart` 两套主题改为显式 `ColorScheme`(或 `fromSeed` + 覆盖 `primary/onPrimary`),确保浅色与深色下主色一致为 `#002FA7`;`seedColor` 不得再出现其他蓝色字面量。 +- **对比度硬要求**:正文文字 ≥ 4.5:1、大字号/图标 ≥ 3:1;主色大面积背景使用 `bluePressed`,主色上的文字用 `onPrimary`。 +- 新增单测:至少断言 `blue == Color(0xFF002FA7)`、`onPrimary` 在白底/主色上的对比度达标。 + +#### K5-B 全量 UI 替换 + +- 把 42 处硬编码蓝色全部改为 `AppColors.*` 或 `Theme.of(context).colorScheme.*`,页面层禁止再出现 `0xFF2F7BFF` / `Colors.blue`。 +- 覆盖页面:首页、发现、详情、播放(右栏/进度条)、我的、评论弹窗与评论项、观看记录、收藏、下载、登录、`AuthGate`、快捷抽屉。 +- 统一控件:FilledButton/TextButton、Tab 指示器、Switch、进度条、选中态、链接、SnackBar 主色。 +- **不改语义色**:错误红、成功绿、点赞红(如有)保持原语义,不因主题替换改成蓝色。 + +#### K5-C 印尼市场适配 + +- `LocaleController` 支持 `id`:`'id' => Locale('id')`,读写 `ui.locale` 持久化。 +- 默认语言规则:用户未显式选择时跟随系统;系统语言不是 `en/zh/id` 时**回退 `id`**(首发印尼),不得回退 `en`。 +- 语言选择入口增加 **Bahasa Indonesia** 选项(现有入口位置沿用,不新造页面)。 +- 校验三语 arb:新增文案必须 en/zh/id 三份齐全;禁止硬编码文案。 +- 日期/数字本地化:检查现有格式化是否按 locale(`intl`),`id` 下日期/数字格式正确。 +- 不接 Google 登录、不做支付(属 K4/后续)。 + +#### K5-D 回归与验收 + +- 深色/浅色两套主题、加载/空/失败三态、小屏(iPhone SE 级别)均不破版。 +- 新增 widget 测试:主题主色生效、页面无硬编码蓝色(可用静态扫描测试)、`LocaleController` 支持 id 与回退 id。 +- 真机验收由用户执行:主题色、按钮/进度条/选中态、语言切换与印尼语显示。 + +### 三、明确不做 + +- 不改业务逻辑、接口调用、后端、`ReelShortWeb`、specs。 +- 不做 Google 登录、支付、下载落盘。 +- 不改 App 图标/启动图(无设计资源;如后续提供 Stitch 出稿再做)。 +- 不替换信息架构与页面布局,只做颜色、主题与本地化;布局仅在换色后出现对比度/溢出问题时做最小修正。 + +### 四、规范约束 + +- 颜色只允许在 `app_colors.dart` 与主题层定义;页面禁止硬编码色值(测试断言除外)。 +- l10n 三语齐全并重新生成;所有新增文案进三份 arb。 +- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。 +- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。 + +### 五、门禁 + +1. `dart analyze lib test` 零告警;`flutter test` 全过。 +2. `flutter build apk --debug` 通过。 +3. 新增测试覆盖:主题 token 与对比度、无硬编码蓝色扫描、locale id 支持与回退、语言切换。 +4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」,禁止用开发机 `curl` 冒充设备证据。 + +### 六、Git 与留痕 + +- 开工前先处理上一条诊断分支 `feature/flutter-ios-home-diagnostics`:按实际情况提交为诊断增强或清理回滚,确保 K5 从**干净的 `develop`** 拉分支(当前 develop 为 `e1147cf`)。 +- 分支:`feature/flutter-k5-klein-blue`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。 +- 先写 `FruitServer/docs/plans/2026-10-17-K5.md`(范围、影响文件、色值表、风险、待确认)。 +- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执(含用户验证步骤与预期)。 +- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。 + +### 七、K5 完成回执(2026-10-17) + +**结论**:K5-A 主题 token/ColorScheme、K5-B 蓝色收敛、K5-C 印尼语适配、 +K5-D 回归均已完成并停止,未开始 K4,未接新接口,未跑真机。 + +#### 7.1 诊断分支收口 + +- iOS 首页诊断插桩已保留在未合并分支 + `feature/flutter-ios-home-diagnostics`,提交 + `7efbed97587dd3ed02967dcbbaa6fb85693c1883`。 +- K5 从干净的 ReelShort `develop e1147cf` 拉出,未夹带诊断插桩。 + +#### 7.2 色值、主题与蓝色收敛 + +- `AppColors.blue = #002FA7`、`bluePressed = #00227A`、 + `blueSoft = #5B7BE0`、`onPrimary = #FFFFFF`。 +- `AppTheme.light()` / `AppTheme.dark()` 使用显式 ColorScheme,两套主题 + `primary` 均为 `#002FA7`、`onPrimary` 均为白色;SnackBar 使用深蓝背景白字。 +- 深色表面的小图标、链接和轨道使用 `blueSoft`;大面积选中背景使用 + `bluePressed`;主按钮使用 `blue + onPrimary`。 +- 页面层旧蓝 `0xFF2F7BFF`、`0xFF8FB4FF`、`Colors.blue` 扫描结果为空。 +- 错误红、成功绿、点赞红等语义色未改;App 图标/启动图未改。 + +#### 7.3 印尼市场适配 + +- `LocaleController` 支持选择、持久化和恢复 `id`。 +- 未显式选择时跟随系统;系统语言非 `en/zh/id` 时回退 `id`。 +- “我的”语言弹层与首页快捷抽屉均增加 Bahasa Indonesia; + 快捷抽屉改为 Wrap,避免 320px 小屏溢出。 +- 新增 `bahasaIndonesia` 三语文案并重新生成 l10n。 +- 观看记录与下载授权的日期/时间改由 `MaterialLocalizations` 按当前 locale + 和系统 12/24 小时设置格式化。 + +#### 7.4 门禁原始结果 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:12 +251 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 26.6s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。 + +新增测试覆盖: + +- Klein Blue token、白字/主色对比度、图标/深底对比度。 +- 浅色/深色 ColorScheme 与 widget 实际主题值。 +- `lib` 旧蓝字面量静态扫描。 +- locale `id` 选择、持久化、系统语言跟随与未知语言回退 `id`。 +- Bahasa Indonesia 选项、语言切换、印尼语日期/时间格式。 +- 320×568 深色小屏首页与“我的”页不溢出。 + +#### 7.5 Git + +- 功能提交:`10d448c8ba93aa2762e14babdb528805897b3992` + `feat(flutter): 应用克莱因蓝主题与印尼语适配` +- `--no-ff` 合并提交:`e52d81881c19202c7adc8d13a6f78f78e4f6182f` + `merge: 合并 K5 克莱因蓝主题与印尼语适配` +- 已推送:`e1147cf..e52d818 develop -> develop` +- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。 + +#### 7.6 用户真机验证步骤与预期 + +前置:安装最新 debug APK,分别准备一套 Android 与 iOS 设备,保留系统深色/浅色 +切换条件。以下证据必须由用户在设备侧取得,Codex 未执行真机验证。 + +1. 浅色与深色系统下分别打开首页、发现、详情、播放、我的、收藏、记录和下载。 + +预期:页面可读、无溢出;主按钮为克莱因蓝白字;选中态/进度条可见; +深色底上的链接和图标使用较亮的蓝紫高亮;旧亮蓝 `#2F7BFF` 不再出现。 + +2. 对主按钮、进度条、选中 Tab 截图并用取色工具检查。 + +预期:主色接近 `#002FA7`;按下/激活大面积背景接近 `#00227A`; +主色按钮文字为 `#FFFFFF`。 + +3. 在 iPhone SE 级小屏打开首页与“我的”,展开语言入口。 + +预期:三个语言选项自动换行,不挤出屏幕;底栏文字和图标不重叠。 + +4. 从“我的 → 语言”选择 Bahasa Indonesia,再冷启动 App。 + +预期:界面切为印尼语,重启后仍保持印尼语。 + +5. 从首页快捷抽屉选择 Bahasa Indonesia。 + +预期:同样切为印尼语,两个入口行为一致。 + +6. 清除 App 数据/重新安装,把系统语言设为法语,且不手动选择语言。 + +预期:App 自动使用印尼语,不回退英语。 + +7. 在印尼语下查看观看记录与下载授权时间。 + +预期:日期/时间使用印尼地区 Material 格式;系统 12/24 小时设置变化时格式跟随。 + +8. 未登录点评论发布/点赞/举报,登录页按钮与错误/成功提示仍清晰可辨。 + +预期:业务流程不变;Klein Blue 按钮、蓝色 SnackBar 与红色错误提示对比度正常。 + +设备侧截图、系统语言切换日志与真机结果由用户执行;开发机检查不能替代这些证据。 + +#### 7.7 已知边界 + +- 现有主体页面保持 dark-first 设计,并显式使用 `AppTheme.dark()`; + 根部同时提供浅色/深色 ColorScheme,未重做信息架构或页面布局。 +- App 图标、启动图、Google 登录与支付不在本批范围。 +- K4 未开始。 + +--- + +## [已处理·待用户验收] 2026-10-17 | K3 客户端第三批:互动(评论 + 点赞 + 举报 + 分享) + +**用户指令(2026-10-17)**:客户端继续对接接口及 UI,严格执行 specs。 + +> **本批只做 K3,做完停下写回执,不要连做 K4。** +> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A6、`docs/16-测试用例库.md` INTERACT、`docs/09-测试计划.md`); +> 接口只认 `FruitServer/docs/api-phase1.md` §1.7,**契约里没有的接口一律不许自造**。 + +### 〇、开工前必读(按 specs 顺序,缺一不可) + +1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏) +2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表) +3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛) +4. `FruitServer/docs/api-phase1.md` §0 / §1.7(接口唯一权威) +5. `docs/plans/codex-研发任务清单.md`(A6)、`docs/16-测试用例库.md`(INTERACT-01~09)、`docs/09-测试计划.md` +6. 本 INBOX 条目 + +### 一、PM 已勘察的现状(实测,直接用) + +- ReelShort `develop` `45f5f25`(K2 已合并推送),互动现状全部是**本地演示数据**: + - [engagement_controller.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/engagement_controller.dart#L8-L8) 注释明确 `Local demo interactions ... No public/social backend`,内部维护 `liked` / `comments` 两个本地集合; + - 播放页右栏已有三个入口:点赞 `rail-like`、评论 `rail-comments`、分享 `rail-share`([player_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/player_view.dart#L339-L366)); + - 评论弹窗 [drama_comments_sheet.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/widgets/drama_comments_sheet.dart#L28-L33) 直接 `addComment` 到本地 `List`,无分页、无回复、无删除、无举报; + - 分享走 [platform_share_service.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/platform_share_service.dart) 的系统分享,**不需要后端接口**; + - 全工程没有举报入口。 +- 后端 §1.7 五个接口**均已实现**(`AppCommentController`、`AppInteractionController`),本批**不改后端**。 + +**契约字段(实测自后端 VO,直接用)** + +- `CommentRespVO`:`id,dramaId,episodeId,parentId,rootId,userId,uid,nickname,avatarUrl,content,status,auditReason,likeCount,replyCount,reportCount,createdAt,mine,liked`(ID 按字符串解析) +- `CommentPublishReqVO`:`dramaId`(必填)、`episodeId`、`content`(必填)、`parentId`(回复时传) +- 列表 `GET /app/comments?dramaId=&episodeId=&rootId=&cursor=&size=` → `CursorResult{list,nextCursor,hasMore}`;**匿名可读** +- `POST /app/likes/toggle`:`{targetType: DRAMA|COMMENT, targetId}` → `{liked, count}` +- `POST /app/reports`:`{targetType: COMMENT|USER|DRAMA, targetId, reasonCode(必填), description?}` → 工单 id +- 敏感词三档:`BLOCK` 直接报错、`REPLACE` 替换后放行、`REVIEW` 转待审(`status=pending`,仅自己可见) +- 限流:同一剧集每分钟 5 条评论(后端判定,客户端只展示返回的 `message`) + +### 二、本批范围(4 块,按顺序做) + +#### K3-A 评论 + +- 评论列表弹窗改为服务端数据:首屏 `size=20`,`hasMore` 时展示“加载更多”,用 `nextCursor` 续拉;三态(加载/空/失败可重试)。 +- 发布顶层评论:`dramaId + content`;发布回复:额外传 `parentId`(**只做一层楼层回复**,回复列表用 `rootId` 拉取,不做无限嵌套)。 +- 敏感词三档处理: + - `BLOCK`:原样展示后端 `message`,不清空输入框; + - `REPLACE`:展示后端替换后的内容(以响应为准); + - `REVIEW`:该条以 `status=pending` 出现在自己的列表里,并显示“审核中”标识(文案走 l10n)。 +- 删除自己的评论:仅 `mine=true` 显示删除入口,二次确认后调 `DELETE /app/comments/{id}`;失败回滚并提示。 +- 未登录:列表照常可看(匿名),**发布/点赞/举报前拦截**并弹现有 `AuthGate` 登录提示,不发业务请求。 +- 限流/重复提交:直接展示后端 `message`,前端不自造判断。 + +#### K3-B 点赞 + +- 剧集点赞:播放页 `rail-like` 接 `POST /app/likes/toggle`(`targetType=DRAMA`),乐观更新,失败回滚并提示;未登录先走 `AuthGate`。 +- 评论点赞:评论列表项内接 `targetType=COMMENT`,用返回 `{liked, count}` 更新该项。 +- **契约缺口(按此处理,不许改后端)**:`AppDramaDetailVO` / `AppDramaCardVO` 没有 `likeCount` / `liked`,客户端拿不到剧集点赞初始值。 + 本批**不展示剧集点赞数字**(避免假数据),只展示/切换当前会话内的 liked 状态;并在计划文档登记 + 「建议后端详情与卡片补 `likeCount` / `liked`(需 specs 负责人改契约)」。 + +#### K3-C 举报 + +- 入口:评论项“更多”菜单(举报评论)、详情页更多菜单(举报短剧)。**不做举报用户**(本批无用户主页入口)。 +- 弹窗:原因单选 + 可选描述,提交 `POST /app/reports`;成功后提示并关闭。 +- 失败透传:重复举报 `你已举报过该内容,我们正在处理`、举报自己 `不能举报自己发布的内容`,一律展示后端 `message`。 +- 未登录先走 `AuthGate`。 +- **契约缺口(登记并按下述降级)**:§1.7 未定义 `reasonCode` 枚举,后端不校验。本批前端固定 5 项: + `SPAM`(垃圾广告)、`ABUSE`(辱骂骚扰)、`PORN`(色情低俗)、`AD`(引流广告)、`OTHER`(其他);文案进三语 arb。 + 计划文档登记「建议后端补 `reasonCode` 枚举与后台映射」。 + +#### K3-D 分享 + +- 保持现有系统分享(`PlatformShareService`),**不新增后端接口**;只做回归验收(标题/简介/footer 走 l10n)。 + +### 三、明确不做(避免误判“没做完”) + +- 拉黑(TC-INTERACT-06):§1.7 无接口,本批不做。 +- 审核联动闭环(TC-INTERACT-09):属后台 `2.5 内容治理`,已实现,本批不做。 +- 后台敏感词管理、评论审核、举报工单处理:后台已有,本批不动。 +- 评论图片/表情/@提及/无限嵌套、我的评论列表:本批不做。 +- 并发点赞(TC-INTERACT-07):由后端幂等保证,客户端不压测,只做防连点。 + +### 四、UI 与规范约束(严格执行 specs) + +- **l10n**:所有新增文案(评论/回复/审核中/举报原因/失败提示)进 `lib/l10n/app_{en,zh,id}.arb` 三份齐全并重新生成;禁止硬编码。 +- **视觉**:沿用现有深色主题、`AppColors`、播放页右栏与底部弹窗样式;不自行改主题。 +- **三态**:评论列表必须有加载、空、失败(可重试);禁止白屏或假装成功。 +- **分层**:新增 `comment_api / interaction_api`(放 `lib/core/data/remote/`)、`comment_repository`、`comment_controller`;页面只依赖 controller/repository 抽象;沿用 K2 的分层与注入方式。 +- **ID**:全部按字符串解析(`json_numbers.dart`),禁止 `as int`。 +- **不要动**:K2 的分类/记录/收藏/下载逻辑、登录注册、播放进度核心;不改 `ReelShortWeb`、`FruitServer` 后端、specs。 +- 新文件首行 `// Author: fruit.com.cn`;public 类/方法必须有文档注释;无 AI 痕迹;测试命名 `should_<期望>_when_<条件>`。 +- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。 + +### 五、门禁(不达标不提交) + +1. `dart analyze lib test` 零告警;`flutter test` 全过(存量不得跑红)。 +2. `flutter build apk --debug` 通过;若缺 SDK,贴原始输出并标「未验证」。 +3. 新增测试至少覆盖:评论游标翻页与 hasMore、发布成功/敏感词 BLOCK/REPLACE/REVIEW、删除自己的评论与失败回滚、点赞乐观更新与回滚、举报成功/重复举报 message 透传、未登录 AuthGate、评论点赞计数更新。 +4. 真机/设备侧验证由用户执行(红线 7、10),回执给“用户可照做步骤 + 每步预期”,禁止用开发机 `curl` 冒充设备证据。 +5. Git:从 `develop` 拉 `feature/flutter-k3-interaction`,完成后 `git merge --no-ff` 回 `develop` 并 push;**不动 `main`**;只 `git add` 自己的文件。 + +### 六、节奏与留痕 + +- 先写 `FruitServer/docs/plans/2026-10-17-K3.md`:范围、影响文件、上述 3 个契约缺口、风险、待确认。 +- TDD:每块先红灯 → 实现 → 绿灯 → 重构;每完成一块跑一次 `dart analyze lib test` + 定向测试。 +- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`。 +- 做完停下写回执:真数据/本地数据页面清单、用户验证步骤与预期、门禁原始输出、提交号。 + +### 七、K3 完成回执(2026-10-17) + +**结论**:K3-A 评论、K3-B 点赞、K3-C 举报、K3-D 分享回归均已完成并停止, +未开始 K4。后端、`ReelShortWeb`、specs 均未修改。 + +#### 7.1 真数据 / 本地数据页面清单 + +真数据(走契约 §1.7): + +- 播放页评论弹窗:匿名评论列表、游标加载更多、顶层评论、一层回复、 + BLOCK/REPLACE/REVIEW 结果、本人删除、评论点赞。 +- 播放页剧集点赞:`POST /app/likes/toggle` 切换,乐观更新与服务端状态校准; + **不展示剧集点赞数字**。 +- 评论更多菜单与详情页更多菜单:评论/短剧举报,固定五项原因,可选描述。 + +本地/系统能力: + +- 剧集 liked 仅保存在当前 App 会话内。因详情/卡片契约没有 `likeCount/liked`, + App 重启或换账号后不展示服务端初始点赞状态。 +- 分享继续走 `PlatformShareService` 系统分享,不新增后端接口。 +- “我的 → 喜欢”只过滤当前 feed 的会话内 liked,没有契约外加“我的点赞列表”接口。 + +按降级方案未做:拉黑、举报用户、举报后台处理、无限嵌套、图片/表情/@、我的评论列表。 + +#### 7.2 门禁原始结果 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:12 +242 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.8s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +`~1` 是既有 `live_watch_flow_test` 在 8081 未启动时按测试机制跳过,不是失败。 + +#### 7.3 Git + +- 功能提交:`d361034226ec2bc431548d45068dc60046e5e473` + `feat(flutter): 接入互动评论点赞举报` +- `--no-ff` 合并提交:`9db6935308e65342be46c81419c2992ed2df1df0` + `merge: 合并 K3 客户端互动能力` +- 举报本人错误透传修复:`1488ee8f85671de96c70347c1b17fb9c4556bd88` + `fix(flutter): 透传举报本人评论错误` +- 修复合并提交:`e1147cfcbabcdf457e427bbd118c32d37f8d9b15` + `merge: 合并 K3 举报错误透传修复` +- 已推送:`45f5f25..9db6935`、`9db6935..e1147cf develop -> develop` +- author 与 committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。 + +#### 7.4 用户真机验证步骤与预期 + +前置:启动 8081 客户端接口服务,准备可登录测试账号;在 Android 设备安装 +`build/app/outputs/flutter-apk/app-debug.apk`,确认 App 使用设备可访问的后端地址。 +以下证据必须由用户在设备侧取得,Codex 未执行真机验证。 + +1. 未登录打开任意有评论的剧集,点播放页评论入口。 + +预期:评论列表正常加载;空列表显示“还没有评论”;断网后显示失败与重试,不白屏。 + +2. 未登录点发布、评论点赞或举报,再点剧集点赞。 + +预期:均先出现登录提示;关闭提示后不发业务请求;登录后可重试。 + +3. 登录后发表顶层评论,再输入命中 `REPLACE` 或 `REVIEW` 策略的词。 + +预期:顶层评论出现在列表;REPLACE 展示服务端替换后的内容;REVIEW 显示“审核中”; +BLOCK 原样显示后端 message 且输入框内容保留。 + +4. 在顶层评论点“回复”并发布。 + +预期:请求带 `parentId`;回复归入该楼层,只做一层回复;点“查看回复”可展开。 + +5. 连续加载超过首屏 20 条的评论。 + +预期:按 `nextCursor` 续拉,不重复、不跳页;没有下一页时加载更多消失。 + +6. 点赞一条评论和当前剧集,再点一次取消。 + +预期:图标立即切换;评论计数在服务端响应后校准;断网时状态回滚并提示; +剧集点赞不显示数字。 + +7. 删除自己的评论:更多 → 删除评论 → 二次确认。 + +预期:成功后从列表移除;制造断网/服务失败时恢复原位置并提示。其他人的评论不显示删除项。 + +8. 举报评论与短剧:更多 → 举报;分别选择 `SPAM/ABUSE/PORN/AD/OTHER` 和可选描述。 + +预期:成功后提示“举报已提交,感谢反馈。”;重复举报原样显示后端 +“你已举报过该内容,我们正在处理”;举报自己发布内容原样显示后端 message。 + +9. 点播放页分享。 + +预期:打开系统分享面板;文本顺序为标题、简介、`FruitShorts` footer; +切换 en/zh/id 后三类文案同步切换。 + +10. 在 Android logcat 保存评论/点赞/举报请求与响应,并保留关键页面截图。 + +预期:可见 `/app/comments*`、`/app/likes/toggle`、`/app/reports` 的真实响应; +设备侧取证由用户执行,开发机 `curl` 或本机返回 200 不替代本步骤。 + +#### 7.5 未验证与已知边界 + +- 真机、模拟器、设备日志、设备截图均未由 Codex 执行,状态为“设备侧取证待用户执行”。 +- 剧集点赞初始状态和数字因契约缺口无法展示,App 重启后当前会话状态丢失。 +- 敏感词具体词表、限流阈值、后台审核联动由后端/运营环境决定,本轮只做客户端透传与状态展示。 +- 拉黑与 K4 不在本轮范围。 + +--- + +## [已处理·待用户验收] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI + +**用户指令(2026-10-17)**:「现在先做客户端 对接相关接口 及ui 严格执行specs 内的要求」。 + +> **本批只做 K2,做完停下写回执,不要连做 K3/K4。** +> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A3/A5、`docs/16-测试用例库.md` CONTENT/RECORD、`docs/09-测试计划.md`); +> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.5 / §1.6,**契约里没有的接口一律不许自造**。 + +### 〇、开工前必读(按 specs 顺序,缺一不可) + +1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏) +2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表) +3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛) +4. `FruitServer/docs/api-phase1.md` §0 / §1.3 / §1.5 / §1.6(接口唯一权威) +5. `docs/plans/codex-研发任务清单.md`(A3/A5)、`docs/16-测试用例库.md`(CONTENT/RECORD)、`docs/09-测试计划.md` +6. 本 INBOX 条目 + +**硬性执行口径(摘自上述规范,违反即打回)** + +- **Git**:从 `develop` 拉 `feature/flutter-k2-library` 开发;提交信息 `type(scope): 描述`(`type` ∈ feat/fix/docs/test/refactor/style/perf/chore,≤50 字符,一个 commit 只做一件事);完成后 `git merge --no-ff` 回 `develop` 再 push;**禁止直接改 `main`**;只 `git add` 自己的文件,禁止 `git add -A`/`.`。 +- **TDD**:先写测试(红灯)→ 实现(绿灯)→ 重构;测试命名 `should_<期望>_when_<条件>`;核心类(repository/service/logic)必配 `*_test.dart`。 +- **Dart**:`provider` binding/logic/view 三层;`dio`;全程 `async/await`,**禁裸 `then()`**;IO 失败用结果对象/控制器错误字段承接,**不把异常直接抛到 UI**;禁止空 catch。 +- **代码规模**:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10;除 `0/1/-1/null/空串/布尔` 外禁止魔法值。 +- **注释**:每个 public 类/方法必须有文档注释;文件头写一行中文职责;新文件首行 `// Author: fruit.com.cn`;代码/注释/提交信息**不得出现任何 AI 痕迹**。 +- **留痕**:每步「叙述 + 工具命令 + 原始输出 + 解读」,追加到 `docs/plans/PROGRESS.md`;做前先写 `docs/plans/2026-10-17-K2.md`(步骤/影响文件/风险/待确认)。 +- **l10n 生成物**(`lib/l10n/app_localizations*.dart`)豁免作者头,但 `.arb` 必须手写三份。 + +### 一、PM 已勘察的现状(实测,直接用) + +- `ReelShort`(`develop` `00280ca`)已接:`/app/home/feed`、`/app/discover/feed`、`/app/dramas/{id}`、`/app/episodes/{id}/play`、`/app/watch/progress`(POST/GET)、`/app/watch/recent`、`DELETE /app/watch/progress/{dramaId}`、`/app/favorites`(toggle/list/delete)、账号 4 个接口。 +- **未接**:`GET /app/categories`、观看记录删除/清空、下载全部 8 个接口、`/app/me*`(属 K4)、互动(属 K3)。 +- 现状缺陷: + - 发现页分类**是本地按 sections 下标硬编码**([discover_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/discover/discover_view.dart#L363)),不是 `GET /app/categories`; + - "我的-记录"只渲染**能在已加载首页/发现目录里匹配到**的剧([mine_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/mine/mine_view.dart#L634-L716)),匹配不到就消失; + - 收藏列表嵌在"我的"页签里,不能单独刷新/取消收藏; + - 下载**完全没有**。 + +### 二、本批范围(4 块,按顺序做) + +#### K2-A 分类(A3 剩余) + +- 接口:`GET /app/categories` → `[{ id: string, name: string }]`(J1:**id 是字符串**)。 +- UI:发现页分类条改为**服务端分类**;点分类展示该分类的剧。 + - 内容来源只允许 `GET /app/discover/feed` 的 `sections[]` 中 `code == "category-{id}"` 的分区(后端已按分类生成,见 `AppCatalogService#discoverFeed`)。 + - 某分类在 discover 里没有对应分区 → 显示空态,**不要自己拼请求/自造接口**。 + - 保留现有 `hot` / `new` 分区的入口与样式。 + +#### K2-B 观看记录页(A5 剩余) + +- 接口: + - `GET /app/watch/recent?limit=20` → `[{ dramaId, episodeId, positionMs, durationMs, finished, playCount, updatedAt }]`(全部 ID 为字符串) + - `DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`(清空) +- **契约缺口(必须按下面方式处理,不许改后端)**:`recent` **不返回剧名/封面**。做法: + - 先去重 `dramaId`,再对每个 id 调 `GET /app/dramas/{id}` 补卡片; + - **并发上限 4**,结果按 dramaId 缓存(同页内不重复请求); + - 单条详情失败**不能拖垮整页**:该条显示占位(剧名"加载失败"+重试),其余正常渲染; + - 在计划文档里记为「契约缺口:建议后续在 `ProgressRespVO` 补卡片字段(需 specs 负责人改契约)」。 +- UI:独立"观看记录"页(入口:我的 → 观看记录) + - 每条:封面、剧名、第 N 集、`positionMs/durationMs` 进度、更新时间;点击进详情并续看; + - 单条删除、清空(二次确认);删除**乐观更新**,失败回滚并提示; + - 未登录 → 复用现有 `AuthGate` 登录引导(接口需要登录,见 `SecurityConfig`); + - 三态:加载中 / 空("还没有观看记录")/ 失败(可重试)。 + +#### K2-C 收藏列表页(A5 剩余) + +- 接口:`GET /app/favorites`(返回 `AppDramaCardVO[]`,**与首页卡片同结构**)、`DELETE /app/favorites/{dramaId}`、`POST /app/favorites/toggle`。 +- UI:独立"我的收藏"页(我的 → 收藏),复用首页卡片渲染: + - 取消收藏(按钮或长按/左滑,二选一但要能发现)→ **乐观更新 + 失败回滚**; + - 点击进详情;空/错/载三态;未登录 → `AuthGate`。 +- **契约缺口**:TC-RECORD-04 写"分页正确",但 `GET /app/favorites` **没有分页参数**、返回全量数组 → 本批按全量实现,计划文档登记该缺口,**不许自造 `page/size`**。 + +#### K2-D 下载授权与管理(A5 最后一块,**不做实际落盘**) + +- 接口(§1.6,全部需登录;设备号优先请求体、其次 `X-Device-Id`,现有 `ApiClient` 已带): + | 方法 | 路径 | 本批用途 | + |---|---|---| + | GET | `/app/downloads/policy` | 配额/授权天数/wifiOnly/清晰度/已用/剩余/`expirePolicyText` | + | PUT | `/app/downloads/settings` | 仅 Wi-Fi、最高清晰度 | + | POST | `/app/downloads` | 申请授权(body `episodeId`,`quality` 可省=AUTO) | + | GET | `/app/downloads` | 我的下载授权列表 | + | GET | `/app/downloads/{episodeId}/url` | 续传前刷新短时效地址(本批只提供 API 方法) | + | POST | `/app/downloads/{episodeId}/progress` | 进度上报(本批只提供 API 方法) | + | DELETE | `/app/downloads/{episodeId}` | 删除单条 | + | DELETE | `/app/downloads` | 清空,返回条数 | +- UI: + - 详情页/播放页加"下载"入口:调 `POST /app/downloads`;**付费/未解锁/配额满时直接展示后端 `message`**,不要自造判断(判定归服务端); + - 我的 → 下载:策略卡(剩余配额、授权天数、`expirePolicyText`)、授权列表(剧名、集号、`status`、`taskStatus`、`progressPercent`、到期时间)、单条删除、清空、仅 Wi-Fi/清晰度设置; + - 状态枚举全部**后端返回值驱动**(`ACTIVE/EXPIRED/REVOKED`、`QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED`),前端不新增枚举; + - 空/错/载三态 + 未登录 `AuthGate`。 +- **明确不做**(写进计划文档,避免误判"没做完"):文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验 —— 需要存储/权限/加密播放方案,另立批次。本批只交付"授权与状态管理"。 + +### 三、UI 与规范约束(严格执行 specs) + +- **语言**:所有新增文案必须进 `lib/l10n/app_{en,zh,id}.arb` **三份齐全**并重新生成;禁止硬编码中文/英文。印尼语为首发语言。 +- **视觉**:沿用现有深色主题与 `AppColors`/组件风格(specs 里未提供 Stitch 出稿;若用户后续给出稿,再单独做视觉替换,本批不自行改主题)。 +- **三态**:每个新页面/列表都要有加载、空、失败(可重试);失败文案走 `errorText` + l10n,禁止白屏/假装成功。 +- **分层**:新增 `category_api / download_api`(放 `lib/core/data/remote/`),页面只依赖 repository/controller 抽象;沿用 K1 的分层与依赖注入方式。 +- **ID**:19 位雪花 ID 一律按字符串解析(`json_numbers.dart`),**禁止 `as int`**;计数/时长同理按契约类型处理。 +- **不要动**:首页/发现已接 feed 逻辑(分类来源替换除外)、登录注册、播放/进度/收藏已有逻辑(除必要接线)、`ReelShortWeb`、`FruitServer` 后端。 +- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。 + +### 四、门禁(不达标不提交) + +1. `dart analyze lib test` **零告警**;`flutter test` **全过**(存量 33 个测试文件不得跑红)。 +2. `flutter build apk --debug` 通过(CODING_STANDARDS §6 门禁总表含构建);若本机缺 Android SDK,回执里贴失败原始输出并标「未验证」,不得静默跳过。 +3. 新增测试至少覆盖:分类→section 映射(含无分区空态)、记录页 N+1 详情请求去重/并发上限/单条失败降级、删除与清空回滚、收藏取消回滚、下载策略/授权列表渲染、未登录 AuthGate;核心类覆盖率 ≥80%。 +4. **真机/设备侧验证由用户执行**(红线 7、10):回执里给"用户可照做的步骤 + 每步预期结果",**禁止用开发机 `curl` 冒充设备可达**;拿不到设备证据就标「未验证」。 +5. Git:`feature/flutter-k2-library` 开发 → `git merge --no-ff` 回 `develop` → `git push origin develop`;**不动 `main`**;合并/推送前再跑一次门禁。 + +### 五、节奏与留痕 + +- 先写 `FruitServer/docs/plans/2026-10-17-K2.md`:范围、影响文件清单、上述 3 个契约缺口、风险、待确认。 +- **TDD 节奏**:每块先写测试(红灯)→ 实现(绿灯)→ 重构;每完成一个模块跑一次门禁,零告警才继续。 +- 每步在公司 `PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」(PM 验收口径,缺一即打回)。 +- 做完**停下写回执**(追加到 `docs/plans/PROGRESS.md` + 本条下),回执必须含: + - 哪些页面已走真数据、哪些仍是本地/未做; + - 用户可照做的验证步骤与每步预期; + - 测试与 analyze 的原始输出; + - 提交号。 +- 禅道:开工/完成各同步一次任务状态。 + +### 六、PM 登记:specs 契约缺口(Codex 不要动,反馈给项目负责人) + +1. **搜索**:任务清单 A3 写了"搜索"、TC-CONTENT-05 有搜索用例,但 `api-phase1.md` **没有 `/app/search` 接口** → 本批**不做搜索**。 +2. `GET /app/favorites` 无分页参数,与 TC-RECORD-04"分页正确"不一致。 +3. `GET /app/watch/recent` 不返回剧名/封面,记录页需 N+1 详情请求(建议后端补卡片字段)。 + +> **用户裁定(2026-10-17)**:**先按现状契约执行,上述缺口后期再补**。本批不得因缺口停工,也不得临时改后端或自造接口;按第二节的降级方案实现,缺口留到后续批次。 + +### 七、Codex 回执(2026-10-17) + +**状态**:K2-A/B/C/D 代码、自动化测试、Android debug 构建与 develop 推送已完成; +真机/模拟器侧业务验证**未验证**,按红线交用户执行。用户照做清单与测试跳过口径见 +`docs/plans/PROGRESS.md`「动作 17:用户真机验收清单」。K3/K4 未开始。 + +**提交** + +``` +feature: cc4d7c5a9dc6a4f6f0565a32503cce5973af703c +merge: 45f5f25640a274edb51bd90b3b5e52c43ba9d9cd +branch: feature/flutter-k2-library -> develop(--no-ff,已 push,feature 已删除) +main: 未改动 +``` + +**已接真实契约数据** + +- 发现页分类:`GET /app/categories` + `GET /app/discover/feed`; + 服务端分类只匹配 `category-{id}`,无分区显示空态。 +- 独立观看记录页:`GET /app/watch/recent?limit=20`、按 dramaId 去重并发上限 4 补详情、 + `DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`。 +- 独立我的收藏页:`GET /app/favorites`、`POST /app/favorites/toggle`、 + `DELETE /app/favorites/{dramaId}`。 +- 我的下载页:§1.6 全部 8 个 `/app/downloads*` 接口;详情页/播放页下载入口调用 + `POST /app/downloads`,拒绝时直接展示后端 `message`。 +- 三页面均为加载 / 空 / 失败可重试三态;未登录显示登录引导。 + +**仍本地或未做** + +- 搜索未做:契约没有搜索接口。 +- 文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验未做;本批只交付授权与状态管理。 +- 点赞/评论仍沿用既有本地状态,属 K3,不在本批。 +- 发现 feed 当前只下发 `category-{id}`;若后端没有 `hot`/`new` 分区,保留的入口显示空态。 +- 收藏接口仍返回全量数组,本批未发送 `page/size`。 + +**门禁原始输出** + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:11 +220 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 2,377ms +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +`~1` 是既有真实后端联调用例因本机 8081 服务未启动而按设计跳过,不是失败。 +首次冷构建也成功,耗时 `705.7s`;最终修复后缓存构建 `2,377ms`。 + +**用户验证步骤与预期** + +1. 在 ReelShort 执行 `git checkout develop && git pull origin develop`,确认 HEAD 为 + `45f5f25`;后端 8081 已启动,手机与开发机在同一局域网。 +2. 取局域网地址并运行:`IP=$(ipconfig getifaddr en0)`, + `flutter run -d --dart-define=API_BASE_URL=http://$IP:8081`。 + 预期:App 启动、首页/发现能加载;地址必须按当前网络重新获取。 +3. 打开发现页:分类条应出现 `/app/categories` 返回的分类。选择一个有内容分类, + 预期显示 discover feed 对应 `code=category-{id}` 的剧;选择无分区分类显示空态。 +4. 播放任意免费集约 10 秒后返回,进入“我的 → 观看记录”。 + 预期:出现该剧封面/剧名/集号/进度/更新时间;即使该剧不在首页分区也能显示。 + 单条删除先立即消失;断网重试删除时,记录应恢复并提示失败。清空应先弹二次确认。 +5. 在详情或播放页收藏一部剧,进入“我的 → 我的收藏”。 + 预期:卡片来自服务端;点粉色收藏按钮取消后立即消失;断网取消应恢复卡片并提示失败。 +6. 登录后在详情页/播放页点下载入口。 + 预期:成功时提示“下载授权已就绪”;付费、未解锁或配额满时,直接出现后端 `message`。 + 进入“我的 → 下载”,预期策略卡显示剩余配额/授权天数/`expirePolicyText`; + 列表显示后端 `status`、`taskStatus`、`progressPercent`、到期时间; + 仅 Wi-Fi、清晰度、单删、清空可操作。 +7. 退出登录后分别进入观看记录、我的收藏、我的下载。 + 预期:三页都显示登录引导,不显示网络错误或白屏。 + +**证据口径** + +- 以上设备侧步骤尚未执行,因此真机/模拟器验证统一标记「未验证」。 +- 没有用开发机 `curl` 冒充设备可达证据。 +- 本批不包含实际文件下载,因此不能据此验收“离线播放”。 + +--- + +## [已处理] 2026-10-17 | 三库全量拉取 + `main` → `develop` 同步(Trae 执行,无代码改动) + +**用户指令(2026-10-17)**:「先拉最新全量代码,三个库全拉,相关规范已经备齐、接口文档完备、INBOX更新、环境地址清单在docs下21-环境地址清单.md 同步main 分支到开发分支」。 + +### 一、执行结果(实测) + +| 仓库 | 分支 | HEAD | 落后 origin/develop | 领先 | `origin/main` 未并入 | 工作区 | +|---|---|---|---|---|---|---| +| FruitServer | develop | `2d70858` | 0 | 0 | 0 | 干净 | +| ReelShort | develop | `00280ca` | 0 | 0 | 0 | 干净 | +| ReelShortWeb | develop | `3ec9ea6` | 0 | 0 | 0 | 干净 | + +- 三库 `git fetch --all --prune` 后均无新提交,无需 `pull`。 +- **ReelShort** 是唯一有 `main` 未同步的库:`eaf8fb4 style: 全量补充作者标识` 与 develop 改名(`nontonki`→`fruitshorts`)冲突 15 个文件。 + 解法:保留 develop 口径(`package:fruitshorts`)+ 按文件类型补回署名头(`.dart` → `//`,`.yaml` → `#`,`.xml` → ``); + `main` 版复活了 develop 已删除的 5 个文件(`test/catalog/demo_media_test.dart`、`test/catalog/home_feed_category_test.dart`、 + `lib/pages/home/home_feed_sections.dart`、`lib/pages/home/widgets/home_feed_header.dart`、`lib/pages/home/widgets/category_swipe_region.dart`)→ 已 `git rm`; + `lib/l10n/app_localizations*.dart`(生成文件)不加署名头,已还原。 + 门禁:`dart analyze lib test` → `No issues found!`;提交 `00280ca`(作者 `jianghaifeng `),已推 `origin/develop`。 + +### 二、⚠️ 待 PM / 用户确认 + +1. **`docs/21-环境地址清单.md` 在 specs 仓库中不存在**。已核对:specs `docs/`(仅 01~20 + README + plans + 参考规范材料清单 + 代码现状盘点)、三个代码仓 `git ls-files`、specs 全部远端分支(只有 `origin/main`)—— 均无此文件。请确认是尚未推送,还是在别的路径/仓库。 +2. specs 本地仍有 22 笔提交未推送到远端(无写权限,`origin/main` 停在 `c528345`)。若以远端为准,需要决定是否把本地回退到 `origin/main`。 + +### 三、用户裁定(2026-10-17) + +1. **specs 只有 `main`,只需同步** → 已将本地 `specs` 强制对齐到 `origin/main`(`c528345`),本地领先的 22 笔未推送提交已丢弃;此后 specs **只拉取,不提交**。 +2. **specs 是唯一权威** → 规范、接口契约、任务清单一律以 specs 为准;与本仓库文档冲突时以 specs 为准。 +3. **执行前置动作** → Codex 每次开始任务前**必须先读本文件(INBOX)**,按条目顺序执行;新条目由 PM 追加,不覆盖历史。 + +--- + +## [已交接 · 未验证(真机验证改由用户执行)] 2026-10-16 20:30 | K1-verify:用**正确地址**重做真机验证(设备侧取证) + +**用户指令(2026-10-16 20:20)**:「让 Codex 用正确地址重做一次真机验证」。 + +**背景(为什么必须重做)**:你 1-3/1-4 那批的真机验证用的是 +`--dart-define=API_BASE_URL=http://10.157.242.1:8081` —— 该地址是**开发机某块 VPN 网卡(utun)的地址**, +现已不存在,**手机侧没有这块网卡**,所以按它构建、安装到手机上的 App **在真机上必然请求失败** +(用户实测提示「**剧目列表加载失败**」,报错里就显示着这个地址)。 +更关键的是:当时那条"证据"是**在开发机上 `curl` 本机网卡地址拿到 200** —— 本机 curl 本机必然 200, +**不构成真机可达的证据(假阳性)**。PM 已把 specs 里 4 处记录修正并作废该结论(`481d4bb`)。 + +### 一、重做验证(**必须设备侧取证**,见红线第 10 条) + +1. **先取真实地址,不要硬编码**:`ipconfig getifaddr en0` 拿开发机局域网 IP(PM 实测当前 `192.168.31.187`;**换网络会变**)。 +2. **前置条件**:手机与开发机在**同一局域网**;iPhone 已授权「**本地网络**」权限(设置 → 隐私与安全性 → 本地网络)。 +3. 真机跑:`flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://<步骤1得到的IP>:8081` +4. **证据至少一条来自设备侧**(优先级从高到低): + - **首选:加设备侧集成测试**(`integration_test` 依赖 + `integration_test/` 目录), + 用 `flutter test integration_test -d --dart-define=API_BASE_URL=http://:8081` 在**手机进程里** + 真实请求 `/app/home/feed` 与 `/app/dramas/{id}`,断言 `code=0` 且列表非空。 + —— 这是可重复、可入库的最硬证据,也顺带把"设备验证"变成可回归的。 + - 或:App 运行时抓**从手机 IP 到 8081 的已建立连接**(`lsof -nP -iTCP:8081 | grep <手机IP>`), + 并**同时**给出后端侧确实收到该请求的证据。 + - 或:手机侧截图(`device` 截图不支持无线 iPhone —— 那就用设备侧测试或连接证据替代)。 +5. **若都拿不到:如实标「未验证」**,并说明卡在哪一步(同网段?权限未授权?),**不许含糊**。 +6. 顺带确认原故障已消失:首页「剧目列表」能出内容(或由步骤 4 的测试断言覆盖)。 + +### 二、留痕要求(**只写本仓库,别动 specs**) + +- `docs/plans/PROGRESS.md`(本仓库 FruitServer 这份)**追加**逐动作留痕:命令 + 原始输出 + 解读。 +- 本条下写回执,含**用户可照做的验证步骤 + 每步预期结果**,并**明确标注"证据来自设备侧"**。 +- **不要去改 specs 仓库**(它只拉取;那 4 处 PM 已修正,别重复改)。 + +### 三、门禁与 git + +- `dart analyze lib test` 零告警 + `flutter test` 全过(新增的 integration_test 一起跑;若它依赖真机、无法在纯本机跑,就在回执里说明并给出真机执行命令与输出)。 +- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`**;新文件首行 `// Author: fruit.com.cn`。 + +### 四、节奏 + +做完**停下写回执**,不要连做下一批。 + +--- + +## [已处理·待用户验收] 2026-10-16 19:40 | K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整 + +**用户指令(2026-10-16 19:30)**:「现在你要做的事对接客户端接口及 UI 调整,仍然严格按照 specs 执行」→ 转到 **客户端 `ReelShort`(Flutter)**,按 specs 阶段 A 推进。 + +### 一、PM 已勘察的现状(实测,直接用) + +**客户端目前只调了 6 个 C 端接口**(契约里已实现 **44 个**): +``` +/app/home/feed /app/discover/feed +/app/auth/register /app/auth/login /app/auth/refresh /app/auth/logout +``` +- `lib/core/data/remote/` 下**只有** `api_client / api_exception / api_interceptors / auth_api / catalog_api` —— **没有** playback / watch / library / interaction / me 的 api 类。 +- `WatchHistoryRepository` 是 **`SharedPreferencesWatchHistoryRepository`(本地存储)**,`mine_logic` 还在用 **`demoProfileId`** 假数据 —— 观看进度/续看位置**全在本机**,没接 `/app/watch/*`。 +- `EngagementRepository` 是 `SharedPreferences` / `InMemory` —— 点赞/收藏状态**也是本地假数据**。 +- 已有:`dio` 网络层 + 拦截器(A1 ✅)、改名 + 印尼语(✅)、`json_numbers.dart` 兼容字符串 id(J1 ✅)。 + +### 二、本批范围(K1:一条完整看剧闭环) + +> 目标:**从首页点进一部剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏**,全链路走真接口。 + +1. **详情(A3 的一块)**:新增 `catalog_api` 方法对接 `GET /app/dramas/{id}`(含剧集列表;**免费集带已签名播放地址**)。 +2. **播放(A4)**:新增 `lib/core/data/remote/playback_api.dart` + - `GET /app/episodes/{id}/play` → 票据 + 签名地址。**签名地址原样使用,不要拼接/改写**(文档硬要求)。 + - 起播前取票据;票据/签名地址有有效期,**过期要重新取**,不要缓存到失效后还用。 +3. **进度与续看(A5 的一块)**: + - `POST /app/watch/progress`(**每 10s 一次**,切集/退出/切后台各补一次) + - `GET /app/watch/progress?dramaId=`(进入详情/播放时定位续看位置) + - `GET /app/watch/recent?limit=20`("我的"页最近观看) + - **把 `WatchHistoryRepository` 的本地实现换成远程实现**(保留抽象接口,测试注入内存假实现);`mine_logic` **去掉 `demoProfileId`**。 +4. **收藏(A5 的一块)**:`POST /app/favorites/toggle`、`GET /app/favorites`、`DELETE /app/favorites/{dramaId}`;收藏状态**以服务端为准**(不要再用本地 `EngagementRepository` 存收藏)。 +5. **UI 调整**(本批必须一起做): + - 详情页 / 播放页 / "我的"页:**加载中 / 空 / 失败**三态齐备(失败要能重试,不要白屏或假装成功) + - 进度条与**续看定位**、收藏按钮的真实状态与乐观更新回滚 + - 断网/超时的提示文案走 l10n(en / zh / id 三份都要有) + +### 三、口径与约束(照 specs) + +- **ID 是字符串**(J1 刚统一):解析走 `json_numbers.dart`,不要直接 `as int`。**计数类**(`viewCount`/`expiresIn`)也是字符串。 +- `accessType` 等枚举**用后端返回值驱动 UI**(免费/付费/试看),**不要在前端写死**;付费集解锁不在本批(属 B2)。 +- 接口层与业务层分离:**新增 api 类**、controller/logic 只依赖 repository 抽象(沿用现有 `catalog_api` + `catalog_controller` 的分层做法)。 +- **不要动**已对接的首页/发现/登录注册逻辑(除必要的最小接线)。 +- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。 + +### 四、门禁 + +- `dart analyze lib test` **零告警** + `flutter test` **全过**(存量 27 个测试文件不得跑红;新增测试要覆盖:票据过期重取、进度上报节流、续看定位、收藏切换回滚、失败态) +- **真实后端联调**(8081):跑一次端到端并留证据 —— 首页 → 详情 → 播放 → 上报进度 → 重新进入看到续看位置 → 收藏出现在收藏列表 +- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`** + +### 五、节奏 + +先写 `docs/plans/2026-10-16-K1.md`(计划 + 影响面 + 风险)→ 做 → 停下写回执(含**用户可照做的验证步骤 + 每步预期结果**,以及"哪几个页面现在走真数据、哪几个还是假的"的清单)。**不要连做 K2。** + +> **后续批次(预告,不用现在做)**:**K2** = 记录/最近观看页 + 收藏列表页 + 分类/搜索 + 下载授权(A5/A3 剩余);**K3** = 互动(评论/点赞/举报/分享,A6);**K4** = 账号剩余(资料/设备/注销;**Google 登录需 Google OAuth 凭据**,属待你提供的付费/密钥类)。 + +--- + +## [已处理 · 待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一 + +> **PM 独立复核(Trae 2026-10-16 19:05)—— 交付 `FruitServer f35db2c` / `ReelShortWeb 3ec9ea6` / `ReelShort 94ff9f1`(三仓均在 `develop` 且与远端同步),验证通过。** +> - **门禁复跑**:`mvn test` **110 → 115 条全过**(commerce 33 / admin-api **16→21** / app-api 49 / worker 12)+ BUILD SUCCESS ✅ +> - **全局配置落位核实**:`fruitshorts-framework/.../web/JacksonLongToStringConfig.java` 存在(一处生效两端,未改 yml)✅ +> - **端到端真实验证(PM 亲自跑,非采信回执)**:新注册用户雪花 id `2111018722574602241` → +> ① `/admin/app-users/page` 返回 **`"id":"2111018722574602241"`(字符串)**、**无裸数字大 id**、`"total":2` **仍是数字**(符合约定); +> ② 用该 id 调 `/admin/app-users/{id}` → **`code=0`**(修复前此场景即 `200002`); +> ③ **真 node 逐位比对**:`typeof parsed.id === 'string'`、`String(parsed.id)` 与库值**逐位相等 = true**;测试数据已清理(残留 0)。 +> - **按红线第 7 条,最终验收权仍归用户。** +> +> ⚠️ **三处口径待用户点头(PM 意见附后)**: +> 1. **唯一例外 `PageResult.total` 保持数字** —— PM 意见:**接受**(它是条数不是 id,前端分页组件要数字);但需在契约 §0「ID 一律字符串」处**显式写出这个例外**,否则前端会困惑。待 Codex 补一句。 +> 2. **客户端改动比"只改 id 解析"略宽(物理上必须)** —— Codex 查明 Flutter 的 id 本来就是 `toString()` 不会崩,真正会崩的是被全局规则扫到的 **Long 计数**,故只改了 `expiresIn`、`viewCount` 等 3 处 + 新增 `json_numbers.dart`。PM 认可这个判断与最小改动口径。⚠️ **另需用户知晓**:`ReelShort` 原先**只有 `main`**,Codex 新建了 `develop` 并推送;若客户端同事仍在 `main` 上开发,这笔需要带过去。 +> 3. **Codex 自我纠正了一处统计错误** —— 它第一版清理脚本用 `psql ... returning 1 | wc -l` 数行数,数到的是 psql 命令标签(删 0 行也输出 1),那轮"删除 1 行"是假数字;随后用**全库扫描**复核(uid/邮箱/雪花 id 零命中)确认无残留。PM 认可其如实披露。 +> +> 📌 **仍挂着**:禅道里同样**没有 J1 任务**(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),无可同步状态。 +> +> ⚠️ **PM 顺手说明**:为做端到端验证,8080(PID 80375)/ 8081(80383)/ 3005 均为 Codex 重启后的新 jar,接口与页面正常。 **用户裁定(2026-10-16 18:10)**:雪花 id 精度问题 **「按方案 (a) 全局修改,统一契约」**。 @@ -289,3 +2211,158 @@ ReelShort(Flutter)逐文件清单(**只有这些,没夹带别的**) **禅道**:J1 在禅道里同样没有对应任务(项目 2 下仍是 S0/S1/S0-16、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。 **服务现状**:8080/8081 是我刚重建起回的新 jar(自测时在跑);:3005 的 dev server 也在。若被环境回收,按上面「准备」起回来即可。 + +--- + +### 回执(Codex,2026-10-16):K1 完成,停在干净交接点 + +**提交**:`ReelShort d7eff4c`(develop,已推;`main` 未动)。留痕:`docs/plans/PROGRESS.md`;计划:`docs/plans/2026-10-16-K1.md`。 + +**门禁**:`dart analyze lib test` → No issues found!;`flutter test` → **All tests passed!(188 条,存量 27 个测试文件未跑红)**;真实后端联调(8081)见下方原始输出。 + +**先纠正条目里的一处勘察**:客户端**详情接口早已接上**(`CatalogApi.dramaDetail` + `CatalogController.requireDetail`,详情页在用), +票据调用 `CatalogApi.playTicket` 也存在但**没有任何页面调用**。所以本轮真正做的是:把票据接进播放链路、进度从本地换服务端、 +收藏换服务端、补三态 UI。 + +**改了什么(ReelShort,只列这一批)** +新增:`remote/playback_api.dart`、`remote/watch_api.dart`、`remote/library_api.dart`、`models/watch_progress.dart`、 +`repositories/playback_repository.dart`、`repositories/favorite_repository.dart`、`services/favorite_controller.dart`、 +`formatting/error_text.dart`。 +改:`app.dart`(注入三个仓库 + 收藏控制器 + 登录/登出联动)、`remote/api_client.dart`(补 `delete`)、 +`remote/catalog_api.dart` 与 `repositories/catalog_repository.dart`、`services/catalog_controller.dart`(票据职责搬走)、 +`models/play_ticket.dart`(补 dramaId/number/title/previewSeconds)、`repositories/watch_history_repository.dart`(换服务端模型)、 +`pages/{discover,drama_detail,mine,player}/*`、`l10n/app_{en,zh,id}.arb` + 4 个生成文件。 +测试:新增 5 个(含 live 联调),改 8 个(含 fake 与 widget_test 的离线仓库注入)。 + +**怎么改的(要点)** +1. **起播前取票**:`PlaybackRepository.ticketFor()` 带 TTL 缓存(默认 10 分钟 < 后端 15 分钟签名),过期或 `forceRefresh` 时重取; + 回前台发现票据过期会重新取。**签名地址原样交给播放器**(`AppConfig.mediaUrl` 只补 host,path 与 `?e=&s=` 不动,有测试钉住)。 +2. **accessType 驱动 UI**:`COIN_REQUIRED/VIP_REQUIRED` → 锁定提示 + "金币解锁即将开放";`NO_MEDIA` → 暂无片源; + 前端不推断能不能播(契约 §1.4 的硬要求)。付费解锁仍属 B2。 +3. **进度**:服务端每剧一条记录,`POST /app/watch/progress` 节流从 5 秒改成**10 秒**,切集/退出/切后台各补一次,播完带 `finished`; + 续看位置只在"记录里的那一集"生效(其它集从头播);`demoProfileId` 与本地 SharedPreferences 实现**已删除**。 +4. **收藏**:`FavoriteController` 以服务端返回为准,乐观更新 + 失败回滚 + 失败提示;退出登录清空(不跨账号串联)。 +5. **三态**:详情页(加载 / 失败可重试 / 空剧集);播放页(票据加载中 / 失败可重试 / 后端判定不可播); + 我的页(续看读取失败可重试 / 收藏列表加载中·失败·空 / 最近观看加载中·失败·空)。断网与超时文案走 l10n(en/zh/id 各 8 个新键)。 + +**原始联调输出(8081)** +``` +① /app/home/feed → dramaId=5 +② /app/dramas/5 → episodes=3;第一集 id='7' + playUrl=/files/episode/202610/cec39d8e….mp4?e=1792144372&s=8791826a… +③ /app/episodes/7/play → episodeId='7' dramaId='5' accessType=FREE accessible=True +④ POST /app/watch/progress (42s) → positionMs="42000" durationMs="228000" playCount=1 +⑤ GET /app/watch/progress?dramaId=5 → episodeId='7' positionMs='42000' ← 续看位置读得回来 +⑥ GET /app/watch/recent?limit=20 → 条数=1(dramaId='5' episodeId='7') +⑦ POST /app/favorites/toggle → true;GET /app/favorites → 1 条;DELETE → 0 条 +$ flutter test test/integration/live_watch_flow_test.dart → All tests passed! +``` +自测数据已清干净:`app_user` 只剩 PM 那条(id=28)、`watch_progress` 0、`user_favorite` 0。 + +**哪些页面现在走真数据、哪些还是假的** +| 页面/能力 | 数据来源 | 状态 | +| --- | --- | --- | +| 首页 / 发现页 | `/app/home/feed`、`/app/discover/feed` | ✅ 真数据(本批未动) | +| 登录 / 注册 / 刷新 / 登出 | `/app/auth/*` | ✅ 真数据(本批未动) | +| **详情页(剧集列表、可播地址)** | `/app/dramas/{id}` | ✅ 真数据 | +| **详情页"继续观看"与剧集进度条** | `/app/watch/progress?dramaId=` | ✅ 本批接上(原来读本机) | +| **播放页(票据/签名地址/起播判定)** | `/app/episodes/{id}/play` + accessType | ✅ 本批接上 | +| **播放页进度上报(10s 节流)** | `POST /app/watch/progress` | ✅ 本批接上(原来存本机) | +| **我的页:收藏列表 + 收藏按钮** | `/app/favorites*` | ✅ 本批接上(原来存本机) | +| **我的页:最近观看** | `/app/watch/recent` | ✅ 接上;**只渲染能在已加载目录里按 id 匹配到的剧**,匹配不到先不显示(见待确认 2) | +| 我的页:点赞 | 本地 `EngagementController` | ❌ 仍是本地假状态(属 **K3** 互动) | +| 播放页:评论 | 本地 `DramaCommentsSheet` | ❌ 仍是本地(属 **K3**) | +| 播放页:解锁付费集 | — | ❌ 本批不做(属 **B2**,票据已按 `accessType` 提示) | +| 记录页 / 收藏列表页 / 分类搜索 / 下载 | — | ❌ 属 **K2** | + +**用户可照做的验证步骤(每步预期结果)** +准备:后端两个 jar 起着(`admin=200`、`app=200`);`cd ReelShort && flutter run`(模拟器用 +`--dart-define=API_BASE_URL=http://10.0.2.2:8081`;iOS/桌面用默认 127.0.0.1)。 + +1. **匿名可看**:不登录进首页 → 点任一剧 → 详情正常出剧集列表;点某集能起播(免费集)。 + 预期:起播前有一瞬"加载票据",无白屏;付费集显示"第 N 集已锁定 + 金币解锁即将开放"。 +2. **登录后进度**:用「我的 → 登录」注册/登录一个邮箱账号 → 进一部剧 → 看 20~30 秒 → 返回详情。 + 预期:详情页的"继续观看"按钮下方出现进度条与"续看至 mm:ss / mm:ss",剧集行上也有一条进度条。 +3. **跨进程续看(真数据验证)**:杀进程重开 → 进同一部剧 → 直接点"继续观看"。 + 预期:从上次的位置继续播(不是从头)。也可以直接查库: + `psql -c "select drama_id, episode_id, position_ms from watch_progress order by updated_at desc limit 3;"` 预期有该剧记录。 +4. **收藏**:播放页右侧点收藏(或详情页右上角)→ 打开「我的 → Watchlist」。 + 预期:该剧出现在收藏列表;再点一次取消 → 从列表消失。断网时点收藏 → 提示"收藏操作失败,请重试"且图标回到原状态。 +5. **异常路径**:把后端停掉(`lsof -tiTCP:8081 -sTCP:LSLISTEN | xargs kill`)后进详情/播放。 + 预期:详情页给"网络异常,请检查网络后重试"+ 重试按钮;播放页给"此视频暂时无法播放"+ 重试; + 我的页收藏区给失败文案 + 重试;**都不白屏、也不假装成功**。恢复后端后点重试即可继续。 +6. **一套命令验证(不用模拟器)**: + `cd ReelShort && flutter test test/integration/live_watch_flow_test.dart` → 预期 All tests passed(服务没起会 skip)。 + +**待确认 / 如实说明** +1. **票据过期用的是客户端 TTL(10 分钟)**,因为契约里 `play` 响应没有 `expiresAt` 字段。后端若补上过期时间,改成按字段判断即可(一行)。 +2. **`/app/watch/recent` 只返回 dramaId/episodeId/位置,没有剧名封面**,所以"我的页最近观看"目前按 id 去已加载的目录卡片里匹配; + 匹配不到的条目先不显示。**K2 的记录页**建议要么后端补卡片字段,要么做成独立请求(已在计划里记为风险 4)。 +3. **`WatchHistoryRepository` 的接口签名被改了**(服务端是"每剧一条续看记录",没有 profile 概念),连带改了 4 个 logic 与 6 个测试文件—— + 这是接口模型决定的,不是顺手重构;`clampResumePosition` 等本地工具函数保留。 +4. **未登录时**进度与收藏接口一律不发(匿名浏览/试看不受影响),收藏按钮沿用"先引导登录"。 +5. 禅道里仍**没有 K1 任务**(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。 + +--- + +### 回执(Codex,2026-10-16):K1-verify —— **未验证(真机验证由用户执行,Codex 未执行)** + +**结论先说**:按用户 20:45 指令「codex 不用跑测试 我来执行」,本批的**真机验证状态 = 未验证**, +由用户执行后以其结果为准。我**没有**执行任何最终真机验证,也**没有**用任何"开发机 curl"当作设备可达证据。 + +**交付物(ReelShort `develop`,已推)** +- `pubspec.yaml`:dev_dependencies 加 `integration_test`(sdk)—— 唯一的新依赖 +- `integration_test/app_backend_test.dart`(新增,首行 `// Author: fruit.com.cn`):两个设备侧用例 + 1. 在手机进程里请求 `/app/home/feed` 与 `/app/dramas/{id}`,断言列表非空、详情 id 与卡片一致; + 2. 用**真 App**(真网络、真目录控制器)启动,轮询到首页底部标签栏出现 —— 即"剧目列表加载成功", + 也就是用户在真机上遇到的「剧目列表加载失败」是否消失。 +- 该文件**按用户要求保持原样,不再改动**。 + +**当前机器状态(交接前已清理)** +- 我起的 `flutter run` / `xcodebuild` / lsof 轮询进程**已全部终止**(`pgrep -fl "flutter_tools|xcodebuild"` 只剩 IDE 常驻 daemon,非构建)。 +- `build/` 里留着我被叫停那次构建的半成品 → **用户从 `flutter clean && flutter pub get` 起跑即可清掉**。 +- 后端 8080 / 8081 仍在跑(`admin=200` / `app=200`)。 + +**用户可直接复制执行的命令(⚠️ 本项目 iOS 用 SPM,没有 Podfile,不要跑 `pod install`)** +```bash +# 0) 起点:清掉残留构建产物(我被打断的那次留下的) +cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +flutter clean && flutter pub get + +# 1) 取开发机真实局域网地址(不要硬编码;换网络会变) +IP=$(ipconfig getifaddr en0) && echo "API_BASE_URL=http://$IP:8081" + +# 2) 设备侧集成测试(最硬的证据,跑在手机进程里) +flutter test integration_test -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://$IP:8081 + +# 2b) 只想跑网络那一条 +flutter test integration_test/app_backend_test.dart -d 00008101-000875813E42001E \ + --plain-name "should_fetch_home_feed_and_drama_detail_when_device_reaches_backend" \ + --dart-define=API_BASE_URL=http://$IP:8081 + +# 3) 真机手动看 App(可选,验证首页列表与交互) +flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://$IP:8081 +``` +(本次实测 `ipconfig getifaddr en0` = **192.168.31.187**;若换网络请以命令输出为准。) + +**前置条件** +1. 手机与开发机在**同一局域网**(例如都在 192.168.31.x)。 +2. iPhone 已授权「**本地网络**」权限:设置 → 隐私与安全性 → 本地网络 → 打开本项目(App 名 **FruitShorts**)。 + 没授权时 App 请求局域网会失败;`Info.plist` 里 `NSLocalNetworkUsageDescription` 与 + `NSAllowsLocalNetworking` 都已配好(上一批加的),所以系统应当会弹权限询问。 +3. 同一时刻**只跑一个** flutter 构建(并发写同一个 `build/` 会互相破坏产物 —— 这正是本次 + `video_player_avfoundation…bundle: No such file or directory` 的疑似成因)。 + +**设备侧证据看什么(红线第 10 条)** +- 测试输出里必须出现**由手机进程打印**的: + - `[device] API_BASE_URL=http://192.168.31.187:8081`(证明手机上用的是局域网地址,不是开发机环回/VPN 地址) + - `[device] 首页分区=4 剧目=3` / `[device] 详情 id=5 剧集=3`(证明设备真的拿到了数据) + - `[device] 首页剧目列表已渲染:API_BASE_URL=http://192.168.31.187:8081`(证明真 App 首页列表出内容) + - 最后一行 `All tests passed!` +- 若失败:`reason` 里会带上 `API_BASE_URL=…`,据此区分"地址不对"还是"设备连不上/权限未授权"。 + +**如实说明(失败与历史记录,详见 PROGRESS 的 K1-verify 段)** +- 我在 17:56 跑过一次,**那次是设备侧输出且全过**(上面那几行 `[device]` 就是它的原始输出)。 +- 随后两次执行失败:17:59 那次 `The Dart VM Service was not discovered after 60 seconds`(app 没 attach,我也没抓到任何手机→8081 的连接); + 18:03 那次 `flutter run` 构建完成后 PM 报产物缺失(`video_player_avfoundation…bundle: No such file or directory`)并叫停。 +- 因此**在用户重跑之前,本批一律按「未验证」计**;上面那次的历史输出只作参考,不作为结论。 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 29ad3db..173c106 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -358,3 +358,3401 @@ DELETE 1 # 创作者 J1PROBE ⚠️ 口径纠正:我第一版清理脚本用 `psql -t -A -c "delete … returning 1" | wc -l` 数"行数", **数到的其实是 psql 的命令标签**(实测删 0 行时输出 `DELETE 0`,也是 1 行),所以那轮打印的 "删除 1 行"是假数字。真正的证据是后面的**全库残留扫描**(零命中)与现存数据核对,已按此结论写回执。 + +--- + +## 2026-10-16 K1:客户端对接(详情 + 播放 + 进度续看 + 收藏)+ 三态 UI + +计划文件:`docs/plans/2026-10-16-K1.md`(先写计划再动手)。仓库:`ReelShort`(Flutter,develop)。 + +### 动作 1:勘察更正 +条目说"客户端只调了 6 个接口、没有详情和票据"——实测**详情已接**(`CatalogApi.dramaDetail`), +**票据半接**(`CatalogApi.playTicket` 存在但没页面调用)。这一点写进计划与回执。 + +### 动作 2:分层实现(api → repository → logic → view) +- 新增 `playback_api.dart` / `watch_api.dart` / `library_api.dart` + `watch_progress.dart` 模型 +- 新增 `PlaybackRepository`(票据 TTL 缓存,默认 10 分钟;时钟/TTL 可注入)、`FavoriteRepository` +- `WatchHistoryRepository` 换成服务端模型(每剧一条续看记录),删掉 `demoProfileId` 与 SharedPreferences 实现 +- `FavoriteController` 乐观更新 + 失败回滚;`app.dart` 注入三个仓库与收藏控制器 + +### 动作 3:门禁(最终数字) + +``` +$ dart analyze lib test +No issues found! +$ flutter test +00:09 +188: All tests passed! +``` + +新增测试:`playback_ticket_expiry_test`(票据过期重取 + 签名 query 原样保留)、 +`progress_throttle_test`(10 秒节流 + 切集补报 + 播完 finished + 续看定位)、 +`watch_history_test`(重写为服务端模型)、`library/favorite_controller_test`(回滚/错误态/退出清空)、 +`integration/live_watch_flow_test`(真实后端端到端)。 + +### 动作 4:真实后端联调(8081,原始输出) + +``` +① 首页 → dramaId=5 +② 详情 → episodes=3,第一集 id='7' playUrl=/files/episode/202610/cec39d8e….mp4?e=1792144372&s=8791826a… +③ 票据 → episodeId='7' dramaId='5' accessType=FREE accessible=True + playUrl 与详情里的签名地址一致(客户端只补 host,不动 path/query) +④ 上报进度(42s)→ {"dramaId":"5","episodeId":"7","positionMs":"42000","durationMs":"228000","playCount":1} +⑤ 读回续看位置 → dramaId='5' episodeId='7' positionMs='42000' durationMs='228000' +⑥ 最近观看 → 条数=1,dramaId='5' episodeId='7' positionMs='42000' +⑦ 收藏 → toggle 返回 true → 收藏列表条数=1(首个 '5')→ DELETE → 取消后条数=0 +``` + +`flutter test test/integration/live_watch_flow_test.dart` → All tests passed(同一链路在客户端代码里跑通)。 +解读:`positionMs/durationMs/dramaId` 都是**字符串**(J1 的新口径),客户端用 `jsonToInt` 解析。 + +### 动作 5:自测数据清理 + +``` +已清理 2 个自测账号(k1.selftest.* / k1.receipt.*)+ 依赖行 +残留核对:app_user 1 行(PM 的 id=28)、app_user_credential 1 行、watch_progress 0、user_favorite 0 +``` +(第一次清理漏了 `app_user` 本体——它的主键列叫 `id` 不是 `user_id`,随后单独删掉并复核。) + +--- + +## 2026-10-16 K1-verify:用正确地址重做真机验证(设备侧取证)—— 结果:**未验证(已按用户要求交回用户执行)** + +红线第 10 条新增后,本轮目标是"用正确地址 + 设备侧证据"重做真机验证。以下是逐动作留痕。 + +### 动作 1:取真实地址(不硬编码) +``` +$ ipconfig getifaddr en0 +192.168.31.187 +$ for i in en0 en1 en2 utun0..3; do ipconfig getifaddr $i; done +en0 = 192.168.31.187 # 只有 en0 有地址;旧的 10.157.242.1 已不存在 +$ lsof -nP -iTCP:8081 -sTCP:LISTEN +java 80383 ... TCP *:8081 (LISTEN) +``` + +### 动作 2:加设备侧集成测试(可重复、可入库的证据) +- `pubspec.yaml` dev_dependencies 加 `integration_test`(sdk) +- 新增 `integration_test/app_backend_test.dart`:在**手机进程里**请求 `/app/home/feed` 与 `/app/dramas/{id}`, + 断言列表非空、详情 id 一致;再用真 App 跑一遍,断言首页底部标签栏出现(= 剧目列表加载成功)。 + 两个用例都把 `API_BASE_URL` 打进设备日志,便于区分"地址不对"还是"设备连不上"。 +- `dart analyze lib test integration_test` → No issues found! + +### 动作 3:第一次真机执行(**设备侧输出,17:56**) +``` +$ IP=$(ipconfig getifaddr en0) && flutter test integration_test -d 00008101-000875813E42001E \ + --dart-define=API_BASE_URL=http://$IP:8081 +Running Xcode build... Xcode build done. 31.3s +Installing and launching... 20.0s +00:00 +0: should_fetch_home_feed_and_drama_detail_when_device_reaches_backend +[device] API_BASE_URL=http://192.168.31.187:8081 +[device] 首页分区=4 剧目=3 +[device] 详情 id=5 剧集=3 +00:01 +1: should_render_home_drama_list_when_app_boots_on_device +[device] 首页剧目列表已渲染:API_BASE_URL=http://192.168.31.187:8081 +00:05 +2: (tearDownAll) +00:06 +2: All tests passed! +``` +解读:`[device]` 行是**手机进程里**打印的(不是开发机 curl),断言也在手机进程里跑;这一次是全过的。 + +### 动作 4:第二次执行(只跑第一个用例,17:59)—— 失败 +``` +Running Xcode build... Xcode build done. 17.8s +Installing and launching... +The Dart VM Service was not discovered after 60 seconds. This is taking much longer than expected... +``` +我同时起了 lsof 轮询想抓"手机 IP → 8081"的已建立连接,**一个连接都没抓到**(原始文件只有 lsof 表头)—— +因为 app 那次根本没 attach 上,也就没有发请求。这一步没有产出任何设备侧证据。 + +### 动作 5:第三次(`flutter run` 真机跑,18:03–18:04)—— 被叫停 +``` +Launching lib/main.dart on iPhone in debug mode... +Running Xcode build... Xcode build done. 30.4s +Installing and launching... +You may be prompted to give access to control Xcode... +``` +PM 报告:该轮后续撞上 iOS 构建报错 +`build/ios/iphoneos/video_player_avfoundation_video_player_avfoundation.bundle: No such file or directory`, +并把进程停掉、执行了 `flutter clean`(我自己的日志到 "Installing and launching" 为止,上面这条报错文本来自 PM 报告,不是我抓到的)。 +**推测成因(如实写,未定论)**:我这轮 flutter run 与另一次执行**并发写同一个 build/ 目录**, +产物被互相破坏(PM 也把"并发写 build 目录"列为成因之一)。 + +### 动作 6:按用户指令停止执行,转为交接 +用户 20:45 指令:「codex 不用跑测试 我来执行」。我随即: +``` +$ kill -TERM 76461 76458 76675 77472 76250 # 我的 flutter run / xcodebuild / frontend_server / lsof 轮询 +$ pgrep -fl "flutter_tools|xcodebuild|k1v_poller" +4173 ... flutter_tools.snapshot daemon # 只剩 IDE 常驻 daemon(非构建) +``` +`build/` 目录里留着我被 kill 的那次构建的半成品,用户按 `flutter clean && flutter pub get` 起跑即可清掉。 + +### 结论口径 +- 按**红线第 10 条**与用户要求:本批真机验证**标注「未验证」**,由用户执行后以用户的结果为准。 +- 动作 3 那次是**真实的设备侧输出且全过**,但随后两次执行失败(一次 VM service 未 attach、一次构建产物缺失), + 所以在用户重跑之前**不把它当作最终结论**;它作为历史记录保留在这里。 +- 我**没有**用任何"开发机 curl"作为设备可达证据。 + +--- + +## 2026-10-17 K2:分类 + 观看记录 + 收藏列表 + 下载授权管理 + +### 动作 1:按 INBOX 指定顺序读取规范与契约 + +叙述:开工前先完整核对 INBOX K2 条目、项目约束、Git 流程、接口 §0/§1.3/§1.5/§1.6, +再读 A3/A5、CONTENT/RECORD 用例与测试计划。当前没有启动客户端构建,也没有改业务代码。 + +``` +$ sed -n '1,260p' docs/plans/INBOX.md +# INBOX — 给 Codex 的指令队列(FruitServer 仓库内) +## [待处理] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI +本批只做 K2,做完停下写回执,不要连做 K3/K4。 +接口只认 FruitServer/docs/api-phase1.md §1.3 / §1.5 / §1.6 + +$ sed -n '1,260p' /Users/jianghaifeng/Desktop/ReelShort/AGENTS.md +# 水果短剧(FruitShorts)— Codex 项目上下文 +## 强制约束(不可违反) +## 自主执行原则(Autonomy — 最高优先级) + +$ sed -n '1,340p' /Users/jianghaifeng/Desktop/ReelShort/CODING_STANDARDS.md +## 3. Dart / Flutter(ReelShort) +TDD 强制:先写测试(红灯)→ 实现(绿灯)→ 重构 +门禁:dart analyze lib test 零告警 + flutter test 全过 + +$ sed -n '1,300p' /Users/jianghaifeng/Desktop/ReelShort/GIT_WORKFLOW.md +## 一、分支模型(Git Flow) +feature/*:具体功能开发,来源 develop +合并一律 --no-ff(禁用 fast-forward) + +$ sed -n '1,64p;150,158p;186,222p' docs/api-phase1.md +## 0. 通用约定 +所有 ID 字段(主键与外键)在 JSON 里都是字符串 +### 1.3 内容浏览(已实现) +GET /app/categories | 分类列表 +### 1.5 观看记录与收藏(已实现) +GET /app/watch/recent?limit=20 | 最近观看 +DELETE /app/watch/progress/{dramaId} | 删除某剧记录 +DELETE /app/watch/progress | 清空记录 +### 1.6 离线下载(已实现) +GET /app/downloads/policy +PUT /app/downloads/settings +POST /app/downloads +GET /app/downloads +GET /app/downloads/{episodeId}/url +POST /app/downloads/{episodeId}/progress +DELETE /app/downloads/{episodeId} +DELETE /app/downloads + +$ sed -n '1,90p' /Users/jianghaifeng/Desktop/ReelShort/docs/plans/codex-研发任务清单.md +- A3 内容(03):首页 feed、发现 feed、分类、详情(一次请求渲染详情页)、搜索。 +- A5 记录/收藏(05):观看记录、最近观看、收藏切换与列表、下载授权。 + +$ sed -n '1,135p' /Users/jianghaifeng/Desktop/ReelShort/docs/16-测试用例库.md +TC-CONTENT-09 | 分类题材 | 有分类 | 查看分类 | 分类列表正确 | P2 +TC-RECORD-01 | 观看历史 | 有播放 | 查看历史 | 按时间倒序,可删除 | P0 +TC-RECORD-03 | 取消收藏 | 已收藏 | 取消 | 状态反转 | P0 +TC-RECORD-04 | 收藏列表 | 有收藏 | 查看 | 分页正确 | P1 +TC-RECORD-09 | 历史清空 | 有历史 | 清空 | 全部清空 | P2 + +$ sed -n '1,100p' /Users/jianghaifeng/Desktop/ReelShort/docs/09-测试计划.md +单元测试 | Dart 核心类 | flutter test | 核心模块 ≥80% +UI 测试 | 关键页面 | Flutter widget test | 核心页面 +``` + +解读:K2 范围、TDD 顺序、接口白名单、三语言文案、三态、登录闸、门禁与设备验证边界均已确认。 +搜索没有契约接口,本批不做;收藏分页和观看记录卡片字段按 INBOX 的降级方案处理。 + +### 动作 2:只读核对两个仓库基线与客户端现状 + +叙述:确认客户端从干净 `develop` 开工,并核对现有网络层、记录/收藏实现和页面入口, +用于计划中的真实文件清单。 + +``` +$ git status --short --branch && git log -5 --oneline --decorate +## develop...origin/develop +00280ca (HEAD -> develop, origin/develop) merge: 同步 main 的作者标识提交到 develop +84a1f58 test(client): 新增设备侧集成测试(真机跑通后端可达性) +d7eff4c feat(client): 对接详情/播放/进度续看/收藏(K1 看剧闭环)+ 三态 UI + +$ git status --short --branch && git log -5 --oneline --decorate # FruitServer +## develop...origin/develop +4118efd (HEAD -> develop, origin/develop) docs(inbox): 按 specs 补全 K2 执行规范与门禁 +096e90c docs(inbox): 记录用户裁定——K2 先按现状契约执行,契约缺口后期再补 +582453f docs(inbox): 下发 K2 客户端第二批(分类/记录/收藏/下载授权 + UI) + +$ find lib -maxdepth 4 -type f | sort +lib/core/data/remote/catalog_api.dart +lib/core/data/remote/library_api.dart +lib/core/data/remote/watch_api.dart +lib/pages/discover/discover_view.dart +lib/pages/mine/mine_view.dart +... +``` + +解读:两仓均无未提交改动。分类 API、下载 API/page/controller 不存在; +观看记录删除只有单条,没有清空;收藏数据层已有但无独立页面; +`ApiClient` 只有 GET/POST/DELETE,下载设置所需的 PUT 需最小补齐。 + +### 动作 3:写 K2 计划 + +叙述:先落计划再动业务代码,明确四块范围、实施顺序、影响文件、三个契约缺口、 +风险和待确认项。 + +``` +文件:docs/plans/2026-10-17-K2.md +章节:范围与完成口径 / 实施顺序与 TDD / 预计影响文件 / + 契约缺口 / 风险与控制 / 待确认 / 完成定义 +``` + +解读:后续实现按 K2-A → K2-B → K2-C → K2-D 推进;每块先红后绿并单独跑门禁。 +真机与 Android 构建结果按实际证据记录,缺失时明确标“未验证”。 + +### 动作 4:创建功能分支并跑存量基线门禁 + +叙述:客户端从 `develop` 创建任务指定分支;沙箱不允许写 `.git/index.lock`, +Git 与 Flutter 工具链缓存写操作按权限流程在受控环境执行。先确认存量门禁本身是绿的。 + +``` +$ git checkout -b feature/flutter-k2-library develop +Switched to a new branch 'feature/flutter-k2-library' +## feature/flutter-k2-library + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:10 +188: All tests passed! +``` + +解读:客户端基线 188 个测试全过、静态检查零告警,可以开始 K2-A。 + +### 动作 5:K2-A 测试红灯 + +叙述:先新增分类模型/API/控制器的契约测试,覆盖雪花 ID 字符串、分类到 +`category-{id}` 分区映射、缺失分区空态、hot/new 入口、全量去重和失败重试。 + +``` +$ flutter test test/category/category_controller_test.dart +test/category/category_controller_test.dart:6:8: Error: Error when reading + 'lib/core/data/models/content_category.dart': No such file or directory +test/category/category_controller_test.dart:10:8: Error: Error when reading + 'lib/core/data/remote/category_api.dart': No such file or directory +test/category/category_controller_test.dart:11:8: Error: Error when reading + 'lib/core/data/repositories/category_repository.dart': No such file or directory +test/category/category_controller_test.dart:12:8: Error: Error when reading + 'lib/core/data/services/category_controller.dart': No such file or directory +00:00 +0 -1: Some tests failed. +``` + +解读:红灯原因符合预期,目标实现尚不存在。下一步实现分类数据层与控制器。 + +### 动作 6:K2-A 实现绿灯与门禁 + +叙述:实现 `ContentCategory`、`CategoryApi`、`CategoryRepository`、 +`CategoryController`,发现页改为读取 `/app/categories` 与 `/app/discover/feed`; +保留 For You / Hot / New 样式,服务端分类只匹配 `category-{id}`。 + +``` +$ flutter test test/category/category_controller_test.dart +00:00 +5: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:08 +191 ~1: All tests passed! +``` + +解读:分类块绿。`~1` 是既有真实后端联调用例因本机 8081 未启动而按设计跳过, +不是失败。分类 UI 仍保留 hot/new 入口;后端发现 feed 未下发对应分区时显示空态。 + +### 动作 7:K2-B 测试红灯 + +叙述:先固定观看记录页核心逻辑:`updatedAt` 解析、recent 按 dramaId 去重、 +详情并发上限 4、单条详情失败降级与单项重试、删除/清空乐观回滚、recent 失败态。 + +``` +$ flutter test test/watch/watch_records_controller_test.dart +test/watch/watch_records_controller_test.dart:11:8: Error: Error when reading + 'lib/core/data/services/watch_records_controller.dart': No such file or directory +test/watch/watch_records_controller_test.dart:23:3: Error: No named parameter + with the name 'updatedAt'. +test/watch/watch_records_controller_test.dart:49:21: Error: The getter + 'updatedAt' isn't defined for the type 'WatchProgress'. +00:00 +0 -1: Some tests failed. +``` + +解读:红灯符合预期,观看记录控制器与更新时间字段尚不存在。 + +### 动作 8:K2-B 实现与门禁 + +叙述:增加 `updatedAt`、观看记录清空 API/repository,新增独立 +`WatchRecordsController` 与 `WatchRecordsPage`。“我的 → 观看记录”已改为打开独立页。 +详情请求按 dramaId 去重、最多 4 个并发;单条详情失败显示占位与单项重试; +单删和清空先更新 UI,失败回滚并提示;清空有二次确认。 + +``` +$ flutter test test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart +00:00 +10: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:08 +201 ~1: All tests passed! +``` + +解读:K2-B 绿。`~1` 仍是既有真实后端联调用例跳过;本轮新增 10 个记录相关测试。 + +### 动作 9:K2-C 测试红灯 + +叙述:先补收藏取消失败的原位回滚测试,再新增独立收藏页的卡片、取消失败、 +空态、失败重试与未登录闸测试。 + +``` +$ flutter test test/library/favorite_controller_test.dart test/library/favorites_page_test.dart +test/library/favorite_controller_test.dart:80:8: Error: The setter 'failRemove' + isn't defined for the type 'InMemoryFavoriteRepository'. +test/library/favorites_page_test.dart:13:8: Error: Error when reading + 'lib/pages/favorites/favorites_page.dart': No such file or directory +test/library/favorites_page_test.dart:108:23: Error: The getter 'listCalls' + isn't defined for the type 'InMemoryFavoriteRepository'. +00:00 +0 -2: Some tests failed. +``` + +解读:红灯符合预期:独立收藏页不存在,测试仓库也还不能模拟删除失败。 + +### 动作 10:K2-C 实现与门禁 + +叙述:新增独立 `FavoritesPage` 和复用列表卡片,服务端列表三态齐全; +取消收藏采用乐观更新,失败时按原索引回滚;我的页增加“我的收藏”入口。 + +``` +$ flutter test test/library/favorite_controller_test.dart test/library/favorites_page_test.dart +00:00 +13: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:10 +207 ~1: All tests passed! +``` + +解读:K2-C 绿。收藏页按契约全量读取,不发送 `page/size`;真实后端联调用例仍按设计跳过。 + +### 动作 11:K2-D 测试红灯 + +叙述:先用 API 契约测试钉住 §1.6 全部 8 个下载接口与字符串 ID; +再用控制器测试固定策略/授权加载、后端 message 透传、设置回滚、删除和清空回滚。 + +``` +$ flutter test test/download/download_api_test.dart test/download/download_controller_test.dart +test/download/download_api_test.dart:7:8: Error: Error when reading + 'lib/core/data/remote/download_api.dart': No such file or directory +test/download/download_controller_test.dart:4:8: Error: Error when reading + 'lib/core/data/models/download_license.dart': No such file or directory +test/download/download_controller_test.dart:9:31: Error: Type + 'DownloadController' not found. +00:00 +0 -2: Some tests failed. +``` + +解读:红灯符合预期,下载模型、API、仓库与控制器尚未实现。 + +### 动作 12:K2-D 数据层与页面实现 + +叙述:实现下载策略/授权模型、8 个接口的 `DownloadApi`、仓库与控制器; +`ApiClient` 仅补契约需要的 `PUT`。新增“我的下载”页,展示策略、配额、 +授权/任务原始状态、进度、有效期,并提供仅 Wi-Fi、清晰度、单删和清空; +详情页与播放页接入真实授权申请。授权失败直接展示后端 `message`。 + +``` +$ flutter test test/download/download_api_test.dart test/download/download_controller_test.dart test/download/downloads_page_test.dart +00:00 +10: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:10 +219 ~1: All tests passed! +``` + +解读:K2-D 首轮绿。文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验未做, +`url` 与 `progress` 只交付 API 方法;这些边界不会写成“已完成”。 + +### 动作 13:提交前边界审查(清晰度下拉修复) + +叙述:审查发现当后端策略 `maxQuality` 已是 `720P/1080P` 时, +下拉选项会重复同一值并触发 Flutter 断言。先补测试得到红灯,再去重选项; +同时处理退出后重新登录页面不刷新、登录闸文案覆盖观看记录/下载的问题。 + +``` +$ flutter test test/download/downloads_page_test.dart +Failed assertion: There should be exactly one item with +[DropdownButton]'s value: 720P. +Either zero or 2 or more [DropdownMenuItem]s were detected with the same value +00:01 +6 -1: Some tests failed. + +$ flutter test test/download/downloads_page_test.dart +00:00 +7: All tests passed! +``` + +解读:边界缺陷已由测试固定并修复,避免特定后端清晰度值导致下载页崩溃。 + +### 动作 14:最终门禁与 Android 构建 + +叙述:整理掉格式化造成的无关测试 diff 后,在 develop 合并前后都复跑门禁。 +首次 Android 构建因本机 Gradle 依赖经代理下载持续约 12 分钟,但最终成功; +修复后二次构建命中缓存(2.377 秒)。 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:11 +220 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 705.7s +✓ Built build/app/outputs/flutter-apk/app-debug.apk + +$ flutter build apk --debug # 最终修复后 +Running Gradle task 'assembleDebug'... 2,377ms +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:静态检查零告警、220 个测试通过;`~1` 是既有真实后端联调用例因本机未启动 +8081 服务按设计跳过。Android debug APK 构建通过。 + +### 动作 15:Git 提交、合并与推送 + +叙述:客户端从干净 develop 建功能分支,功能提交与 `--no-ff` 合并提交均使用 +项目规定作者。合并前确认 `origin/develop` 无新提交;合并后在 develop 复跑门禁, +再 push,最后删除已完成 feature 分支。未改 main。 + +``` +commit=cc4d7c5a9dc6a4f6f0565a32503cce5973af703c +author=Zhou Weidong <502333560@qq.com> +subject=feat(flutter): 接入分类记录收藏与下载授权 + +merge=45f5f25640a274edb51bd90b3b5e52c43ba9d9cd +author=Zhou Weidong <502333560@qq.com> +subject=merge: 合并 K2 客户端分类记录收藏与下载授权 + +$ git push origin develop +To http://git.reelshort2.com/jianghaifeng/ReelShort.git + 00280ca..45f5f25 develop -> develop + +$ git branch -d feature/flutter-k2-library +Deleted branch feature/flutter-k2-library (was cc4d7c5). +``` + +解读:客户端 develop 已与 origin/develop 对齐,工作区干净。 + +### 动作 16:真实数据与未验证边界 + +叙述:生产环境注入的发现分类、观看记录、收藏、下载授权均调用契约 HTTP 接口, +不再用本地演示目录补齐。由于设备侧验证归用户执行,本轮没有把任何开发机请求 +当作“设备可达”证据。 + +``` +已接生产 HTTP 链路: +- GET /app/categories + GET /app/discover/feed +- GET /app/watch/recent + DELETE /app/watch/progress/{dramaId} + DELETE /app/watch/progress +- GET /app/favorites + DELETE /app/favorites/{dramaId} + POST /app/favorites/toggle +- §1.6 全部 8 个 /app/downloads* 契约接口 + +仍未做: +- 搜索(契约无接口) +- 下载文件分片落盘 / 断点续传 / 离线播放 / licenseKey 本地校验 +- 真机 / 模拟器侧验证(未验证,由用户执行) +``` + +解读:功能代码和构建门禁已完成;设备网络、实际授权状态与 UI 交互需用户按回执步骤验证。 + +### 动作 17:用户真机验收清单 + +叙述:本动作只补齐验收证据与回执缺项,不新做功能、不改 ReelShort 产品代码。 +本轮重新核对 ReelShort `develop` 为 `45f5f25`,FruitServer 文档开工前为 `698bb78`; +并复跑 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 +设备侧取证由用户执行,Codex 未执行。下面所有“预期”都是用户在手机上应看到的业务结果; +开发机 `curl` 只用于检查后端是否启动,不作为手机可达或设备侧验收证据。 + +#### 17.1 本轮门禁复跑原始输出 + +叙述:先确认 8081 监听状态,再按 ReelShort `develop` 顺序复跑三个客户端门禁。 +Flutter 首次在受限沙箱内运行时被拒绝写 SDK 缓存 +(`engine.stamp.tmp` / `engine.realm: Operation not permitted`), +随后在受控环境用同一组命令复跑;下面贴的是复跑成功的原始输出。 + +``` +$ lsof -nP -iTCP:8081 -sTCP:LISTEN +(无输出,说明本轮复跑时 8081 未监听) + +$ flutter devices +Found 3 connected devices: + iPhone (mobile) • 00008101-000875813E42001E • ios • iOS 26.6.1 23G83 + macOS (desktop) • macos • darwin-arm64 • macOS 15.5 24F74 darwin-arm64 + Chrome (web) • chrome • web-javascript • Google Chrome 155.0.8059.80 + +Checking for wireless devices... + +No wireless devices were found. + +Run "flutter emulators" to list and start any available device emulators. +``` + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +test/integration/live_watch_flow_test.dart: should_complete_the_watch_flow_when_backend_is_running + 客户端接口服务未启动,已跳过联调测试 +test/integration/live_auth_test.dart: should_surface_business_error_when_sign_in_uses_unknown_email + 客户端接口服务未启动,已跳过联调测试 +test/integration/live_catalog_test.dart: should_parse_banner_and_sections_when_home_feed_is_fetched + 客户端接口服务未启动,已跳过联调测试 +test/integration/live_catalog_test.dart: should_order_episodes_by_number_and_attach_url_when_detail_is_fetched + 客户端接口服务未启动,已跳过联调测试 +test/integration/live_catalog_test.dart: should_return_play_url_when_episode_is_playable + 客户端接口服务未启动,已跳过联调测试 +00:10 +216 ~5: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 2,672ms +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:静态检查零告警;8081 未启动时测试结果是 `+216 ~5`,5 条跳过均来自 live 联调用例的 +`markTestSkipped`,不是失败;Android debug APK 构建成功(缓存命中,`2,672ms`)。 +本轮没有设备侧业务证据,也没有用开发机请求替代手机取证。 + +#### 17.2 从零前置条件 + +以下命令都需要在 Mac 终端执行。第 1 步只在首次搭建环境时执行;已有数据库可跳过。 + +1. 启动 PostgreSQL;首次环境再建角色、数据库并初始化脚本。 + +``` +$ brew services start postgresql@18 +$ export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH" +$ psql -d postgres -c "CREATE ROLE fruitshorts LOGIN PASSWORD 'fruitshorts';" +$ psql -d postgres -c "CREATE DATABASE fruitshorts OWNER fruitshorts ENCODING 'UTF8';" +$ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer +$ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema.sql +$ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data.sql +$ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema-phase1.sql +$ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/partition-maintenance.sql +$ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data-phase1.sql +``` + +预期:五个 SQL 脚本按顺序执行完成,没有主键冲突或表不存在错误。 +重复执行是幂等的;不要颠倒 `partition-maintenance.sql` 与 `data-phase1.sql` 的顺序。 + +2. 构建并启动客户端接口服务 8081。 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer +$ mvn -pl fruitshorts-app-api -am package -DskipTests +$ java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar +``` + +预期:启动日志出现 `Started AppApiApplication`;另开终端检查: + +``` +$ lsof -nP -iTCP:8081 -sTCP:LISTEN +java ... TCP *:8081 (LISTEN) +$ curl -fsS http://127.0.0.1:8081/healthz +{"code":0,...,"data":{"status":"UP","app":"fruitshorts-app-api",...}} +``` + +`curl` 只证明开发机上的 8081 已启动,不能证明手机可达。 + +3. 准备手机与局域网。 + +- 手机和 Mac 连接同一个 Wi-Fi;关闭会隔离客户端的访客网络/VPN 代理。 +- iPhone 首次启动 App 时,在系统弹窗中允许「本地网络」权限。 +- Mac 防火墙若拦截 Java,到「系统设置 → 网络 → 防火墙 → 选项」允许 `java` 传入连接。 +- 同网段地址用 `IP=$(ipconfig getifaddr en0)` 获取。若为空,先确认当前 Wi-Fi 网卡名, + 再改用对应接口(如 `en1`)。 + +预期:终端打印形如 `192.168.x.x` 的局域网地址;手机浏览器能访问 +`http://<该地址>:8081/healthz`,页面显示 `"status":"UP"`。 + +4. 获取 Flutter 运行设备号。 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter devices +``` + +预期:真机以 `iPhone (mobile) • • ios` 形式出现。本轮机器识别到的设备号是 +`00008101-000875813E42001E`;换手机或重装后必须重新执行 `flutter devices`, +不能继续照抄旧 UDID。也可以执行 `xcrun xctrace list devices` 查 iPhone UDID。 + +这里的“运行设备号”用于 `flutter run -d`。下载绑定使用的设备号是另一个概念: +客户端首次运行会在本地生成并持久化,随后由 `ApiClient` 作为 `X-Device-Id` 发送, +用户不需要也不能在界面上手填。测试文件里的 `dio-check-device`、`k1-live-device` +只是测试固定值,不是生产 App 的设备号。 + +5. 用当前 Mac 的局域网 IP 注入接口地址并运行 App。 + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort +$ git checkout develop +$ git pull origin develop +$ git rev-parse HEAD +45f5f25640a274edb51bd90b3b5e52c43ba9d9cd + +$ cd ReelShort +$ IP=$(ipconfig getifaddr en0) +$ echo "API_BASE_URL=http://$IP:8081" +$ flutter run -d 00008101-000875813E42001E \ + --dart-define=API_BASE_URL=http://$IP:8081 +``` + +`API_BASE_URL` 是 Dart 编译期 `--dart-define`,不是 `export API_BASE_URL=...` 环境变量; +改了地址必须停止 App 后用新参数重新 `flutter run`,热重载不会替换它。 +真机不能用 `127.0.0.1` 访问 Mac,Android 模拟器才用 `10.0.2.2:8081`。 + +预期:App 启动后首页和发现页能加载封面、标题与分区;手机没有出现网络异常空屏。 +如果页面一直报网络错误,先在手机浏览器验证第 3 步的 `/healthz`,再检查 Mac 防火墙和 +本地网络权限;不要用开发机 `curl` 冒充设备通过。 + +#### 17.3 K2-A 分类验收 + +1. 打开「发现」页。 + +预期:先出现分类/分区加载态,随后分类条显示保留的 `For You`、`Hot`、`New` 入口, +再显示后端 `/app/categories` 返回的分类名称。默认种子数据下包括「都市情感」「重生逆袭」 +「甜宠爱情」「悬疑复仇」;数据库改过名称时以实际后端返回名称为准。 + +2. 依次点「都市情感」「甜宠爱情」。 + +预期:顶部对应分类 chip 高亮;下方只展示 `/app/discover/feed` 中 +`code == "category-{id}"` 的分区卡片。切换分类时内容跟着切换,不会保留上一个分类的卡片。 + +3. 点默认种子中没有已发布内容的「重生逆袭」或「悬疑复仇」。 + +预期:分区区域显示「这个分类暂时没有剧集。」。这是无分区空态,不应出现自行拼装的推荐列表、 +旧分类残留卡片或网络失败文案。 + +4. 切回有内容的分类。 + +预期:卡片恢复显示;封面和标题与后端发现 feed 一致;点卡片可进入对应详情。 + +5. 失败与重试:停止 8081,冷启动 App 后打开「发现」页。 + +预期:分类区域显示「分类加载失败。」和「重试」按钮,不白屏。 +重新启动 8081 后点「重试」,预期分类和分区恢复显示;必要时下拉刷新。 + +设备侧只能直接证明服务端分类名称与分类切换结果;`category-{id}` 的代码映射、 +全量去重和并发/重试细节由 `test/category/category_controller_test.dart` 自动化覆盖。 + +解读:K2-A 的用户验收点是“分类条来自服务端、内容只取对应 `category-{id}` 分区、 +缺失分区显示空态、失败可重试”;真机没有执行这些步骤,当前结论仍为未验证。 + +#### 17.4 K2-B 观看记录验收 + +1. 登录后播放任意一部剧的免费集约 10 秒,返回并进入「我的 → 观看历史」。 + +预期:页面先显示加载态,随后出现该剧卡片;卡片可见封面、剧名、 +「第 N 集」、进度条、`续看至 mm:ss / mm:ss` 和「更新于 ...」。 +即使该剧不在首页/发现已加载目录中,也应正常显示,不能因为目录里匹配不到而消失。 + +2. N+1 合并卡片:对同一部剧先后播放两个不同剧集,再播放另一部剧的一集,重新打开观看历史。 + +预期:同一部剧只出现一张卡片,不因看过多个剧集而重复;卡片展示该剧最新进度和对应集号。 +不同剧各自一张卡片。详情是按 `dramaId` 去重后补取的,单条卡片加载中不应拖住其他卡片。 + +3. 单条删除:点某张卡片右侧的关闭图标。 + +预期:卡片立即从页面消失;返回再进入观看历史后仍然不存在。 + +4. 单删失败回滚:保持页面已加载,停止 8081,再点另一张卡片的关闭图标。 + +预期:卡片先乐观消失,随后回到原位置,并出现「删除记录失败,请重试。」。 +重新启动 8081 后,删除动作可再次执行。 + +5. 清空:点页面右上方「清空」。 + +预期:先弹出「清空观看记录?」及「这会从当前账号移除全部观看记录。」; +点「暂不」列表保持不变;点「清空」后列表变空并显示「还没有观看记录。」。 + +6. 清空失败回滚:有记录且页面已加载时停止 8081,点「清空」并在二次确认中点「清空」。 + +预期:列表先清空,随后恢复原记录,并出现「清空观看记录失败,请重试。」。 + +7. 三态和失败重试:后端正常时进入页面观察加载态;清空后观察空态;停止 8081 后冷启动 App +再进入观看历史,观察「续看进度加载失败。」和「重试」。重启 8081 后点「重试」, +预期记录重新加载。 + +若单条详情请求失败,预期该卡片显示「剧集信息加载失败」与「重试」,其他卡片仍正常显示; +该分支和 4 并发上限由 `test/watch/watch_records_controller_test.dart`、 +`test/watch/watch_records_page_test.dart` 自动化覆盖。真机验收重点看合并卡片和整页不被单条失败拖垮。 + +解读:K2-B 的用户验收点是三态、同一剧 N+1 详情合并为一张卡片、单删与清空的乐观回滚; +自动化测试已覆盖控制器行为,手机上的真实网络与回滚表现仍待用户取证。 + +#### 17.5 K2-C 收藏验收 + +1. 登录后在详情页或播放页点书签图标收藏一部剧。 + +预期:图标立即切换为已收藏状态;失败时图标回到原状态并显示「收藏操作失败,请重试。」。 + +2. 再收藏 1 至 2 部不同的剧,进入「我的 → 我的收藏」。 + +预期:页面展示服务端 `GET /app/favorites` 返回的全部收藏卡片,封面、标题与首页卡片一致; +列表不显示分页控件,也不发送自造的 `page/size`。点击卡片可进入详情。 + +3. 取消收藏:点某张收藏卡片右侧的粉色收藏图标。 + +预期:卡片立即消失;返回再进入我的收藏后仍然不存在。 + +4. 取消收藏失败回滚:保持列表已加载,停止 8081,再点另一张卡片的粉色图标。 + +预期:卡片先乐观消失,随后回到原位置,并出现「收藏操作失败,请重试。」。 +重新启动 8081 后可再次取消。 + +5. 三态和失败重试:后端正常时进入页面观察加载态;取消全部收藏后观察「还没有收藏。」; +停止 8081 后冷启动 App 再进入我的收藏,观察「收藏列表加载失败。」和「重试」。 +重启 8081 后点「重试」,预期列表恢复。 + +6. 服务端状态校准:在另一台设备或另一次登录中取消同一部收藏,再回到当前设备的我的收藏刷新。 + +预期:当前列表以服务端结果为准,不继续显示已被取消的收藏。 + +解读:K2-C 的用户验收点是服务端全量收藏列表、取消收藏后的乐观回滚和三态; +未登录引导另见 17.7,设备侧列表与回滚表现仍待用户取证。 + +#### 17.6 K2-D 下载授权验收 + +1. 登录后进入「我的 → 下载」。 + +预期:先出现加载态,随后显示策略卡: +「剩余 X / Y 个授权」、「授权有效期 N 天」以及后端返回的 `expirePolicyText` 原文; +下面显示「仅 Wi-Fi 下载」开关和「最高清晰度」下拉框。 + +2. 验证 §1.6 的 8 个接口。真机可观察结果如下: + +| # | 接口 | 真机操作与预期可见结果 | +|---|---|---| +| 1 | `GET /app/downloads/policy` | 打开下载页后策略卡出现,配额、授权天数、`expirePolicyText` 来自后端 | +| 2 | `PUT /app/downloads/settings` | 切换仅 Wi-Fi 或清晰度后,页面立即显示新值;离开再进入仍保持 | +| 3 | `POST /app/downloads` | 详情页/播放页点下载图标,出现授权成功或后端拒绝提示 | +| 4 | `GET /app/downloads` | 打开下载页后出现当前设备授权列表;无授权时显示空态 | +| 5 | `GET /app/downloads/{episodeId}/url` | 本批只提供 API 方法,UI 不调用;真机无可观察入口,见自动化测试 | +| 6 | `POST /app/downloads/{episodeId}/progress` | 本批只提供方法,不做文件落盘/进度上报;真机无可观察入口,见自动化测试 | +| 7 | `DELETE /app/downloads/{episodeId}` | 点单条右侧关闭图标后该授权消失,重新进入仍不存在 | +| 8 | `DELETE /app/downloads` | 点「清空」并二次确认后授权列表变空,重新进入仍为空 | + +3. 申请授权成功:进入任意可下载剧集的详情页或播放页,点下载图标(详情页右上角下载按钮, +或播放页右侧下载入口)。 + +预期:服务端接受时出现「下载授权已就绪。」;进入我的下载后能看到对应剧名、集号、 +授权状态、任务状态、进度百分比、清晰度(后端有值时)和到期时间。 + +4. 授予失败透传后端 `message`:选择付费/未解锁剧集,或把当前设备配额用满后再次申请。 + +预期:SnackBar 原样显示后端返回的 `message`,不能替换成前端自造的“付费/未解锁/配额满”判断。 +只有后端没有可用 message 时才回退到「申请下载失败。」。验收时记录手机屏幕上看到的完整原文; +当前环境没有执行这一步,因此具体拒绝文案仍属未验证。 + +5. 仅 Wi-Fi/清晰度成功:切换「仅 Wi-Fi 下载」,再选择 `AUTO`、`720P` 或 `1080P`。 + +预期:开关/下拉值立即变化,没有崩溃或重复选项;退出下载页再进入,值从 +`GET /app/downloads/policy` 重新读取且保持。 + +6. 设置失败回滚:保持下载页已加载,停止 8081,再切换仅 Wi-Fi 或清晰度。 + +预期:值先乐观变化,随后恢复原值,并出现「下载设置更新失败。」。 + +7. 单删和清空。 + +- 点单条右侧关闭图标:预期该授权立即消失,重新进入后仍不存在。 +- 停止 8081 后点另一条关闭图标:预期先消失再恢复,并提示「删除下载授权失败。」。 +- 点「清空」:预期先弹出「清空下载授权?」及「这会吊销当前设备的全部下载授权。」; + 点「暂不」不变,点「清空」后列表为空并显示「还没有下载授权。」。 +- 停止 8081 后清空:预期列表先清空再恢复,并提示「清空下载授权失败。」。 + +8. 状态枚举后端驱动:查看授权卡上「授权:...」「任务:...」后的文本。 + +预期:显示后端原值,如 `ACTIVE/EXPIRED/REVOKED`、 +`QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED`,前端不做中文映射或本地推算。 +现有数据只可能展示实际出现的状态;其余枚举由下载 API/控制器测试覆盖, +设备侧未逐一构造全部后端状态,回执不能写成“所有状态已真机验证”。 + +9. 三态和失败重试:后端正常时进入观察加载态;清空授权后观察空态; +停止 8081 后冷启动 App 再进入下载页,观察「下载设置加载失败。」和「重试」。 +重启 8081 后点「重试」,预期策略卡和授权列表恢复。 + +本批不落盘、不断点续传、不提供离线播放;即使授权状态为 `ACTIVE`,也不能据此验收文件已下载。 + +解读:K2-D 的用户验收点是策略卡、8 个接口的可见结果、后端 `message` 透传、 +仅 Wi-Fi/清晰度、单删/清空以及状态枚举原样展示。URL/进度两个接口没有本批 UI 入口, +只能由自动化测试证明方法契约;真机拒绝文案和全部状态仍未验证。 + +#### 17.7 未登录 AuthGate 与失败重试 + +1. 在「我的」页退出登录。 + +预期:账号回到未登录状态。 + +2. 依次进入「我的 → 观看历史」「我的 → 我的收藏」「我的 → 下载」。 + +预期:三个页面都显示登录引导: +「登录后可继续」、「观看记录、收藏、评论和下载需要登录。」和「去登录」按钮; +不显示网络错误、白屏或空列表冒充无数据。 + +3. 在未登录的详情页点收藏或在详情页/播放页点下载。 + +预期:出现底部登录提示;点「去登录」进入登录页,点「暂不」关闭提示且不发业务请求。 + +4. 完成登录后返回观看历史、我的收藏或下载页。 + +预期:页面自动从登录引导切换到加载态,再展示对应数据;不需要杀 App 才能刷新。 + +5. 失败重试统一检查:先让页面加载成功,再停止 8081 并冷启动 App 进入页面。 + +预期:记录页显示「续看进度加载失败。」、收藏页显示「收藏列表加载失败。」、 +下载页显示「下载设置加载失败。」,均有「重试」按钮;重启 8081 后点「重试」能恢复, +不出现白屏或无提示的假成功。 + +解读:未登录时三个独立页面都应由页面级 AuthGate 接住;登录或后端恢复后应重新加载。 +这些手机交互本轮未执行,不能用开发机请求代替。 + +#### 17.8 测试跳过口径差异与稳定复现命令 + +PM 复核事实与本轮复跑结果一致: + +- 8081 未启动:`flutter test` 呈 `+216 ~5`,5 个 live 用例全部由 + `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` 跳过。 +- 8081 已启动:PM 复核呈 `+220 ~1`,4 个 live 用例通过; + 当前演示数据下 `live_watch_flow_test.dart` 仍由播放断言前的 + `markTestSkipped('这部演示剧没有可播剧集,已跳过播放相关断言')` 保留 1 个跳过。 + 如果换了一套有可播剧集的种子数据,这个用例可能通过并呈 `+221 ~0`;以实际输出为准。 + +5 个 live 用例及跳过机制: + +| 文件 | 用例 | 跳过机制 | +|---|---|---| +| `test/integration/live_auth_test.dart` | `should_surface_business_error_when_sign_in_uses_unknown_email` | 捕获连接失败后 `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` | +| `test/integration/live_catalog_test.dart` | `should_parse_banner_and_sections_when_home_feed_is_fetched` | `serverUp()` 失败后 `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` | +| `test/integration/live_catalog_test.dart` | `should_order_episodes_by_number_and_attach_url_when_detail_is_fetched` | 同上 | +| `test/integration/live_catalog_test.dart` | `should_return_play_url_when_episode_is_playable` | 同上 | +| `test/integration/live_watch_flow_test.dart` | `should_complete_the_watch_flow_when_backend_is_running` | 注册失败后跳过“服务未启动”;注册成功但无可播剧集时跳过“没有可播剧集” | + +稳定复现 `+216 ~5`: + +``` +$ lsof -nP -iTCP:8081 -sTCP:LISTEN +(确认无输出) +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test +00:10 +216 ~5: All tests passed! +``` + +稳定复现 `+220 ~1`: + +``` +# 终端 A:启动 8081 后保持运行 +$ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer +$ mvn -pl fruitshorts-app-api -am package -DskipTests +$ java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar + +# 终端 B:确认 8081 已监听后跑测试 +$ curl -fsS http://127.0.0.1:8081/healthz +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test +00:xx +220 ~1: All tests passed! +``` + +这 3 个 live 测试文件把地址硬编码为 `http://127.0.0.1:8081`,不读取 +`--dart-define=API_BASE_URL`。因此复现测试口径必须在同一台 Mac 上启动 8081, +手机同一局域网只用于真机 App 验收,不能替代这里的环回联调。 +本轮没有改这 3 个测试文件或产品代码。 + +解读:`~5` 与 `~1` 的差异来自 8081 是否启动,不是测试代码漂移; +`~1` 还是当前演示数据下播放断言前的跳过,不能把 `~5` 误报成 5 个失败。 + +#### 17.9 提交作者口径差异登记 + +叙述:已发生的历史提交不改写,不回滚,不 force push。下面用原始 Git 输出登记事实。 + +``` +$ git show -s --format='%H%n author: %an <%ae>%n committer: %cn <%ce>%n subject: %s' cc4d7c5 +cc4d7c5a9dc6a4f6f0565a32503cce5973af703c + author: Zhou Weidong <502333560@qq.com> + committer: Zhou Weidong <502333560@qq.com> + subject: feat(flutter): 接入分类记录收藏与下载授权 + +$ git show -s --format='%H%n author: %an <%ae>%n committer: %cn <%ce>%n subject: %s' 45f5f25 +45f5f25640a274edb51bd90b3b5e52c43ba9d9cd + author: Zhou Weidong <502333560@qq.com> + committer: Zhou Weidong <502333560@qq.com> + subject: merge: 合并 K2 客户端分类记录收藏与下载授权 +``` + +差异口径与最终裁定: + +- specs 规范要求提交作者统一为 `Zhou Weidong <502333560@qq.com>`,上述两个历史提交按此口径产生。 +- 用户曾口头要求使用 `jianghaifeng `;该口径已作废,不再作为后续提交依据。 +- 两个历史提交均已 push 到 `develop`;保留原始历史,不通过回滚或 force push 改写,不动 `main`。 +- 已裁定:后续提交统一使用 `Zhou Weidong <502333560@qq.com>`,author 与 committer 均按此口径。 +- 本回执提交 `cfce77a` 曾误用 `jianghaifeng ` / `trae-solo-agent `,现已通过 amend 修正为 `Zhou Weidong <502333560@qq.com>`;amend 后最终提交号以本轮 `git log -2` 原始输出为准。 + +解读:提交作者差异是已发生事实,已推送的历史提交不通过回滚或 force push 改写; +jianghaifeng 口径作废,后续统一使用 Zhou Weidong,本回执提交已通过 amend 校正。 + +#### 17.10 仍未验证的边界 + +- 设备侧业务取证由用户执行,Codex 未执行;本轮没有手机屏幕截图、设备日志或手机到 8081 的连接证据。 +- 具体后端拒绝 `message` 原文尚未在真机触发并记录。 +- 8 个下载接口中,`GET /app/downloads/{episodeId}/url` 与 + `POST /app/downloads/{episodeId}/progress` 本批没有 UI 调用入口,只有自动化 API 契约测试。 +- `ACTIVE/EXPIRED/REVOKED` 与 `QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED` + 只验证“前端展示后端原值”的机制;没有逐一构造全部状态做真机验收。 +- 文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验不在 K2 范围,仍未实现,也不能验收。 +- 收藏接口没有分页参数,本批按全量列表验收,不验收服务端分页。 +- 搜索契约没有接口,本批不验收搜索。 + +--- + +## 2026-10-17 K3-A 评论实施(【Codex】) + +叙述:按 K3 任务书先补计划文档基线,再按评论 API/controller/UI 顺序做 TDD。 +首轮测试在实现前运行,用于确认红灯;实现后运行同一组定向测试转绿。 + +### 18.1 计划文档 + +命令:使用补丁工具增量更新 `docs/plans/2026-10-17-K3.md`。 + +原始结果: + +``` +Success. Updated the following files: +M /Users/jianghaifeng/Desktop/ReelShort/FruitServer/docs/plans/2026-10-17-K3.md +``` + +解读:保留既有 K3 计划,仅补记 ReelShort `45f5f25`、FruitServer `a0156ee` +和计划文件未跟踪的实测基线,没有覆盖已有内容。 + +### 18.2 K3-A 首次红灯 + +命令: + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test test/comment/comment_api_test.dart test/comment/comment_controller_test.dart +``` + +原始输出(节选): + +``` +Error when reading 'lib/core/data/remote/comment_api.dart': No such file or directory +Error when reading 'lib/core/data/models/comment.dart': No such file or directory +Error when reading 'lib/core/data/services/comment_controller.dart': No such file or directory +00:00 +0 -2: Some tests failed. +``` + +解读:生产 API、模型和 controller 尚不存在,红灯原因符合预期。 + +### 18.3 K3-A 数据层绿灯 + +命令:实现评论模型、API、仓库和 controller 后,重跑同一组定向测试。 + +原始输出: + +``` +00:00 +9: All tests passed! +``` + +解读:评论游标分页、String ID、发布回复、替换内容、敏感词 BLOCK message、 +删除成功与失败回滚均通过。 + +### 18.4 K3-A UI 红灯与绿灯 + +首次运行: + +``` +$ flutter test test/comment/drama_comments_sheet_test.dart +ProviderNotFoundException: Could not find the correct Provider +00:00 +0 -5: Some tests failed. +``` + +解读:旧弹窗仍直接依赖本地 `EngagementController`,符合 UI 红灯预期。 + +替换为服务端 controller、三态和回复输入后: + +``` +00:01 +5: All tests passed! +``` + +随后运行整个评论测试目录: + +``` +$ flutter test test/comment +00:01 +14: All tests passed! +``` + +解读:API、controller 和弹窗测试共 14 项全部通过。 + +### 18.5 K3-A 静态分析 + +首次分析: + +``` +info - lib/core/data/remote/comment_api.dart:23:9 - use_null_aware_elements +info - lib/core/data/remote/comment_api.dart:24:9 - use_null_aware_elements +2 issues found. +``` + +按 lint 改为 null-aware map value 后: + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +解读:K3-A 达到零告警口径。真机交互未执行,设备侧取证仍由用户完成。 + +### 18.6 K3-B 点赞红灯 + +命令: + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test test/interaction test/comment/drama_comments_sheet_test.dart +``` + +原始输出(节选): + +``` +Error when reading 'lib/core/data/services/interaction_controller.dart': No such file or directory +Method not found: 'InteractionController'. +00:01 +8 -1: Some tests failed. +``` + +解读:互动 API 已先通过,缺失的剧集点赞 controller 构成 K3-B 红灯。 + +### 18.7 K3-B 绿灯 + +实现 `InteractionController`、替换播放页右栏、去掉本地互动演示层后: + +``` +$ flutter test test/interaction test/comment/drama_comments_sheet_test.dart +00:01 +11: All tests passed! +``` + +补齐评论点赞成功/回滚和整机 AuthGate 断言后: + +``` +$ flutter test test/comment/comment_controller_test.dart test/widget_test.dart +00:04 +23: All tests passed! +``` + +静态分析: + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +解读:剧集与评论点赞均使用服务端结果校准;失败会回滚;未登录点剧集赞会打开 +AuthGate 且仓库调用次数为 0。剧集点赞数字未展示,符合契约缺口降级方案。 + +### 18.8 K3-C 举报红灯 + +命令: + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test test/interaction/report_dialog_test.dart test/comment/drama_comments_sheet_test.dart test/widget_test.dart +``` + +原始输出(节选): + +``` +Error when reading 'lib/pages/interaction/widgets/report_dialog.dart': No such file or directory +Found 0 widgets with key [<'comment-menu-10'>] +Found 0 widgets with key [<'detail-more'>] +00:05 +21 -5: Some tests failed. +``` + +解读:举报弹窗、评论更多菜单和详情页更多菜单均尚未实现,红灯符合 K3-C 范围。 + +### 18.9 K3-C 绿灯 + +实现固定五项 `SPAM/ABUSE/PORN/AD/OTHER` 单选、可选描述、评论与短剧举报入口后: + +``` +$ flutter test test/interaction/report_dialog_test.dart test/comment/drama_comments_sheet_test.dart test/widget_test.dart +00:05 +26: All tests passed! +``` + +解读:举报原因与描述提交、评论举报成功、重复举报 message 透传、未登录 AuthGate +和详情页举报短剧均通过。 + +### 18.10 K3-C 静态分析 + +首次分析: + +``` +info - lib/pages/drama_detail/drama_detail_view.dart:319:42 - use_build_context_synchronously +info - lib/pages/drama_detail/drama_detail_view.dart:321:26 - use_build_context_synchronously +info - lib/pages/drama_detail/drama_detail_view.dart:327:26 - use_build_context_synchronously +3 issues found. +``` + +改为 State 自身 `context` + `mounted` 守卫后: + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +解读:举报实现达到零告警口径。 + +### 18.11 K3-D 分享回归 + +命令: + +``` +$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort +$ flutter test test/core/platform_share_service_test.dart test/formatting/localized_content_test.dart test/widget_test.dart +``` + +原始输出: + +``` +00:05 +25: All tests passed! +``` + +解读:系统分享文本保持“标题 → 简介 → footer”顺序;en/zh/id 三语均生成 +非空标题、简介和 `FruitShorts` footer;播放页分享入口回归通过。 + +### 18.12 代码规模红线修正 + +提交前检查命令: + +``` +$ wc -l lib/pages/player/widgets/drama_comments_sheet.dart +535 lib/pages/player/widgets/drama_comments_sheet.dart +``` + +解读:主弹窗超过单文件 500 行红线。将评论条目与交互回调拆到 +`comment_tile.dart` 后: + +``` +$ wc -l lib/pages/player/widgets/drama_comments_sheet.dart lib/pages/player/widgets/comment_tile.dart +396 lib/pages/player/widgets/drama_comments_sheet.dart +149 lib/pages/player/widgets/comment_tile.dart +``` + +拆分后复跑互动定向测试: + +``` +00:06 +51: All tests passed! +``` + +解读:行为未回归,两个文件均低于 500 行。 + +### 18.13 最终门禁 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:11 +241 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.8s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:唯一跳过项是既有 live watch 用例,因客户端接口服务未启动而按测试机制跳过。 +真机/设备侧集成、截图、设备日志仍未执行。 + +### 18.14 Git 合并与推送 + +ReelShort 功能提交: + +``` +d361034226ec2bc431548d45068dc60046e5e473 +author: Zhou Weidong <502333560@qq.com> +committer: Zhou Weidong <502333560@qq.com> +subject: feat(flutter): 接入互动评论点赞举报 +``` + +`--no-ff` 合并提交: + +``` +9db6935308e65342be46c81419c2992ed2df1df0 +author: Zhou Weidong <502333560@qq.com> +committer: Zhou Weidong <502333560@qq.com> +subject: merge: 合并 K3 客户端互动能力 +``` + +推送: + +``` +To http://git.reelshort2.com/jianghaifeng/ReelShort.git + 45f5f25..9db6935 develop -> develop +``` + +解读:K3 已合并到 ReelShort `develop` 并推送,未操作 `main`。 + +### 18.15 本人评论举报错误透传补充修复 + +逐条复核任务书时发现:本人评论的更多菜单原先只提供删除,无法触发 +“不能举报自己发布的内容”这一后端 message。先补红灯用例,再让菜单始终提供 +“举报评论”,本人评论额外提供“删除评论”。 + +红灯: + +``` +$ flutter test test/comment/drama_comments_sheet_test.dart --plain-name should_show_backend_message_when_reporting_own_comment +Found 0 widgets with text "Report comment" +00:00 +0 -1: Some tests failed. +``` + +修复后: + +``` +00:00 +1: All tests passed! +``` + +最终重跑: + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:12 +242 ~1: All tests passed! + +$ flutter build apk --debug +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +修复提交: + +``` +1488ee8f85671de96c70347c1b17fb9c4556bd88 +fix(flutter): 透传举报本人评论错误 +``` + +`--no-ff` 合并提交: + +``` +e1147cfcbabcdf457e427bbd118c32d37f8d9b15 +merge: 合并 K3 举报错误透传修复 +``` + +推送: + +``` +9db6935..e1147cf develop -> develop +``` + +解读:报告本人内容现在会把后端错误原样展示;author/committer 均保持统一。 + +--- + +## 2026-10-17 K5 克莱因蓝主题与印尼语适配(【Codex】) + +### 19.1 诊断分支收口 + +叙述:上一项 iOS 首页诊断插桩已提交在独立分支,不并入 K5,也不阻塞干净基线。 + +``` +$ git status --short --branch +## feature/flutter-ios-home-diagnostics + +$ git log -1 --oneline +7efbed9 chore(flutter): 增加首页加载失败诊断日志 +``` + +解读:诊断提交保留为未合并 feature 分支;K5 从 `develop e1147cf` 新建, +后续所有 K5 改动均在该基线上。 + +### 19.2 K5 计划 + +叙述:先写 `docs/plans/2026-10-17-K5.md`,记录方案 A 色值、影响文件、TDD、 +风险和待确认,再开始代码。 + +``` +Success. Updated the following files: +A docs/plans/2026-10-17-K5.md +``` + +### 19.3 K5 首次红灯 + +命令: + +``` +$ flutter test test/theme test/config/locale_controller_test.dart test/mine/mine_logic_test.dart test/widget_test.dart +``` + +原始输出(节选): + +``` +Error when reading 'lib/core/theme/app_theme.dart': No such file or directory +Member not found: 'bluePressed' +Member not found: 'onPrimary' +Method not found: 'resolveAppLocale' +Found 0 widgets with text "Bahasa Indonesia" +Expected: empty + Actual: [ + 'lib/core/theme/app_colors.dart:5: static const blue = Color(0xFF2F7BFF);', + ... + ] +00:07 +22 -5: Some tests failed. +``` + +解读:主题 token/ThemeData、locale 解析、语言选项和旧蓝扫描全部按预期红灯。 + +### 19.4 K5 实现与定向绿灯 + +实现内容: + +- `AppColors` 新增 `bluePressed/onPrimary`,更新 `blue/blueSoft`。 +- 新增 `AppTheme.light/dark` 显式 ColorScheme 和控件主题。 +- 页面旧蓝清理,深色高亮改 `blueSoft`,大面积选中态改 `bluePressed`。 +- `resolveAppLocale` 支持 en/zh/id,未知系统语言回退 id。 +- 两个语言入口增加 Bahasa Indonesia;快捷抽屉改 Wrap。 +- 日期/时间改用 `MaterialLocalizations`。 + +定向测试: + +``` +$ flutter test test/theme test/config test/formatting test/mine/mine_logic_test.dart test/widget_test.dart test/auth test/library test/watch test/download test/drama_detail +00:08 +97: All tests passed! +``` + +旧蓝扫描补跑: + +``` +$ rg -n '0xFF2F7BFF|0xFF8FB4FF|Colors\.blue|_orange|seedColor' lib -g '*.dart' +``` + +结果:无匹配(仅保留 `AppColors.blue/blueSoft/bluePressed` 等 token 引用)。 + +### 19.5 K5 最终门禁 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:12 +251 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 26.6s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:全量测试除既有 live watch 跳过外全部通过,APK 构建成功。 + +### 19.6 Git + +``` +10d448c8ba93aa2762e14babdb528805897b3992 +author: Zhou Weidong <502333560@qq.com> +committer: Zhou Weidong <502333560@qq.com> +subject: feat(flutter): 应用克莱因蓝主题与印尼语适配 + +e52d81881c19202c7adc8d13a6f78f78e4f6182f +author: Zhou Weidong <502333560@qq.com> +committer: Zhou Weidong <502333560@qq.com> +subject: merge: 合并 K5 克莱因蓝主题与印尼语适配 + +To http://git.reelshort2.com/jianghaifeng/ReelShort.git + e1147cf..e52d818 develop -> develop +``` + +解读:K5 已以 `--no-ff` 合并到 ReelShort develop 并推送,未操作 main。 + +--- + +## 2026-10-17 K6 登录页、底部导航与震动反馈(【Codex】) + +### 20.1 K6 计划与 UI 红灯 + +叙述:先写 `docs/plans/2026-10-17-K6.md`,再基于 K5 `develop e52d818` +创建 `feature/flutter-k6-auth-nav-ui`。首轮登录/底栏测试在实现前运行: + +``` +$ flutter test test/auth/auth_page_test.dart test/widget_test.dart +Found 0 widgets with text "FruitShorts" +Found 0 widgets with key [<'auth-email'>] +Expected: Color(... #101114) +00:07 +22 -5: Some tests failed. +``` + +解读:品牌区、表单 Key/显隐/焦点、底栏主题样式尚未实现,红灯符合任务范围。 + +### 20.2 K6 UI 绿灯 + +实现登录品牌区、autofill、密码显隐、next/done、busy 禁用、主题错误色、 +Small screen 滚动和底部导航主题胶囊后: + +``` +$ flutter test test/auth test/home/home_nav_ui_test.dart test/widget_test.dart test/config/l10n_id_test.dart test/theme +00:06 +42: All tests passed! +``` + +提交:`e609e3a feat(flutter): 优化登录页与底部导航`。 + +### 20.3 Haptics 补充需求红灯 + +FruitServer `93af19e` 新增 K6-C 点击震动反馈。补 fake Haptics 和触发测试后: + +``` +Error when reading 'lib/core/platform/haptics.dart': No such file or directory +No named parameter with the name 'haptics' +00:02 +1 -4: Some tests failed. +``` + +解读:Haptics 抽象、Provider 注入与页面触发尚未实现。 + +### 20.4 Haptics 实现与定向绿灯 + +新增 `Haptics` + `FlutterHaptics`,通过 Provider 注入;tab selection、 +关键操作 light、危险确认 medium;默认实现吞掉平台异常,页面调用全部 +`unawaited`。新增 fake/静态扫描/异常静默测试。 + +``` +$ flutter test test/core/haptics_test.dart test/core/haptics_scan_test.dart test/auth test/home/home_nav_ui_test.dart test/comment test/watch test/download test/library test/drama_detail test/player +00:05 +98: All tests passed! +``` + +提交:`8a481d5 feat(flutter): 增加关键操作震动反馈`。 + +### 20.5 K6 最终门禁 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:14 +263 ~1: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.6s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:震动反馈、登录页、底栏和存量测试全部通过;`~1` 为既有 live watch 跳过。 + +### 20.6 Git + +``` +e609e3a feat(flutter): 优化登录页与底部导航 +8a481d5 feat(flutter): 增加关键操作震动反馈 +0cedaab merge: 合并 K6 登录导航与震动反馈 + +To http://git.reelshort2.com/jianghaifeng/ReelShort.git + e52d818..0cedaab develop -> develop +``` + +解读:K6 已合并并推送,author/committer 均为 Zhou Weidong,未操作 main。 + +### 20.7 iOS Xcode SPM 构建修复(K6 收尾) + +叙述:只修改 `Runner.xcscheme` PreActions。新增 Python 无关的 shell +兜底链解析 Flutter,先 `flutter pub get`;原 prepare action 保留,并改为 +同一路径解析后执行 `xcode_backend.sh prepare`。 + +XML 校验: + +``` +$ xmllint --noout ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +(无输出,退出码 0) +``` + +清理 ephemeral 后直接 xcodebuild: + +``` +$ rm -rf ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage +$ xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build +Resolve Package Graph +the package at '.../FlutterGeneratedPluginSwiftPackage' cannot be accessed +xcodebuild: error: Could not resolve package dependencies +``` + +解读:Xcode 在运行 scheme PreActions 之前先解析 Package Graph,因此 +“包目录完全不存在”的首次冷启动无法仅靠 scheme PreAction 自动生成。 +这是 Xcode 执行顺序限制,不是 XML 语法错误。 + +使用 `flutter pub get` 恢复包后: + +``` +$ flutter pub get +Got dependencies! + +$ xcodebuild -list -project ios/Runner.xcodeproj +Resolved source packages: + FlutterGeneratedPluginSwiftPackage: .../ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage +... +``` + +完整重建精简日志: + +``` +$ xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build +Resolve Package Graph +Resolved source packages: +Run pre-actions + export SCHEME_ACTION_NAME="Bootstrap Flutter SPM Package" +Resolving dependencies... +Got dependencies! +** BUILD SUCCEEDED ** +``` + +解读:SPM 包存在时,PreAction 能成功刷新依赖,prepare 和完整 iPhoneOS +Debug 构建通过并完成签名。冷启动自动生成未通过验证,用户仍需在 +新克隆/`flutter clean`/手动删除 ephemeral 后先运行 `flutter pub get`。 + +Git: + +``` +cc037a2956848128c4899df1f5092fea44d3620b +fix(ios): 刷新 Flutter SPM 包后再准备框架 + +0cedaab..ac706a4 develop -> develop +``` + +解读:scheme 修复已合并推送;author/committer 均为 Zhou Weidong。 + +### 20.8 K6 导航白线与观看历史缺陷修复 + +### 导航白线 + +首次红灯: + +``` +Expected: a value less than <0.2> + Actual: <1.0> +``` + +解读:当前深色 ColorScheme 默认 `outlineVariant` 实为白色,确认了白线根因。 + +修复:新增 light/dark outline token,两个 ColorScheme 显式设置 +`outline/outlineVariant`;导航边框继续使用主题 token。 + +### 观看历史真实诊断 + +后端日志: + +``` +2026-10-17T16:35:22.816+08:00 POST /app/auth/register 耗时 242ms +2026-10-17T16:35:37.123+08:00 POST /app/watch/progress 耗时 51ms +``` + +数据库只读查询: + +``` +uid | drama_id | episode_id | position_ms | duration_ms +UKZBA9IBE | 1 | 1 | 1522 | 91326 +UKZBA9IBE | 5 | 7 | 0 | 227880 +``` + +真实 recent 响应: + +``` +{"code":0,"message":"ok","data":[ + {"dramaId":"5","episodeId":"7",...}, + {"dramaId":"1","episodeId":"1",...} +]} +``` + +临时直连 controller 诊断: + +``` +watch-records-diagnostic error=null records=2 +record drama=5 episode=7 detail=闪婚后,傅先生每天都在撒糖 loading=false failed=false +record drama=1 episode=1 detail=替嫁新娘:总裁的掌心宠 loading=false failed=false +``` + +解读:真实用户同类数据在 API、JSON 解析、controller 和详情补取链路均正常, +未复现设备侧 401/100000、解析异常或详情失败。后端日志中的 429 仍属于 +其他联调设备,用户 watch 请求没有该错误。 + +确认的客户端缺陷是 controller 吞掉真实异常: + +``` +Expected: '账号不存在,请先注册' + Actual: 'progressError' +``` + +修复:debug 打印异常类型、原文与堆栈;ApiException message 透传页面错误态。 + +### 定向与全量门禁 + +``` +$ flutter test test/theme test/home/home_nav_ui_test.dart test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart +00:01 +21: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:13 +266 ~1: All tests passed! + +$ flutter build apk --debug +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +### Git + +``` +732483465ced2b28438585a4b2f1573e9a0529cb +fix(flutter): 修复导航白线与观看记录错误透传 + +ac706a4..b7b1c50 develop -> develop +``` + +解读:修复已合并推送;用户设备特定 error 仍需设备日志确认。 + +--- + +## 2026-10-17 K7 首页轮播与站内通知(【Codex】) + +### 21.1 K7 计划与首次红灯 + +叙述:先写 `docs/plans/2026-10-17-K7.md`,从干净 `develop b7b1c50` +创建 `feature/flutter-k7-carousel-notices`。 + +首次轮播/通知测试: + +``` +No named parameter with the name 'active' +Error when reading 'lib/core/data/models/notice.dart': No such file or directory +Error when reading 'lib/core/data/remote/notice_api.dart': No such file or directory +00:00 +0 -3: Some tests failed. +``` + +解读:轮播增强、通知数据层和页面尚不存在,红灯符合预期。 + +### 21.2 轮播绿灯 + +实现 Timer、交互暂停、active/TickerMode、圆点、失败占位与语义后: + +``` +$ flutter test test/discover/cover_carousel_test.dart +00:00 +7: All tests passed! +``` + +### 21.3 通知实现与定向绿灯 + +实现 Notice model/API/repository/controller/Page、我的页入口和三语文案后, +真实匿名接口返回: + +``` +$ curl -sS 'http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=20' +{"code":0,"message":"ok","data":{"total":0,"list":[]}} +``` + +通知与轮播定向测试: + +``` +$ flutter test test/discover/cover_carousel_test.dart test/notice +All tests passed! +``` + +期间修正 320px 下类型/时间同一行 3.5px 溢出,改为双 Flexible。 + +### 21.4 K7 最终门禁 + +``` +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test +00:14 +284 ~1: All tests passed! + +$ flutter build apk --debug +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +### 21.5 Git + +``` +b870956665f5268070c691533cc2a334f2aa7f59 +feat(flutter): 增加轮播自动播放与站内通知 + +b7b1c50..3e8703b develop -> develop +``` + +解读:K7 已合并推送;author/committer 均为 Zhou Weidong。 + +--- + +## 2026-10-17 K7 真机缺陷修复:观看历史 Provider 与首页轮播数据源 + +### 22.1 范围与分支 + +叙述:用户真机验收后报告观看历史仍报错、首页轮播仍不显示。本轮只修客户端 +Provider 注册与首页轮播数据源,不改后端、Web、specs 或既有发现页分区逻辑。 +ReelShort 从 `develop 3e8703b` 拉 +`fix/flutter-home-watch-provider-banners`;FruitServer 从 `develop 9c6aaed` +拉 `docs/flutter-home-watch-device-fix`,本仓库只改 `docs/plans/PROGRESS.md`。 + +### 22.2 先红:复现设备报告 + +真实 `LumaDramaApp` Provider 树打开「我的 -> 观看历史」: + +```text +ProviderNotFoundException: +Could not find the correct Provider +above this _WatchRecordsBody Widget +``` + +首页 feed 返回 2 条 banner、discover feed 返回 0 条 banner: + +```text +Expected: exactly one matching candidate + Actual: Found 0 widgets with type "CoverCarousel" +``` + +解读:两处红灯分别复现了用户报告的 Provider 缺失和轮播为空。 + +### 22.3 修复 + +- `lib/app.dart` 在现有 Repository Provider 附近注册 + `Provider.value(value: _catalogRepository)`,与 + `CatalogController` 共用同一实例。 +- `lib/pages/discover/discover_view.dart` 改为监听 + `CatalogController.feed.banners`,修复后首页 tab 使用 `/app/home/feed` + 的 banners;发现页分区仍使用 `CategoryController`,没有新增接口。 +- 新增 `test/watch/watch_history_provider_test.dart`:在真实 Provider 树打开 + 观看历史,断言不抛错并展示空态。 +- 新增 `test/discover/home_banner_source_test.dart`:home banners 为 2、 + discover banners 为空时,断言渲染两个圆点、首图显示且 4.5 秒后自动切到次图。 + +### 22.4 门禁原始输出 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test test/watch/watch_history_provider_test.dart test/discover/home_banner_source_test.dart +00:01 +2: All tests passed! +``` + +```text +$ flutter test test/discover/cover_carousel_test.dart test/watch/watch_records_page_test.dart +00:00 +12: All tests passed! +``` + +```text +$ flutter test +00:16 +286 ~1: All tests passed! +``` + +解读:完整套件 286 项通过,1 项联调测试因客户端接口服务未启动按设计跳过; +输出中的 `[WatchRecordsController] ...` 异常堆栈来自故意注入失败的既有测试。 + +### 22.5 Git + +```text +5058853 fix(flutter): 注册目录仓库并修正首页轮播数据源 +6cc5bfc merge: 合并首页轮播与观看历史真机修复 +3e8703b..6cc5bfc develop -> develop +``` + +修复提交与合并提交的 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 22.6 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,开发机测试不能替代真机证据。 + +1. 使用最新 `develop` 重新构建并安装,登录用户设备 + `d-mvc33zh5-pemv3e`。 + 预期:`flutter run` 终端不再出现 `ProviderNotFoundException`。 +2. 打开「我的 -> 观看历史」。 + 预期:页面正常打开;有历史时显示剧目与进度,无历史时显示空态,不再进入 + Provider 错误页。 +3. 回到发现页并等待首页 feed 加载完成。 + 预期:顶部轮播显示后端 `/app/home/feed` 返回的 2 张 banner,底部有两个圆点。 +4. 等待 4.5 秒。 + 预期:轮播自动切到第 2 张;手动拖动后仍可从当前页继续自动轮播。 + +设备侧截图、日志与最终验收结论由用户执行。 + +--- + +## 2026-10-17 K10 统一空列表占位 + +### 26.1 范围、分支与计划 + +叙述:新增统一 `EmptyState`,替换任务书表格中的 10 处纯文字空态。只改空态 +视觉、三语 l10n 与测试,不改列表数据、接口、加载/失败逻辑。 + +- 客户端分支:`feature/flutter-k10-empty-states`,基线 `19babb8`。 +- 文档分支:`docs/k10-empty-states`,基线 K10 任务书 `3f84bc6`。 + +开工前先写并提交计划: + +```text +f1d56b1 docs(plans): 增加 K10 统一空态实施计划 +``` + +### 26.2 先红:组件与页面 + +组件文件尚不存在: + +```text +$ flutter test test/empty_state/empty_state_test.dart +Error when reading 'lib/core/widgets/empty_state.dart': No such file or directory +Failed to load ".../empty_state_test.dart" +``` + +页面测试已能运行,但五处空态仍是旧实现: + +```text +$ flutter test test/library/favorites_page_test.dart test/download/downloads_page_test.dart test/watch/watch_records_page_test.dart test/notice/notices_page_test.dart test/comment/drama_comments_sheet_test.dart +Found 0 widgets with type "EmptyState": [] +00:03 +28 -6: Some tests failed. +``` + +解读:组件、收藏、下载、记录、通知、评论空态均未接入,红灯符合 K10 范围。 + +### 26.3 组件实现 + +新增: + +```text +128 lib/core/widgets/empty_state.dart +``` + +规格: + +- 默认 96x96、圆角 28、`AppColors.blue` 11% 背景,图标 42。 +- compact 64x64、圆角 18、图标 28。 +- 标题 16/w600,副标题 13 muted,最多 2 行。 +- 可选 TextButton 最小高度 44,主题主色。 +- 深色主题图标 `blueSoft`,浅色主题图标 `blue`。 +- 外层 Semantics 只读标题;图形和文本排除重复语义,操作按钮仍可访问。 + +新增三语副标题: + +```text +en: New content will appear here. +zh: 有新内容时会显示在这里。 +id: Konten baru akan muncul di sini. +``` + +### 26.4 10 处替换 + +```text +lib/pages/home/home_view.dart movie_outlined +lib/pages/discover/widgets/home_drama_grid.dart grid_view_rounded / search_off_rounded +lib/pages/favorites/favorites_page.dart favorite_border_rounded +lib/pages/notices/notices_page.dart notifications_none_rounded +lib/pages/drama_detail/drama_detail_view.dart video_library_outlined +lib/pages/mine/mine_view.dart bookmark_border_rounded / history_rounded +lib/pages/downloads/downloads_page.dart download_outlined +lib/pages/watch_records/watch_records_page.dart history_rounded +lib/pages/player/widgets/drama_comments_sheet.dart chat_bubble_outline_rounded(compact) +``` + +首页将原合并的 `error || empty` 分支拆开: + +- error 继续使用 `error_outline_rounded`、`catalogError`、重试与 debug API 地址。 +- 空列表才使用 `EmptyState(noStories)`。 + +收藏、通知、记录、评论等失败态仍保留原错误与重试,不用 `EmptyState`。 + +### 26.5 定向与全量绿灯 + +组件测试: + +```text +$ flutter test test/empty_state/empty_state_test.dart +00:00 +4: All tests passed! +``` + +组件与五处页面: + +```text +$ flutter test test/empty_state/empty_state_test.dart test/library/favorites_page_test.dart test/download/downloads_page_test.dart test/watch/watch_records_page_test.dart test/notice/notices_page_test.dart test/comment/drama_comments_sheet_test.dart +00:03 +38: All tests passed! +``` + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:18 +302 ~1: All tests passed! +``` + +解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;既有页面 +用例保留,新增断言只增加 `EmptyState` 校验,不删除原用例。 + +### 26.6 Android debug 构建 + +用户 iOS `flutter run` 仍在运行,未终止该会话。为避免争用原项目 `build/`, +将同一工作区(排除 build/.git/.dart_tool/ephemeral)同步到独立临时副本后构建: + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 6.9s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:K10 最终代码通过 Android debug 构建,且原 iOS 运行会话未被中断。 + +### 26.7 Git + +```text +6d08330 feat(flutter): 统一空列表占位组件 +0466b4b merge: 合并 K10 统一空态组件 +19babb8..0466b4b develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 26.8 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 使用最新客户端 `develop`,分别切换深色和浅色主题进入一个空列表页面。 + 预期:显示 96x96 柔和蓝色底、图标、标题与可选说明,不白屏、不溢出。 +2. 打开收藏、下载、观看记录、通知页面并清空对应数据。 + 预期:均显示统一 `EmptyState`,原有文案与清空/刷新逻辑正常。 +3. 打开无评论的剧目评论弹窗。 + 预期:显示 compact 64x64 图标占位,弹窗仍可滚动与输入。 +4. 打开无剧集详情或首页无剧目场景。 + 预期:显示对应空态;重试按钮行为保持不变。 +5. 模拟接口失败。 + 预期:仍显示原错误图标、错误文案和重试,不显示空态占位。 +6. 在 320px 宽小屏检查长标题、副标题和按钮。 + 预期:最多两行、可换行且无横向溢出,按钮点击区域不小于 44pt。 + +设备侧截图、语音朗读与最终验收结论由用户执行。 + +--- + +## 2026-10-17 K11 首页 3 列与全部分类 + +### 27.1 范围、分支与计划 + +叙述:在 K9 首页「全部剧目」网格上做最小修订,每行 3 个,分类首项从 +`For You` 改为「全部」。只改网格参数、首个分类 label 和对应测试。 + +- 客户端分支:`feature/flutter-k11-home-grid-3col`,基线 `0466b4b`。 +- 文档分支:`docs/k11-home-grid-3col`,基线任务书 `2a06190`。 + +开工前先写并提交计划: + +```text +f97574b docs(plans): 增加 K11 首页三列网格实施计划 +``` + +### 27.2 先红:3 列与全部文案 + +先更新 K9 网格测试的期望值并执行: + +```text +$ flutter test test/discover/home_all_dramas_grid_test.dart +Expected: exactly one matching candidate + Actual: _TextWidgetFinder: + +00:01 +0 -1: Some tests failed. +``` + +解读:分类首项仍显示旧文案,3 列断言在首个失败处之后尚未执行;红灯与 K11 +目标一致。 + +### 27.3 实现 + +`home_drama_grid.dart`: + +```text +crossAxisCount: 3 +mainAxisSpacing: 14 +crossAxisSpacing: 10 +childAspectRatio: 2 / 3 +``` + +`discover_view.dart`: + +```text +(code: CategoryController.allCode, label: l10n.all) +``` + +已核对三份 ARB 原本已有 `all`: + +```text +lib/l10n/app_en.arb:49: "all": "All", +lib/l10n/app_zh.arb:49: "all": "全部", +lib/l10n/app_id.arb:49: "all": "Semua", +``` + +解读:直接复用现有三语 getter,未添加重复 JSON key,也未改动 l10n 生成文件。 + +### 27.4 定向绿灯 + +```text +$ flutter test test/discover/home_all_dramas_grid_test.dart +00:01 +5: All tests passed! +``` + +测试同时断言: + +- `crossAxisCount == 3` +- `crossAxisSpacing == 10` +- `mainAxisSpacing == 14` +- `childAspectRatio == 2/3` +- 首个 chip 为 `All`,且位于 `Hot` 左侧 +- 320px 小屏 3 列布局无溢出 + +两个既有首页壳层用例仍保留,只更新旧分类断言: + +```text +$ flutter test test/widget_test.dart +00:05 +17: All tests passed! +``` + +### 27.5 全量门禁与构建 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:18 +302 ~1: All tests passed! +``` + +解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 + +用户 Android `flutter run` 仍在运行,未终止该会话。为避免争用原项目 +`build/`,将当前工作区同步到独立临时副本后执行: + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 21.8s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +### 27.6 Git + +```text +f0db631 feat(flutter): 首页网格改为三列并首位显示全部 +b5a8656 merge: 合并 K11 首页三列网格 +0466b4b..b5a8656 develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 27.7 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 使用最新客户端 `develop` 进入首页。 + 预期:分类 chips 第一项显示「全部 / All / Semua」,其后仍是热门、最新和 + 服务端分类。 +2. 查看「全部剧目」列表。 + 预期:每行 3 个剧目,列间距 10、行间距 14,左右边距保持 16。 +3. 检查长标题和类型。 + 预期:标题最多 2 行、类型最多 1 行,不横向或纵向溢出。 +4. 点击「全部」及其他分类。 + 预期:网格内容按现有逻辑更新,点击卡片仍进入对应详情。 +5. 在 320px 宽设备或小屏模拟器滚动列表。 + 预期:3 列布局稳定,无卡片重叠或整页溢出。 + +设备侧截图、滚动和分类切换结果由用户执行。 + +--- + +## 2026-10-17 K12 首页去标题与空态重做 + +### 28.1 范围、分支与计划 + +叙述:删除 K11 首页「全部剧目」标题,使分类 chips 后直接进入网格;同时将 +K10 空态从实心圆角块改为双层同心圆。只改首页间距、`EmptyState` 和对应测试。 + +- 客户端分支:`feature/flutter-k12-home-empty-polish`,基线 `b5a8656`。 +- 文档分支:`docs/k12-home-empty-polish`,基线任务书 `87a37e9`。 + +开工前先写并提交计划: + +```text +e49664c docs(plans): 增加 K12 首页与空态视觉修订计划 +``` + +构建前已确认用户真机会话停止,进程检查无并发 Flutter run/build。 + +### 28.2 先红:标题与旧空态 + +```text +$ flutter test test/empty_state/empty_state_test.dart test/discover/home_all_dramas_grid_test.dart +Found 0 widgets with key [<'empty-state-art-inner'>]: [] +Expected: Size(60, 60) + Actual: Size(64, 64) +Expected: <260> + Actual: <280.0> +Found 1 widget with key [<'home-all-dramas-title'>] +00:02 +2 -7: Some tests failed. +``` + +解读:旧实心圆角空态仍为 96/64、最大宽 280,首页标题仍存在,红灯与 K12 +目标一致。 + +### 28.3 首页去标题 + +删除 `home-all-dramas-title` Text 及其间隔,分类 Sliver 的底部 padding 改为 14。 +实际 chips 控件增加稳定 Key: + +```text +home-category-chips +``` + +测试读取 chips 底边与首张卡片的 RenderBox 顶边,断言间距: + +```text +closeTo(14, 0.1) +``` + +### 28.4 空态双层同心圆 + +最终规格: + +```text +default outer: 84, blue alpha 0.055, border alpha 0.11 +default inner: 56, blue alpha 0.11 +compact outer: 60; inner: 40 +icon: 27 / compact 21 +title: 15 w600 +subtitle: 12.5, height 1.45 +button: 13, min height 44 +padding: 24x28 / compact 16x16 +max width: 260 +``` + +新增 Key: + +```text +empty-state-art-inner +empty-state-padding +``` + +`empty-state-art`、`empty-state-icon`、`empty-state-title`、 +`empty-state-action` 保持,外层 Semantics 仍只读标题。 + +### 28.5 定向与全量绿灯 + +组件、首页和既有壳层: + +```text +$ flutter test test/empty_state/empty_state_test.dart test/discover/home_all_dramas_grid_test.dart test/widget_test.dart +00:06 +26: All tests passed! +``` + +既有壳层测试中的 `All Dramas` 文本锚点改为稳定网格 Key +`home-all-dramas-grid`,原导航流程与用例全部保留。 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:17 +302 ~1: All tests passed! +``` + +解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 + +### 28.6 APK 构建 + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 16.6s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +### 28.7 Git + +```text +2dd4627 feat(flutter): 精简首页标题并重做空态视觉 +b762289 merge: 合并 K12 首页与空态视觉修订 +b5a8656..b762289 develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 28.8 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 打开首页并查看分类 chips 下方。 + 预期:不再显示「全部剧目」标题,chips 下方 14px 后直接出现 3 列网格。 +2. 打开任一空列表页面。 + 预期:显示双层同心圆,外层比内层更淡,图标居中,不再是大块实心圆角矩形。 +3. 分别切换深色和浅色主题。 + 预期:深色图标为 `blueSoft`,浅色图标为 `blue`,文字与背景对比清晰。 +4. 检查带副标题和操作按钮的空态。 + 预期:标题 15、副标题 12.5、按钮 13,间距舒展,按钮点击区域不少于 44pt。 +5. 打开无评论弹窗。 + 预期:使用 60/40 compact 双层圆,图标 21,上下留白 16,不挤压输入区。 +6. 在 320px 宽小屏检查空态和首页。 + 预期:最大宽 260 生效,无横向溢出,标题最多两行。 + +设备侧截图、深浅主题与小屏结果由用户执行。 + +--- + +## 2026-10-17 K13 运行时服务器地址设置 + +### 29.1 范围、分支与计划 + +叙述:解决 debug APK 不带 `--dart-define` 时固定回退到 `.local` 的问题。 +新增 debug-only 运行时地址,优先级为本地设置 > dart-define > 现有默认。 + +- 客户端分支:`feature/flutter-k13-dev-server-setting`,基线 `b762289`。 +- 文档分支:`docs/k13-dev-server-setting`,基线任务书 `1a5951c`。 + +开工前先写并提交计划: + +```text +6405dff docs(plans): 增加 K13 开发服务器地址实施计划 +``` + +当前有真机 `flutter run` 会话,未主动终止;构建在独立副本中完成。 + +### 29.2 先红:运行时配置与入口缺失 + +```text +$ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart +Error when reading 'lib/core/config/server_endpoint.dart': No such file or directory +Member not found: 'AppConfig.clearRuntimeBaseUrl' +No named parameter with the name 'showServerSetting' +Some tests failed. +``` + +解读:运行时解析器、AppConfig 覆盖入口和 debug 设置参数均不存在,红灯符合 K13 +范围。 + +### 29.3 运行时解析与启动顺序 + +新增: + +```text +86 lib/core/config/server_endpoint.dart +``` + +关键规则: + +```text +preference key: dev.api_base_url +runtime > dart-define(API_BASE_URL) > default +normalize: trim, http(s), non-empty host, trailing slash removed +release: ServerEndpoint.load(debug: false) returns before preferences are read +``` + +`main()` 在 `runApp` 前执行: + +```dart +await ServerEndpoint.load(); +runApp(const LumaDramaApp()); +``` + +因此 `LumaDramaApp` 创建共用 ApiClient 前,运行时地址已经加载完成。 + +### 29.4 Debug 设置入口 + +我的页设置弹窗增加服务器地址行: + +- 显示当前 `AppConfig.apiBaseUrl`; +- 编辑弹窗预填当前地址; +- 保存合法值后提示重启生效; +- 非法值显示三语错误且不写 SharedPreferences; +- 恢复默认清除 `dev.api_base_url`; +- 设置面板改为 85% 屏高内可滚动,避免小屏溢出。 + +可见性通过 `MyPage.debugShowSetting` 注入,默认 `kDebugMode`。release +编译时默认 false,不显示该行,也不读取本地覆盖。 + +新增三语 key: + +```text +serverAddress +serverAddressHint +saveAddress +restartToApply +restoreDefault +invalidServerAddress +``` + +### 29.5 定向与全量绿灯 + +```text +$ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart +00:00 +7: All tests passed! +``` + +覆盖: + +- 本地设置 > dart-define > 默认; +- trim、末尾斜杠、http/https; +- `ftp://`、缺 scheme、空 host 拒绝; +- debug=false 不应用本地覆盖; +- save/clear SharedPreferences; +- debug 入口可见、注入 false 不可见; +- 非法地址不保存并显示错误。 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:19 +309 ~1: All tests passed! +``` + +解读:309 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 + +### 29.6 无 dart-define 的 Debug APK + +用户真机 run 未终止。将当前工作区同步到独立临时副本,并明确不带构建参数: + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 23.4s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:即使 APK 没有 `--dart-define`,安装后也可在 debug 设置中保存真实服务器 +地址,重启后覆盖现有 `.local` 默认值。 + +### 29.7 Git + +```text +907562a feat(flutter): 增加 debug 运行时服务器地址设置 +f95325d merge: 合并 K13 开发服务器地址设置 +b762289..f95325d develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 29.8 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 安装本次不带 `--dart-define` 构建的 debug APK。 + 预期:App 可以启动,进入「我的 → 设置」能看到「服务器地址」。 +2. 点击「服务器地址」。 + 预期:编辑框预填当前地址,并显示「重启 App 生效」。 +3. 输入真实局域网地址,例如 `http://192.168.31.187:8081`,保存。 + 预期:返回设置页并提示重启;输入框不会保留非法格式。 +4. 完全结束 App 后重新打开,确认首页/发现请求成功,并在后端日志看到来自手机 + 的 `/app/home/feed` 等请求。 + 预期:请求使用新地址,不再访问 `.local` 默认地址。 +5. 再次进入设置,点击「恢复默认」并重启。 + 预期:本地覆盖被清除,回到编译配置地址。 +6. 用 release 构建检查。 + 预期:设置中完全没有服务器地址入口,且即使本地存有 key 也不会读取。 + +设备侧日志、截图与最终连通性结论由用户执行。 + +--- + +## 2026-10-17 K14 客户端文档 + +### 30.1 范围与依据 + +叙述:重写客户端 README,新增任务清单和文件说明。只改 ReelShort 的三份文档, +不改代码、specs、后端或 Web。 + +核对依据: + +- `pubspec.yaml`、`pubspec.lock` +- `lib/` 与 `test/` 实际目录 +- `lib/core/data/remote/**` 实际调用 +- `FruitServer/docs/api-phase1.md` 章节 +- `FruitServer/docs/plans/PROGRESS.md` 已完成批次 + +客户端分支:`docs/k14-client-docs`,基线 `f95325d`。 + +### 30.2 接口与目录核对 + +Flutter 版本: + +```text +$ flutter --version +Flutter 3.47.7 • channel stable +Tools • Dart 3.13.5 • DevTools 2.60.0 +``` + +接口路径: + +```text +$ rg -o "'/app/[^']+'" lib/core/data/remote -g '*.dart' | sort -u | wc -l + 26 +``` + +按 HTTP 方法和重复 URL 逐项统计为 31 个方法级调用。README 已按账号、内容、 +观看记录、收藏、下载、互动和公告分组列出。 + +### 30.3 文档内容 + +`ReelShort/README.md`: + +- 印尼市场定位、技术栈、目录概览; +- 实际接口清单; +- Android USB、Android 模拟器、iOS 和 debug 运行时地址; +- iOS foundation 2.5.6、SPM 恢复、Android libdartjni 处理; +- 门禁命令、测试命名和当前边界。 + +`ReelShort/docs/TASKS.md`: + +- K1-K13 批次表; +- K4、支付/金币、搜索、下载落盘待办; +- 未确认范围标「需核对」,未编造提交号。 + +`ReelShort/docs/FILES.md`: + +- 目录树、关键文件职责、test 结构; +- `app_localizations*.dart`、`*.g.dart`、build、ephemeral 等生成物标注为勿手改。 + +仓库 `.gitignore` 包含 `/docs/`。本轮只对新增的两个 docs 文件使用 +`git add -f`,没有修改 `.gitignore`。 + +### 30.4 门禁 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:20 +309 ~1: All tests passed! +``` + +解读:文档改动未误改代码;309 项通过,1 项联调测试因服务未启动跳过。 + +### 30.5 Git + +```text +5a28845 docs(client): 重写 README 并补充任务与文件说明 +213c86e docs(client): 修正已接接口数量说明 +9fe62d1 merge: 合并 K14 文档接口统计修正 +f95325d..9fe62d1 develop -> develop +``` + +主提交、修正提交和最终合并的 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 30.6 文档验收 + +1. 打开 `ReelShort/README.md`,按表中命令可完成 Android USB、iOS 和 debug + 运行时地址配置。 +2. 打开 `ReelShort/docs/TASKS.md`,能区分已合入功能与待办/需核对事项。 +3. 打开 `ReelShort/docs/FILES.md`,能定位核心配置、数据层、页面、测试和生成物。 +4. 文档中的接口路径与 `lib/core/data/remote/` 一致。 + +--- + +## 2026-10-17 K15 首页服务器设置与真机默认地址 + +### 31.1 最终范围 + +用户最终确认手机与 Mac 在同一网络,不使用 USB 转发。修复范围确定为: + +1. 真机默认地址改为 `http://10.157.242.1:8081`,只替换 + `devHostBaseUrl`; +2. 平台默认选择与优先级保持不变:运行时设置 > dart-define > 平台默认; +3. 首页加载失败页 debug 下增加「服务器设置」,复用 K13 地址弹窗; +4. release 不显示入口。 + +客户端修复分支:`fix/home-server-default`,基线 `9fe62d1`。 + +### 31.2 地址与失败页实现 + +`AppConfig`: + +```text +devHostBaseUrl = "http://10.157.242.1:8081" +``` + +注释已说明该 IP 跟随开发机网络变化,换网络时只改这一行。模拟器、桌面端和 +iOS 模拟器仍使用 `127.0.0.1`;iOS/Android 真机使用该局域网地址。 + +K13 的服务器地址弹窗抽到: + +```text +lib/core/widgets/server_endpoint_dialog.dart +``` + +我的页和首页错误页共用同一弹窗。首页错误页新增 +`home-server-settings` 按钮,仅调试工具开启时显示;可编辑、保存、恢复默认, +保存后提示重启生效。 + +### 31.3 测试与门禁 + +新增/更新测试: + +- `AppConfig.devHostBaseUrl` 为最终局域网地址; +- 原有运行时 > dart-define > 默认优先级继续通过; +- 首页错误页 debug 下显示按钮并可打开 `server-endpoint-input`; +- 注入 release 条件时按钮不显示; +- 我的页设置入口与非法地址拒绝继续通过。 + +```text +$ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart test/home/home_server_setting_test.dart +00:02 +10: All tests passed! +``` + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:21 +312 ~1: All tests passed! +``` + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 13.2s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:312 项通过,1 项联调测试因服务未启动跳过;debug APK 构建成功。 + +### 31.4 Git + +```text +c4694c6 fix(flutter): 增加首页服务器设置并更新真机默认地址 +4778ce2 merge: 合并 K15 首页服务器设置修复 +9fe62d1..4778ce2 develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 31.5 用户真机验证步骤 + +1. Android Studio 直接 Run,不额外传 `--dart-define`。 + 预期:默认请求 `http://10.157.242.1:8081`。 +2. 若首页仍加载失败,在错误页查看 debug 地址和「服务器设置」按钮。 + 预期:点击后打开与「我的 → 设置」相同的地址弹窗。 +3. 修改地址并保存,完全结束 App 后重开。 + 预期:使用新地址请求,后端日志出现手机侧请求。 +4. release 构建中检查首页错误页。 + 预期:不显示服务器设置入口。 + +设备侧日志、截图和最终网络连通性结论由用户执行。 + +--- + +## 2026-10-17 K16 注册限流后台配置化 + +### 32.1 最终口径 + +叙述:K16 开工后 Trae 于 `461fb8e` 更新任务书,要求从 `sys_config` 读取配置, +不使用 `application.yml` 固化。初版 `@Value` 实现已按最新口径修正。 + +最终配置键: + +```text +app.register.max-per-ip-per-day +app.register.max-per-device-per-day +``` + +- 值 <= 0:对应维度不限制; +- 键缺失或非整数:回退 IP 5 / 设备 3; +- 错误码和错误文案不变。 + +### 32.2 后端实现与测试 + +`AppAuthService` 注入 `ConfigService`,module-user 增加 module-system 依赖。 +首次红灯: + +```text +Could not find field 'maxRegisterPerIpPerDay' +Tests run: 16, Failures: 0, Errors: 4 +``` + +最终定向测试: + +```text +$ mvn -pl fruitshorts-app-api -am -Dtest=AppAuthServiceTest -Dsurefire.failIfNoSpecifiedTests=false test +Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +覆盖:IP 超限、设备超限、自定义阈值、IP=0、设备=0、缺省回退、非数字回退。 + +全量测试: + +```text +$ mvn test +[INFO] BUILD SUCCESS +``` + +### 32.3 种子配置与后台页面 + +`sql/data-phase1.sql` 增加幂等种子: + +```sql +INSERT INTO sys_config (id, config_key, config_value, remark) VALUES + (3, 'app.register.max-per-ip-per-day', '0', '0=不限制;生产建议 5'), + (4, 'app.register.max-per-device-per-day', '0', '0=不限制;生产建议 3') +ON CONFLICT (config_key) DO NOTHING; +``` + +当前开发库执行结果: + +```text +INSERT 0 2 + id | config_key | config_value | remark +----+-------------------------------------+--------------+-------------------- + 3 | app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5 + 4 | app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3 +``` + +后台 `SettingsView.vue` 原有页面只显示两个旧配置字段,因此补充了两个数值项, +沿用现有 `/admin/configs` 接口,不新建页面。 + +```text +$ pnpm build:pro +Build successful. Please see dist-pro directory +``` + +Web 提交 `54db082`,合并 `5c087f4`,已推送。 + +### 32.4 打包、重启与接口自检 + +```text +$ mvn -pl fruitshorts-app-api -am package -DskipTests +[INFO] BUILD SUCCESS +``` + +旧 8081 服务停止后,通过 `launchctl` 启动新 jar。当前服务: + +```text +80642 0 com.fruitshorts.app-api +java 80642 ... TCP *:8081 (LISTEN) +``` + +健康检查: + +```text +HTTP/1.1 200 +{"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api",...}} +``` + +同一新设备号连续注册 4 次(令牌字段已省略): + +```text +attempt 1 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"U-4JA-DDD"} +attempt 2 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UTIOUCLLV"} +attempt 3 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UR6ZJRO74"} +attempt 4 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UXJF6MECJ"} +``` + +解读:开发库两键均为 0,IP 与设备 guard 均跳过,连续注册不再 429。 + +### 32.5 Git + +```text +FruitServer: +3403a52 docs(plans): 增加 K16 注册限流配置化计划 +758eb69 feat(backend): 注册限流阈值改为可配置 +f1aaaee fix(backend): 改为后台配置注册限流阈值 +f4003ed test(backend): 覆盖注册限流缺省回退 +5b7be2e merge: 合并注册限流缺省回退测试 +78fb995..5b7be2e develop -> develop + +ReelShortWeb: +54db082 feat(admin): 增加注册限流配置项 +5c087f4 chore(admin): 合并注册限流配置项 +3ec9ea6..5c087f4 develop -> develop +``` + +最终后端修正与 Web 提交的 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 32.6 用户验收步骤 + +1. 登录后台「系统设置」。 + 预期:看到注册频控两个数值项,当前均为 0。 +2. 将单设备上限改为 3,保存后使用同一设备注册第 4 次。 + 预期:返回 429;恢复 0 后第 4 次可成功。 +3. 将单 IP 上限改为 1,保存后从同一 IP 注册第 2 次。 + 预期:返回 429;恢复 0 后恢复不限。 +4. 验证完成后开发库恢复 0/0;生产配置建议恢复 IP 5 / 设备 3。 + +设备侧接口结果与后台截图由用户最终验收。 + +### 32.7 API 文档键名补正 + +复核 `docs/api-phase1.md` §1.1 时发现文档仍沿用旧的 +`fruitshorts.auth.register-max-*` 键名,与 `AppAuthService` 实际读取的 +`sys_config` 键不一致。已补正为: + +```text +app.register.max-per-ip-per-day +app.register.max-per-device-per-day +``` + +同时将入口表述统一为后台「系统配置」。本次只补正文档,不改运行逻辑。 + +补正后复核: + +```text +$ mvn test +AppAuthServiceTest: Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +开发库两键均为 `0`。同一新设备号连续注册 4 次均为 `code=0`: + +```text +uid=UFCIW1LWU +uid=UP2BH-DN_ +uid=ULOFAAU2Z +uid=UDAYJR-_E +``` + +--- + +## 2026-10-17 K16 返工:sys_config 优先 + @Value 兜底 + +### 33.1 返工范围 + +K16 首版合并后,用户要求明确区分后台运行配置与 yml fallback: + +- `AppAuthService` 运行时优先读取 `sys_config`: + `app.register.max-per-ip-per-day`、`app.register.max-per-device-per-day`; +- 键缺失或非整数时,回退 `@Value` 注入的 + `fruitshorts.auth.register-max-*`; +- yml 中的值只作 fallback,默认 5 / 3;≤0 仍表示不限制。 + +返工分支:`fix/k16-register-config-fallback`。 + +### 33.2 TDD 与测试 + +先把非数字配置用例改为验证 @Value fallback=2,旧实现红灯: + +```text +Could not find field 'maxRegisterPerIpPerDay' +Tests run: 18, Failures: 0, Errors: 1, Skipped: 0 +BUILD FAILURE +``` + +增加注入字段并接入 registerLimit 后,定向测试转绿: + +```text +RUNNING AppAuthServiceTest +Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +全量测试: + +```text +$ mvn test +[INFO] fruitshorts-module-user ............................ SUCCESS +[INFO] fruitshorts-app-api ............................... SUCCESS +[INFO] BUILD SUCCESS +``` + +代码提交: + +```text +35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底 +``` + +### 33.3 数据库与后台接口 + +重新执行 `sql/data-phase1.sql` 中的幂等插入: + +```text +INSERT 0 0 +``` + +查询结果: + +```text +config_key | config_value | remark +app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3 +app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5 +(2 rows) +``` + +使用本机开发管理员登录后,`GET /admin/configs` 返回: + +```json +{ + "code": 0, + "message": "ok", + "configs": [ + { + "key": "app.register.max-per-device-per-day", + "value": "0", + "remark": "0=不限制;生产建议 3" + }, + { + "key": "app.register.max-per-ip-per-day", + "value": "0", + "remark": "0=不限制;生产建议 5" + } + ] +} +``` + +现有 `SettingsView.vue` 已使用 `listConfigsApi` / `updateConfigsApi`, +页面上的两个数值输入项可直接读写这两键。 + +### 33.4 阈值 2 往返验证 + +通过后台 `PUT /admin/configs` 把 IP 上限临时设为 2、设备上限设为 0, +使用同一条测试 IP 连续注册: + +```text +attempt 1 HTTP 200 code=0 message=ok +attempt 2 HTTP 200 code=0 message=ok +attempt 3 HTTP 200 code=429 message=当前网络注册过于频繁,请稍后再试 +``` + +随后通过同一后台接口恢复 IP=0,再注册一次: + +```text +attempt 4 HTTP 200 code=0 message=ok +``` + +说明:当前统一响应包装对外使用 HTTP 200,业务码在 `code` 字段返回 429。 + +### 33.5 打包与 8081 + +```text +$ mvn -pl fruitshorts-app-api -am package -DskipTests +[INFO] BUILD SUCCESS +``` + +通过 `launchctl kickstart -k gui/501/com.fruitshorts.app-api` 重启: + +```text +before PID: 80642 +after PID: 94657 +java 94657 ... TCP *:8081 (LISTEN) +``` + +健康检查: + +```text +{"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api","profiles":"","time":"2026-10-17T23:38:32.363463"}} +HTTP 200 +``` + +--- + +## 2026-10-17 iOS 构建阻塞:固定 shared_preferences_foundation 2.5.6 + +### 25.1 现象与根因 + +用户从 Xcode 16.2 构建 iOS 时,Swift 6.0.3 在 +`shared_preferences_foundation 2.6.0` 生成文件中报: + +```text +Unexpected , separator +messages.g.swift:33:2 +``` + +根因是 2.6.0 的函数参数列表使用 trailing comma,需要 Swift 6.1。此前可构建 +时锁文件为 foundation 2.5.7;`a4415c8` 同步依赖锁后升到了 2.6.0。 + +用户随后修正固定目标:最终使用 2.5.6,不使用 2.5.4;2.5.7 的本地缓存缺 +`PrivacyInfo.xcprivacy`,也不使用。 + +### 25.2 修复 + +客户端从 `develop a4415c8` 创建独立分支: + +```text +fix/ios-shared-preferences-foundation +``` + +`pubspec.yaml`: + +```yaml +dependency_overrides: + shared_preferences_foundation: 2.5.6 +``` + +执行: + +```text +$ flutter pub get +Resolving dependencies... +Downloading packages... +! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available) +Got dependencies! +``` + +锁文件: + +```text +$ rg -n -A 8 '^ shared_preferences_foundation:' pubspec.lock +407: shared_preferences_foundation: +408- dependency: "direct overridden" +409- description: +410- name: shared_preferences_foundation +411- sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" +412- url: "https://pub.flutter-io.cn" +413- source: hosted +414- version: "2.5.6" +``` + +确认缓存包完整: + +```text +$ find "$HOME/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6" -name PrivacyInfo.xcprivacy -print +/Users/jianghaifeng/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/Resources/PrivacyInfo.xcprivacy +``` + +### 25.3 依赖树原始输出 + +```text +$ flutter pub deps --style=compact | grep shared_preferences_foundation +- shared_preferences 2.5.6 [flutter shared_preferences_android shared_preferences_foundation shared_preferences_linux shared_preferences_platform_interface shared_preferences_web shared_preferences_windows] +- shared_preferences_foundation 2.5.6 [flutter shared_preferences_platform_interface] +``` + +解读:`shared_preferences 2.5.6` 与 override 后的 +`shared_preferences_foundation 2.5.6` 兼容,无需降级主插件。 + +### 25.4 iOS 编译验证 + +为排除先前版本的构建缓存,先清理再重新解析: + +```text +$ flutter clean +Cleaning Xcode workspace... 7.2s +Deleting build... 482ms +Deleting .dart_tool... 24ms +Deleting ephemeral... 2ms +Deleting Generated.xcconfig... 0ms +Deleting flutter_export_environment.sh... 0ms +Deleting .flutter-plugins-dependencies... 0ms +``` + +```text +$ flutter pub get +! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available) +Got dependencies! +``` + +完整无签名设备构建: + +```text +$ flutter build ios --debug --no-codesign +Warning: Building for device with codesigning disabled. You will have to manually codesign before deploying to device. +Building com.stormg.lumadramv for device (ios)... +Running Xcode build... +Xcode build done. 22.1s +✓ Built build/ios/iphoneos/Runner.app +``` + +解读:Swift Compiler 编译通过,不再出现 `Unexpected , separator`。 +本轮未运行真机,也未执行签名安装。 + +### 25.5 Git + +```text +2c30f83 chore(ios): 固定 shared_preferences_foundation 2.5.6 +19babb8 merge: 合并 iOS shared_preferences 编译修复 +a4415c8..19babb8 develop -> develop +``` + +该 chore 提交只包含 `pubspec.yaml` 与 `pubspec.lock`,K9 修改未混入; +author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 + +### 25.6 用户后续验证 + +1. 拉取最新客户端 `develop` 并执行 `flutter pub get`。 + 预期:终端显示 `shared_preferences_foundation 2.5.6 (overridden)`。 +2. 从 Xcode 16.2 直接 Run 或执行 `flutter run` 到 iPhone。 + 预期:Swift Compiler 不再报 `Unexpected , separator`,App 可完成构建启动。 +3. 打开设置或任何依赖 `shared_preferences` 的页面,修改语言或登录态。 + 预期:偏好设置读写正常。 + +设备侧构建日志、启动截图与偏好读写结果由用户执行。 + +--- + +## 2026-10-17 K8 登录页视觉重设计 + +### 23.1 范围、分支与计划 + +叙述:按 INBOX K8 条目重设计登录页视觉,不改 AuthLogic、校验、AuthController、 +接口、登录/注册流程或其他页面。客户端从 `develop 6cc5bfc` 创建 +`feature/flutter-k8-auth-visual`;FruitServer 从 K8 任务书提交 `c33e6cd` +创建 `docs/k8-auth-visual`,只改 `docs/plans/`。 + +开工前先写并提交计划: + +```text +e7c11c3 docs(plans): 增加 K8 登录页视觉实施计划 +``` + +实施文件: + +- `lib/pages/auth/auth_view.dart` +- `lib/pages/auth/auth_widgets.dart` +- `lib/pages/auth/auth_form_widgets.dart` +- `test/auth/auth_visual_test.dart` + +`auth_view.dart` 初次实现达到 654 行,触发项目单文件 500 行红线。随后按页面 +组合、卡片/分段控件、表单控件与品牌组件拆分,最终行数: + +```text +259 lib/pages/auth/auth_view.dart +183 lib/pages/auth/auth_widgets.dart +363 lib/pages/auth/auth_form_widgets.dart +311 test/auth/auth_visual_test.dart +``` + +解读:所有新增 Dart 文件均低于 500 行,页面状态类低于 300 行。 + +### 23.2 先红:视觉测试 + +叙述:先新增 `test/auth/auth_visual_test.dart`,未改既有 +`test/auth/auth_page_test.dart`。 + +```text +$ flutter test test/auth/auth_visual_test.dart +Expected: exactly one matching candidate + Actual: Found 0 widgets with key [<'auth-brand'>]: [] + +Bad state: No element + +The finder "Found 0 widgets with key [<'auth-mode-sign-up'>]: []" +could not find any matching widgets. + +Expected: exactly one matching candidate + Actual: Found 0 widgets with key [<'auth-error-capsule'>]: [] + +00:01 +2 -4: Some tests failed. +``` + +解读:品牌区、220ms 入场动画、pill 分段控件和错误胶囊尚不存在,红灯与 K8 +新增视觉范围一致;既有功能型交互仍可运行。 + +### 23.3 绿灯:视觉与交互实现 + +实现后的新增与既有 auth 用例: + +```text +$ flutter test test/auth/auth_visual_test.dart test/auth/auth_page_test.dart +00:01 +15: All tests passed! +``` + +覆盖内容: + +- 深色品牌页、`AppColors.ink`、顶部克莱因蓝渐变透明度不超过 0.18。 +- 48px 蓝色圆角品牌方块、App 名和印尼市场 tagline。 +- `AppColors.card`、20px 圆角、1px `outlineVariant` 边框、20px 内边距。 +- 52px 输入框、14px 圆角、`AppColors.bar` 填充、邮箱/锁前缀图标和聚焦 + 1.5px 主色边框。 +- 52px 主按钮、26px 圆角、`AppColors.blue`/`bluePressed`、16/w700 + 文字与 18px loading。 +- pill 登录/注册分段控件、选中主色底、切换调用现有 `toggleMode()`。 +- 220ms 淡入上滑、错误浅红胶囊、44pt 以上点击区域。 +- 邮箱→密码焦点流转、密码显隐、busy 禁用、Haptics 和 autofill 均保留。 + +既有 `auth-switch-mode` Key 与旧测试所需文本兼容入口保留,但用户可见切换控件 +已改为 pill 分段;旧用例没有删改。 + +### 23.4 全量门禁与构建 + +构建前检查并发进程: + +```text +$ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew' +``` + +输出仅有当前检查命令自身,没有 Flutter run/build 进程后开始门禁。 + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:19 +292 ~1: All tests passed! +``` + +解读:292 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的 +`[WatchRecordsController]` 堆栈来自既有故意注入失败的测试。 + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... + 38.2s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +门禁执行期间 Flutter 工具自动改写 `pubspec.lock` 的包镜像和新版 SDK 传递依赖; +该文件不属于 K8 范围,已单独还原,未进入提交。 + +### 23.5 Git + +```text +45c3c64 feat(flutter): 重设计登录页视觉 +99f64ab merge: 合并 K8 登录页视觉重设计 +6cc5bfc..99f64ab develop -> develop +``` + +修复提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 23.6 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 拉取最新客户端 `develop`,冷启动并进入登录页。 + 预期:全屏深色背景,顶部只有克制的蓝色光晕,品牌方块、App 名和印尼 + tagline 清晰可见。 +2. 观察表单卡片和输入框。 + 预期:卡片为深色圆角面板;输入框高 52、圆角 14,邮箱和锁图标存在。 +3. 点击邮箱输入框并切换到密码框。 + 预期:聚焦输入框出现 1.5px 克莱因蓝边框,图标高亮;键盘 Next 会跳到密码, + Done 会提交。 +4. 点击登录/注册 pill 两段。 + 预期:选中段蓝底白字,标题、副标题和按钮文案同步切换。 +5. 点击密码右侧眼睛按钮。 + 预期:密码明文/密文切换,按钮点击区域不小于 44pt。 +6. 输入错误邮箱或不安全密码并提交。 + 预期:卡片内出现浅红胶囊错误提示,文案与当前语言一致。 +7. 在 iPhone SE 尺寸打开键盘并滚动。 + 预期:无横向溢出,提交按钮可滚动到键盘上方并点击。 +8. 登录/注册提交时观察按钮与触觉。 + 预期:按钮显示 18px 白色 loading 环,输入与分段切换禁用;设备支持触觉时 + 有一次 light impact。 + +设备侧截图、日志与最终视觉验收结论由用户执行。 + +--- + +## 2026-10-17 K9 首页 2 列全部剧目 + +### 24.1 范围、分支与计划 + +叙述:按 INBOX K9 条目移除首页「继续观看」,在分类 chips 下方展示当前分类的 +全部剧目。只改发现页、三语 l10n、首页相关测试与 `docs/plans/`;不改接口、 +数据模型、轮播、通知或其他页面。 + +- 客户端:从 `develop 99f64ab` 创建 `feature/flutter-k9-home-grid`。 +- 文档:从 K9 任务书 `b434f65` 创建 `docs/k9-home-grid`。 + +开工前先写并提交计划: + +```text +496d273 docs(plans): 增加 K9 首页剧目网格实施计划 +``` + +### 24.2 先红:首页网格测试 + +叙述:先新增 `test/discover/home_all_dramas_grid_test.dart`,未改实现与既有测试。 + +```text +$ flutter test test/discover/home_all_dramas_grid_test.dart +Expected: no matching candidates + Actual: Found 1 widget with text "Continue Watching" + +Found 0 widgets with key [<'home-drama-card-city-lights-demo'>]: [] + +Found 0 widgets with text "All Dramas": [] + +00:02 +0 -5: Some tests failed. +``` + +解读:继续观看仍存在,「全部剧目」标题、2 列网格和卡片 Key 尚不存在,红灯与 +K9 范围一致。 + +### 24.3 实现与定向绿灯 + +`discover_view.dart` 改为外层 `CustomScrollView`,首页 sliver 顺序为: +轮播、分类与标题、剧目网格、底部留白。网格代码拆到: + +- `lib/pages/discover/widgets/home_drama_grid.dart` + +关键参数: + +```text +crossAxisCount: 2 +mainAxisSpacing: 16 +crossAxisSpacing: 12 +childAspectRatio: 2 / 3 +horizontal padding: 16 +``` + +卡片使用 `RemoteImage`、14px 圆角、底部深色渐变;标题 2 行、类型 1 行。 +点击直接调用现有 `logic.openCard(...)`。分类数据直接使用 +`CategoryController.visibleDramas`。 + +新增三语 `allDramas` 并执行 `flutter gen-l10n`: + +```text +$ rg -n "allDramas" lib/l10n +lib/l10n/app_localizations_en.dart:161: String get allDramas => 'All Dramas'; +lib/l10n/app_zh.arb:58: "allDramas": "全部剧目", +lib/l10n/app_en.arb:58: "allDramas": "All Dramas", +lib/l10n/app_localizations_zh.dart:158: String get allDramas => '全部剧目'; +lib/l10n/app_id.arb:58: "allDramas": "Semua Drama", +lib/l10n/app_localizations_id.dart:161: String get allDramas => 'Semua Drama'; +``` + +定向绿灯: + +```text +$ flutter test test/discover/home_all_dramas_grid_test.dart +00:01 +5: All tests passed! +``` + +覆盖:无继续观看、全部剧目标题、2 列 delegate、左右/行列间距、2:3 封面、 +分类切换、无剧目空态、分类缺失空态、失败重试与 320px 小屏。 + +文件规模: + +```text +487 lib/pages/discover/discover_view.dart +167 lib/pages/discover/widgets/home_drama_grid.dart +245 test/discover/home_all_dramas_grid_test.dart +``` + +解读:改造前 `discover_view.dart` 为 649 行;删除继续观看旧实现并拆分网格后 +降到 500 行以内。首页源码已无 `continueWatching`、`_continueTile`、 +`_ShowcaseCard`、`_cardsFor` 或嵌套 `GridView`。 + +### 24.4 既有壳层断言更新 + +`test/widget_test.dart` 原有 5 处断言首页出现 `Continue Watching`。K9 明确要求 +该文案彻底消失,因此该断言与 `find.text('Continue Watching'), findsNothing` +不可能同时成立。 + +处理:保留全部既有用例、测试名称和导航流程,仅把 5 处已作废的可见文案断言 +原位改为 `All Dramas`;没有删除测试用例。 + +```text +$ flutter test test/widget_test.dart +00:05 +17: All tests passed! +``` + +### 24.5 全量门禁与构建 + +首次静态分析发现新增测试有一个未使用 import: + +```text +warning - test/discover/home_all_dramas_grid_test.dart:10:8 +Unused import: 'package:fruitshorts/core/data/repositories/catalog_repository.dart'. +1 issue found. +``` + +移除后: + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! +``` + +```text +$ flutter test +00:18 +297 ~1: All tests passed! +``` + +解读:297 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的 +`[WatchRecordsController]` 堆栈来自既有故意注入失败的测试。 + +构建前检查: + +```text +$ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew' +``` + +输出仅有当前检查命令自身,没有并发 Flutter run/build 后执行: + +```text +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 8.4s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +### 24.6 Git + +```text +cf03178 feat(flutter): 首页改为两列全部剧目网格 +2c381be merge: 合并 K9 首页全部剧目网格 +99f64ab..2c381be develop -> develop +``` + +功能提交与合并提交 author/committer 均为 +`Zhou Weidong <502333560@qq.com>`。 + +### 24.7 用户真机验证步骤与预期 + +以下设备侧取证由用户执行,Codex 未跑真机。 + +1. 使用最新客户端 `develop` 进入首页「Home/发现」tab。 + 预期:页面不再出现「继续观看」标题或横向卡片。 +2. 向下查看分类 chips。 + 预期:chips 下方显示当前语言对应的「全部剧目」/ `All Dramas` / + `Semua Drama`。 +3. 观察列表布局。 + 预期:一行 2 个剧目,左右各 16,列间距 12、行间距 16;封面为 2:3 圆角。 +4. 向下滚动长列表。 + 预期:继续按 2 列懒加载,滚动中无整页白屏或卡顿。 +5. 依次点击「为你推荐 / 热门 / 最新 / 服务端分类」。 + 预期:网格内容随分类更新;「为你推荐」显示去重后的全部剧目。 +6. 点击任意剧目卡片。 + 预期:沿用原详情流程打开对应剧目。 +7. 在有长标题或空分类数据时检查卡片。 + 预期:标题最多 2 行、类型最多 1 行,不横向溢出;空态和失败重试文案保留。 + +设备侧截图、日志与最终验收结论由用户执行。 diff --git a/fruitshorts-app-api/src/main/resources/application.yml b/fruitshorts-app-api/src/main/resources/application.yml index c2d1f4c..ae8eefe 100644 --- a/fruitshorts-app-api/src/main/resources/application.yml +++ b/fruitshorts-app-api/src/main/resources/application.yml @@ -43,6 +43,10 @@ springdoc: path: /v3/api-docs fruitshorts: + auth: + # 仅作 sys_config 缺失/非法时的兜底;运行时优先读取后台「系统配置」 + register-max-per-ip-per-day: ${REGISTER_MAX_PER_IP_PER_DAY:5} + register-max-per-device-per-day: ${REGISTER_MAX_PER_DEVICE_PER_DAY:3} interaction: # MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工 # PRE_REVIEW :全部先审后发(生产建议) diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java index bc075e2..f3f3e4d 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java @@ -4,6 +4,7 @@ package com.fruitshorts.app.service; import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ServiceException; import com.fruitshorts.framework.security.JwtTokenProvider; +import com.fruitshorts.system.service.ConfigService; import com.fruitshorts.user.dal.entity.AppUser; import com.fruitshorts.user.dal.entity.AppUserCredential; import com.fruitshorts.user.dal.entity.AppUserOtp; @@ -25,6 +26,7 @@ import com.fruitshorts.user.vo.TokenRespVO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; import java.time.Duration; import java.time.LocalDateTime; @@ -57,6 +59,7 @@ class AppAuthServiceTest { private CredentialHasher hasher; private JwtTokenProvider tokenProvider; private PasswordEncoder passwordEncoder; + private ConfigService configService; private AppAuthService authService; @BeforeEach @@ -70,8 +73,11 @@ class AppAuthServiceTest { hasher = mock(CredentialHasher.class); tokenProvider = mock(JwtTokenProvider.class); passwordEncoder = mock(PasswordEncoder.class); + configService = mock(ConfigService.class); + given(configService.get(anyString(), anyString())) + .willAnswer(invocation -> invocation.getArgument(1)); authService = new AppAuthService(userMapper, credentialMapper, sessionMapper, consentMapper, - deviceMapper, otpService, hasher, tokenProvider, passwordEncoder); + deviceMapper, otpService, hasher, tokenProvider, passwordEncoder, configService); } @Test @@ -164,6 +170,7 @@ class AppAuthServiceTest { @Test void should_reject_register_when_ip_reaches_the_daily_limit() { + setRegisterLimits(5, 3); given(userMapper.selectCount(any())).willReturn(5L); assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8")) @@ -173,6 +180,62 @@ class AppAuthServiceTest { .hasMessageContaining("当前网络注册过于频繁"); } + @Test + void should_reject_register_when_device_reaches_the_daily_limit() { + setRegisterLimits(5, 3); + given(userMapper.selectCount(any())).willReturn(3L); + + assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8")) + .isInstanceOf(ServiceException.class) + .satisfies(ex -> assertThat(((ServiceException) ex).getCode()) + .isEqualTo(ErrorCode.TOO_MANY_REQUESTS.getCode())) + .hasMessage("该设备注册过于频繁,请稍后再试"); + } + + @Test + void should_fallback_to_default_limit_when_config_is_not_numeric() { + ReflectionTestUtils.setField(authService, "maxRegisterPerIpPerDay", 2); + given(configService.get("app.register.max-per-ip-per-day", "2")).willReturn("invalid"); + given(userMapper.selectCount(any())).willReturn(2L); + + assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8")) + .isInstanceOf(ServiceException.class) + .satisfies(ex -> assertThat(((ServiceException) ex).getCode()) + .isEqualTo(ErrorCode.TOO_MANY_REQUESTS.getCode())) + .hasMessageContaining("当前网络注册过于频繁"); + } + + @Test + void should_fallback_to_default_limit_when_config_is_missing() { + given(userMapper.selectCount(any())).willReturn(5L); + + assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8")) + .isInstanceOf(ServiceException.class) + .satisfies(ex -> assertThat(((ServiceException) ex).getCode()) + .isEqualTo(ErrorCode.TOO_MANY_REQUESTS.getCode())) + .hasMessageContaining("当前网络注册过于频繁"); + } + + @Test + void should_not_limit_by_ip_when_ip_limit_is_zero() { + stubHappyRegister(); + setRegisterLimits(0, 3); + given(userMapper.selectCount(any())).willReturn(2L); + + assertThat(authService.register(emailRegister("abcd1234", null), "203.0.113.8").getAccessToken()) + .isEqualTo("access-1"); + } + + @Test + void should_not_limit_by_device_when_device_limit_is_zero() { + stubHappyRegister(); + setRegisterLimits(5, 0); + given(userMapper.selectCount(any())).willReturn(4L); + + assertThat(authService.register(emailRegister("abcd1234", null), "203.0.113.8").getAccessToken()) + .isEqualTo("access-1"); + } + @Test void should_return_tokens_when_password_matches() { stubLogin(credential("bcrypt-hash"), user(AppUserStatus.NORMAL)); @@ -265,7 +328,14 @@ class AppAuthServiceTest { given(otpHasher.hashOtp(any())).willReturn("hash-of-something-else"); AppOtpService realOtpService = new AppOtpService(otpMapper, mock(OtpSender.class), otpHasher); return new AppAuthService(userMapper, credentialMapper, sessionMapper, consentMapper, - deviceMapper, realOtpService, hasher, tokenProvider, passwordEncoder); + deviceMapper, realOtpService, hasher, tokenProvider, passwordEncoder, configService); + } + + private void setRegisterLimits(int perIp, int perDevice) { + given(configService.get("app.register.max-per-ip-per-day", "5")) + .willReturn(String.valueOf(perIp)); + given(configService.get("app.register.max-per-device-per-day", "3")) + .willReturn(String.valueOf(perDevice)); } private RegisterReqVO emailRegister(String password, String code) { diff --git a/fruitshorts-module-user/pom.xml b/fruitshorts-module-user/pom.xml index 43bf348..6773145 100644 --- a/fruitshorts-module-user/pom.xml +++ b/fruitshorts-module-user/pom.xml @@ -21,5 +21,9 @@ com.fruitshorts fruitshorts-framework + + com.fruitshorts + fruitshorts-module-system + diff --git a/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java b/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java index ca862cc..e0f8d56 100644 --- a/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java +++ b/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ServiceException; import com.fruitshorts.framework.security.JwtTokenProvider; +import com.fruitshorts.system.service.ConfigService; import com.fruitshorts.user.dal.entity.AppUser; import com.fruitshorts.user.dal.entity.AppUserConsent; import com.fruitshorts.user.dal.entity.AppUserCredential; @@ -25,6 +26,7 @@ import com.fruitshorts.user.vo.LogoutReqVO; import com.fruitshorts.user.vo.RegisterReqVO; import com.fruitshorts.user.vo.TokenRespVO; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DuplicateKeyException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -58,6 +60,15 @@ public class AppAuthService { private final CredentialHasher hasher; private final JwtTokenProvider tokenProvider; private final PasswordEncoder passwordEncoder; + private final ConfigService configService; + + /** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */ + @Value("${fruitshorts.auth.register-max-per-ip-per-day:5}") + private int maxRegisterPerIpPerDay = 5; + + /** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */ + @Value("${fruitshorts.auth.register-max-per-device-per-day:3}") + private int maxRegisterPerDevicePerDay = 3; @Transactional(rollbackFor = Exception.class) public TokenRespVO register(RegisterReqVO req, String clientIp) { @@ -301,10 +312,9 @@ public class AppAuthService { return resp; } - /** 每个 IP 24 小时内最多注册几个账号 */ - private static final int MAX_REGISTER_PER_IP_PER_DAY = 5; - /** 每台设备 24 小时内最多注册几个账号 */ - private static final int MAX_REGISTER_PER_DEVICE_PER_DAY = 3; + private static final String REGISTER_IP_LIMIT_KEY = "app.register.max-per-ip-per-day"; + private static final String REGISTER_DEVICE_LIMIT_KEY = "app.register.max-per-device-per-day"; + private static final int PASSWORD_MIN_LENGTH = 8; private static final int PASSWORD_MAX_LENGTH = 64; private static final Pattern HAS_LETTER = Pattern.compile("[A-Za-z]"); @@ -328,7 +338,7 @@ public class AppAuthService { } /** - * 注册频控(邮箱免码注册必须带的兜底):同 IP 24h ≤5 个、同设备 24h ≤3 个。 + * 注册频控(邮箱免码注册必须带的兜底):阈值由配置提供,≤0 时跳过对应限制。 * * 直接数 `app_user` 上的 register_ip / register_device_id,不新建计数表: * 注册量本来就低,这两列也已经有数据,省一张表与一套清理任务。 @@ -336,26 +346,40 @@ public class AppAuthService { */ private void guardRegisterRisk(String clientIp, String deviceId) { LocalDateTime since = LocalDateTime.now().minusDays(1); + int ipLimit = registerLimit(REGISTER_IP_LIMIT_KEY, maxRegisterPerIpPerDay); + int deviceLimit = registerLimit(REGISTER_DEVICE_LIMIT_KEY, maxRegisterPerDevicePerDay); String ip = AppIdentifier.nullSafe(clientIp); - if (StringUtils.hasText(ip)) { + if (ipLimit > 0 && StringUtils.hasText(ip)) { Long byIp = appUserMapper.selectCount(Wrappers.lambdaQuery() .eq(AppUser::getRegisterIp, ip) .ge(AppUser::getCreatedAt, since)); - if (byIp != null && byIp >= MAX_REGISTER_PER_IP_PER_DAY) { + if (byIp != null && byIp >= ipLimit) { throw new ServiceException(ErrorCode.TOO_MANY_REQUESTS, "当前网络注册过于频繁,请稍后再试"); } } String device = AppIdentifier.nullSafe(deviceId); - if (StringUtils.hasText(device)) { + if (deviceLimit > 0 && StringUtils.hasText(device)) { Long byDevice = appUserMapper.selectCount(Wrappers.lambdaQuery() .eq(AppUser::getRegisterDeviceId, device) .ge(AppUser::getCreatedAt, since)); - if (byDevice != null && byDevice >= MAX_REGISTER_PER_DEVICE_PER_DAY) { + if (byDevice != null && byDevice >= deviceLimit) { throw new ServiceException(ErrorCode.TOO_MANY_REQUESTS, "该设备注册过于频繁,请稍后再试"); } } } + private int registerLimit(String key, int defaultValue) { + String raw = configService.get(key, String.valueOf(defaultValue)); + if (!StringUtils.hasText(raw)) { + return defaultValue; + } + try { + return Integer.parseInt(raw.trim()); + } catch (NumberFormatException ex) { + return defaultValue; + } + } + private AppUser createUser(RegisterReqVO req, String clientIp) { AppUser user = new AppUser(); user.setUid(hasher.newUid()); diff --git a/sql/data-phase1.sql b/sql/data-phase1.sql index 8c980c4..023c9c0 100644 --- a/sql/data-phase1.sql +++ b/sql/data-phase1.sql @@ -324,3 +324,12 @@ CROSS JOIN sys_menu m WHERE r.code = 'super_admin' AND m.id IN (31, 32, 33) ON CONFLICT DO NOTHING; + +-- ============================================================ +-- K16:注册频控后台配置 +-- ============================================================ +-- 0 = 不限制;生产建议 IP 5 / 设备 3。 +INSERT INTO sys_config (id, config_key, config_value, remark) VALUES + (3, 'app.register.max-per-ip-per-day', '0', '0=不限制;生产建议 5'), + (4, 'app.register.max-per-device-per-day', '0', '0=不限制;生产建议 3') +ON CONFLICT (config_key) DO NOTHING;