Files
FruitServer/docs/plans/INBOX.md
T

2724 lines
182 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# INBOX — 给 Codex 的指令队列(FruitServer 仓库内)
> **Codex 每次开始任务前必须先读本文件**(2026-10-16 起指令通道从 specs 仓库迁到本仓库)。
> 按条目顺序执行,处理完的条目标记 `[已处理]`。指令来源:项目经理(Trae)。有异议在条目下留言,不要静默跳过。
>
> **规范以 specs 仓库为准**(`AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`、`docs/01~20`、`docs/plans/codex-研发任务清单.md`)——
> specs 现在**只拉取、由项目负责人更新**,我们不再往它提交。工作留痕写在本仓库 `docs/plans/PROGRESS.md`。
> ⚠️ **双 Agent 协作红线(Trae + Codex 共守)**
> 1. **单一写者**:同一时刻只有一个 agent 在改代码/写 PROGRESS/跑门禁。
> 2. **留痕**:谁干活谁写 `docs/plans/PROGRESS.md`(命令 + 原始输出 + 解读,逐动作追加)。Trae 接手时用「【Trae 接手】」标记。
> 3. **git 不冲突**:提交前先 `git status`/`git log`;只 `git add` 自己的文件,禁止 `git add -A`/`.`;不夹带对方改动。
> 4. **文件不覆盖**:写 INBOX/PROGRESS 前先读最新,追加不覆盖。
> 5. **求助通道**:遇到阻塞/不确定,在本文件顶部写「【待 Trae 协助】+ 问题描述」。
> 6. **禅道同步**:任务「开始/完成」时同步更新禅道任务状态。
> 7. **验收归用户**:每批做完**停在干净交接点**,由用户亲自验证;回执必须含「用户可照做的验证步骤 + 每步预期结果」。
> 8. **门禁(不达标不提交)**:后端 = `mvn test` 全过(核心接口须有契约测试);Web = `pnpm build:pro` 通过 + eslint 零错误;Flutter = `dart analyze lib test` 零告警 + `flutter test` 全过。
> 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-19 | K21 登录页 UI 优化
**用户指令(2026-10-19)**:「现在主要做登录页面 ui 优化」。
> 只改客户端 `ReelShort` 登录/注册页(`lib/pages/auth/**`)及其 l10n;不改接口、后端、Web、specs、main。
> 主题继续使用辣椒红暖色与 K20 的 `AppDimens` token。
### 一、现状问题
- 品牌区是左上角小横排,缺少登录页应有的品牌感;页面整体顶对齐,大屏下方留白过多。
- 返回按钮独占一行,推高内容且与品牌区挤在一起。
- 登录/注册共用一句 `authSubtitle`,注册态缺少针对性说明。
- 注册态没有密码规则提示(文案已有 `authPasswordHint`,但只作为输入框 hint)。
- 本地校验失败只有一个错误胶囊,出错字段没有红框定位。
- `_submit()` 没有 `busy` 短路,密码框 IME 回车可绕过禁用态重复提交;失败分支 `setState` 缺 `mounted` 判断。
### 二、改造项
1. **布局重排**(`auth_view.dart`)
- 页面改为「可垂直居中、键盘弹起可滚动」:用 `LayoutBuilder` + `SingleChildScrollView`(保留 Key `auth-scroll`)
+ `ConstrainedBox(minHeight: 可视高度 - padding)` + 居中内容,最大宽度仍 440。
- 返回按钮改为顶部浮层(`Positioned` + `SafeArea`,Key `auth-back` 保留),不再占据内容流;
品牌区与卡片整体垂直居中。
- 保留 `auth-background-glow` 主光晕(每个颜色 alpha ≤ 0.18),新增一层金色/暖色极淡光晕(alpha ≤ 0.06,独立 Key)。
2. **品牌区重做**(`auth_widgets.dart`)
- `_AuthBrand` 改为居中竖排:品牌图标 + `appName`(26/w800)+ `authBrandTagline`(14,`blueSoft`)。
- 品牌图标改用真实资源 `assets/icon/splash_mark.png`(`ClipRRect` 圆角 14 + 辣椒红柔光阴影),
**Key `auth-brand-mark` 必须保留且尺寸仍为 48×48**(既有测试断言)。
3. **卡片与文案**
- 卡片结构保持:模式切换 → 标题(25/w800)→ 副标题 → 字段 → 错误胶囊 → 提交按钮。
- 副标题按模式区分:登录用现有 `authSubtitle`;注册新增 `authSignUpSubtitle`(三语):
- en: `Create an account to keep your list, history, and progress.`
- zh: `创建账号,保存你的片单、观看记录和进度。`
- id: `Buat akun untuk menyimpan daftar, riwayat, dan progresmu.`
- 注册态在密码框下增加一行 12px 的 `authPasswordHint` 辅助说明;登录态不显示。
4. **字段级错误定位**(`auth_logic.dart` + `auth_form_widgets.dart`)
- `AuthLogic` 增加 `AuthFieldError? fieldError`;`validate()` 失败时记录具体字段错误,`onChanged` 时清除。
- 邮箱/密码 `InputDecoration` 增加 `errorBorder` / `focusedErrorBorder`(`colorScheme.error`,宽度 1.5),
出错字段显示红框;**错误胶囊 `auth-error-capsule` / `auth-error` 及其文案、圆角 12、alpha 0.1 全部保留**。
5. **交互健壮性**(`auth_view.dart`)
- `_submit()` 开头 `if (_logic.busy) return;`;`catch` 中 `setState` 前判断 `mounted`;
`onSubmitted` 在 busy 时忽略。其余提交与导航行为不变。
6. **视觉细节**
- 输入框聚焦时前缀图标保持 `blueSoft`(现状),提交按钮增加极轻的辣椒红阴影(不改背景色断言)。
- 页面底部保留 `28 + viewInsets.bottom` 的内边距,确保小屏 + 键盘下提交按钮可达。
- 不引入新依赖、不新增图片资源。
### 三、硬约束(既有测试不得改断言)
`test/auth/auth_visual_test.dart` 与 `test/auth/auth_page_test.dart` 必须原样通过,重点保留:
- `auth-brand`、`auth-brand-mark` 尺寸 `48×48`、品牌名与 tagline 文案;
- `auth-background-glow` 渐变每个颜色 alpha ≤ 0.18;
- `auth-card` 圆角 20、边框色 `outlineVariantDark`、宽度 1;
- 输入框高度 52、圆角 14、背景 `AppColors.bar`、聚焦边框 `blue` / 1.5;
- 提交按钮高度 52、圆角 26、背景 `blue` / 按下 `bluePressed`;
- `auth-enter-animation` 时长 ≤220ms、tween 0→1;
- 模式切换 Key、选中色 `blue`、标题文案 `Welcome back` / `Create your account`;
- 隐藏兼容文案 `New here? Create an account` / `Already have an account? Sign in` 必须仍可被 `find.text` 找到;
- 错误胶囊 Key、圆角 12、alpha 0.1、错误图标与语义色;
- 小屏 320×568 + 键盘 260 时 `auth-submit` 可滚动到并可点击。
**测试失败时改实现,不改测试。** 允许新增测试覆盖注册副标题、密码提示与字段红框。
### 四、门禁
`dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
真机验收由用户执行,Codex 不做设备侧取证。
### 五、Git 与回执
- 从干净 `develop`(基线 `e44fbdf`)拉 `feature/flutter-k21-auth-ui`,小步提交,完成后
`--no-ff` 合并回 develop 并 push;不动 main;作者统一 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K21.md`;留痕写 `PROGRESS.md` 并单独提交 FruitServer develop。
- 回执:布局前后差异、三语文案、保留的 Key/尺寸清单、测试条数与门禁原始输出、真机验收步骤。
---
## [已处理·待用户验收] 2026-10-19 | K20 客户端视觉统一(辣椒红主题)
**用户指令(2026-10-19)**:「保留辣椒红,先做 K20 视觉统一」。
> 只改客户端 `ReelShort` 的视觉与布局;不改业务逻辑、接口调用、后端、Web、specs、main。
> 主题口径已拍板:**保留现有辣椒红暖色**(`AppColors.blue = #C4322A`),不恢复克莱因蓝。
### 一、目标
消除跨页面视觉不一致与明显不合理的紧凑布局,统一颜色、字号、间距、圆角、导航、卡片、按钮与进度条;
不改任何接口行为,不改既有测试断言。
### 二、统一 token(新增 `lib/core/theme/app_dimens.dart`)
- 页面左右边距:`16`(详情页 24 → 16;Auth 20 → 16)。
- 卡片:外层圆角 `16`、内边距 `16`;列表内嵌小块圆角 `12`、内边距 `12`;胶囊圆角 `20`。
- 字号档位:caption `12`、body `14`、列表标题 `15`、区块标题 `16`、页面标题 `20`(w800);
导航标签 `11`。**禁止再出现 9.5 / 10px 的业务文案**。
- 主按钮最小高度 `48`(整页主操作 `52`);点击区不小于 `44×44`。
- 主色只用 `AppColors`;新增 `AppColors.favorite`(收藏红心)如需保留粉色。
### 三、改造项(按文件)
1. **底部导航合一**:新增 `lib/core/widgets/app_bottom_nav.dart`,参数化 tab(图标/文案/选中);
高 `64`、图标 `22`、标签 `11`、选中态用主色胶囊(以现有 `home_view.dart` 为基准)。
替换 `home_view.dart`、`discover_view.dart`、`mine_view.dart` 三处导航;
`player_view.dart` 的 `_fallbackNav` 若保留也必须复用该组件。**保留现有 Key**(`tab-home` 等)与文案。
2. **账号入口卡重做**:`account_entry_card.dart` 高度 48 → `64`,图标 `16` → `18`,标签 `9.5` → `11`,
允许 2 行;四个入口点击区高度 ≥44;保留 `mine-account-card`、`mine-account-profile/devices/export/deactivate` 五个 Key。
3. **字号下限**:把 `home_drama_grid.dart:146`、`mine_view.dart:144/648/912`、`discover_view.dart:369`、
`home_view.dart:364`、`comment_tile.dart:114`、`watch_records_page.dart:282/296`、
`notices_page.dart:244/260`、`mine_view.dart:591`、`account_devices_page.dart:204` 提升到 ≥12(导航标签 11)。
4. **卡片/间距收敛**:`downloads_page.dart:305`(10)、`library_drama_tile.dart:26`、
`notices_page.dart:217`、`watch_records_page.dart:230`、`downloads_page.dart:200`、
`mine_view.dart:614/802` 统一到 12;`home_drama_grid.dart:104/109`、`mine_view.dart:377`、
`account_devices_page.dart:115`、`account_deactivate_page.dart:93` 统一到 16;
卡片内边距 9/14 收敛为 12/16。`drama_detail_view.dart:147/233` 的 24 → 16。
5. **颜色收敛**:替换 `downloads_page.dart:201/222/224/370`、`watch_records_page.dart:295`、
`mine_view.dart:143/590/637/717/743/828/835`、`favorites_page.dart:117`、
`home_quick_drawer.dart:54/77/118/262`、`player_view.dart:792/872/879` 中的直接 `Color(0x...)`
为最接近的 `AppColors` 常量(必要时新增 `favorite`);`account_devices_page.dart:196-203` 的
`scheme.tertiary` 改 `AppColors.gold` 或 `blueSoft`;`drama_detail_view.dart:169` 的
`onSurfaceVariant` 改 `AppColors.muted`。
6. **进度条统一**:详情续看 `drama_detail_view.dart:366-373`、观看记录 `watch_records_page.dart:267-272`、
下载 `downloads_page.dart:357-362` 统一为高 4、圆角 4、前景 `AppColors.blueSoft`、背景 `Colors.white24`;
Player 主拖动条保留金色。
7. **按钮与对比度**:`app_theme.dart` 增加 `filledButtonTheme.minimumSize = Size(0, 48)` 与
`outlinedButtonTheme`(深色前景用 `AppColors.blueSoft`);`mine_view.dart:390-401` 去掉
`VisualDensity.compact`;`empty_state.dart:150-158` 的重试色由 `scheme.primary` 改 `AppColors.blueSoft`。
8. **标题层级**:页面标题统一 20/w800,区块 16/w700,列表 15/w600-700;
`drama_detail_view.dart:159-164` 的 `headlineLarge` 显式改 24。
9. **深色唯一口径**:`app.dart:221-223` 改 `themeMode: ThemeMode.dark`;移除页面级
`Theme(data: AppTheme.dark())` 的重复包裹(保留根主题),确保日期选择器、弹窗也是深色。
若短期内删除页面级包裹风险大,可保留包裹,但 `themeMode` 必须为 dark。
10. **状态栏与安全区**:根 Scaffold 统一 `AnnotatedRegion<SystemUiOverlayStyle>(light)`,
`appBarTheme.systemOverlayStyle = light`、`systemNavigationBarColor = AppColors.bar`;
`favorites_page.dart:102`、`watch_records_page.dart:118`、`notices_page.dart:118`、
`downloads_page.dart:96`、`account_profile_page.dart:108`、`account_devices_page.dart:93`、
`account_export_page.dart:89`、`account_deactivate_page.dart:73` 的底部 padding
增加 `MediaQuery.paddingOf(context).bottom`(或 `SafeArea(top: false)`)。
11. **详情页返回与封面压暗**:`drama_detail_view.dart:80-107` 的加载/失败分支补带返回按钮的 AppBar;
`SliverAppBar` 封面顶部加黑色渐变,保证返回/更多图标在浅色封面上可读。
12. **选中态可见性**:`player_view.dart:834-845` 点赞/收藏选中时改实心图标
(`Icons.favorite` / `Icons.bookmark`);`discover_view.dart:262-296` 与
`mine_view.dart:537-564` 的选中项补 `Semantics(selected: true)`。
13. **点击区补足**:`discover_view.dart` 的 `_HeaderPill`(403-447)、`_LoginButton`(449-469)
最小高度 44;分类 Chip(262-296)点击区 44(视觉高度可保持 36,用透明 padding 扩大)。
14. **列表占位对齐**:`watch_records_page.dart:208-222` 占位高度与真实卡片一致(114),并复用相同 padding。
### 四、明确不做(留给 K21)
搜索 UI、Android 返回键行为、播放器切集票据失败黑屏、登录后继续原操作、下拉刷新、注销状态闭环、
时长/日期格式统一、语言上送后端。**本批不碰这些逻辑**。
### 五、约束与门禁
- 不改既有测试断言;视觉改动导致测试失败时改实现,不改测试(保留所有现有 Key 与文案)。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 代码规模红线、文档注释、`// Author: fruit.com.cn` 与既有规范不变。
- 真机验收由用户执行,Codex 不做设备侧取证。
### 六、Git 与回执
- 从干净 `develop`(基线 `42ce1d1`)拉 `feature/flutter-k20-visual`,小步提交,完成后
`--no-ff` 合并回 develop 并 push;不动 main;作者统一 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K20.md`;留痕写 `PROGRESS.md` 并单独提交 FruitServer develop。
- 回执:改动清单、统一后的 token 值、测试条数与门禁原始输出、真机验收步骤(逐屏对照色/字/间距)。
### 七、K20 完成回执
- 客户端分支:`feature/flutter-k20-visual`
- 功能末提交:`5f1df40`
- 合并回 develop:`e44fbdf`
- 已推送:`42ce1d1..e44fbdf develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
已完成 14 项改造:统一 token、四处底部导航、账号入口卡 64 高、
字号下限、卡片/间距、颜色、进度条、按钮高度、标题层级、深色主题、
状态栏/安全区、详情返回与封面渐变、选中态、点击区和观看占位高度。
门禁:
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:24 +341: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 7.8s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
既有测试要求的两项例外:Auth 卡圆角保持 20;EmptyState 副标题/按钮保持
12.5/13。两者均提取为 token,未修改测试断言。
---
## [已处理·待用户验收] 2026-10-19 | K19 客户端账号中心:资料 / 设备 / 数据导出 / 注销
**用户指令(2026-10-19)**:「先写 K19 任务书并派单」。
> 只改客户端 `ReelShort`。后端、`ReelShortWeb`、`specs`、`main` 一律不动。
### 一、背景与缺口
后端 `/app` 共 45 个接口,客户端已接 26 个。本轮对接账号中心 6 个(`api-phase1.md` §1.2,第 157–162 行;实现见
`fruitshorts-app-api/.../AppMeController.java`):
| 方法 | 路径 | 客户端现状 |
| --- | --- | --- |
| GET | `/app/me` | 未对接 |
| PUT | `/app/me` | 未对接 |
| GET | `/app/me/devices` | 未对接 |
| GET | `/app/me/export` | 未对接 |
| POST | `/app/me/deactivate` | 未对接 |
| POST | `/app/me/deactivate/cancel` | 未对接 |
### 二、接口与字段口径(以后端代码为准确认,勿臆造)
1. **GET `/app/me`** → `AppUserRespVO`:`uid, nickname, avatarUrl, gender, countryCode, regionCode,
language, timezone, currency, status, maskedTarget, createdAt, lastLoginAt, vipLevel(Integer),
vipExpireAt`。`vipLevel` 是数字,其余时间字段是 `LocalDateTime` 字符串。
2. **PUT `/app/me`** → 请求 `ProfileUpdateReqVO`,全部字段可选,**只传本次改动的字段**:
`nickname, avatarUrl, gender, birthday(yyyy-MM-dd), language, timezone, regionCode`。
`nickname` 为空串时后端忽略;`gender` 取值按 `schema-phase1.sql` 注释用小写
`male / female / unknown`;`birthday` 后端只能设置、不能清空。响应同 `AppUserRespVO`。
3. **GET `/app/me/devices`** → **直接返回 `AppUserDevice` 实体数组**(不是专用 VO):
`id, userId, deviceId, platform, deviceModel, firstSeenAt, lastSeenAt, status, createdAt, updatedAt, deleted`。
按契约 §0,`Long` 字段以字符串下发。客户端**只展示** `deviceId/platform/deviceModel/firstSeenAt/lastSeenAt/status`,
`userId`、`deleted` 一律忽略、不落日志、不上屏。
4. **GET `/app/me/export`** → `{profile: AppUserRespVO, credentials: [{type, masked, verified}], consents: [...], devices: [...]}`
(以 `AppUserService.exportAccount` 实际结构为准)。
5. **POST `/app/me/deactivate`** → 请求 `{reason}`(`password` 后端当前不校验,**客户端不采集、不上送**);
成功返回 `scheduledAt`(15 天冷静期截止时间,`LocalDateTime`)。后端同时吊销全部会话。
已有申请时返回业务错误,文案「注销申请已在处理中」。
6. **POST `/app/me/deactivate/cancel`** → 成功 `data = null`;无待处理申请时错误文案「没有待处理的注销申请」。
**契约缺口(本批如实标注,不得自行补后端)**:
- `GET /app/me` 不返回「注销申请中」状态 → UI 不能做状态判断,只能把后端错误文案原样透传;
「撤销注销」入口常驻,无申请时展示后端返回的文案。
- 无「设备下线 / 踢出」接口 → 设备页**只读**;`TC-ACCOUNT-10` 中「单点下线」记为缺口,留待后续批次。
- 无头像上传接口 → 头像**只展示**(`avatarUrl` 为空用现有占位图标),本批不做上传、不做 URL 输入框。
### 三、客户端改造范围
1. **remote**:新增 `lib/core/data/remote/account_api.dart`,六个方法,路径与请求体严格按上表;
错误一律抛现有 `ApiException`(禁止吞异常、禁止空 catch)。
2. **models**:新增 `account_profile.dart`、`account_device.dart`、`account_export.dart`。
数值/ID 解析用 `json_numbers.dart` 的 `jsonToId` / `jsonToInt`,兼容字符串与数字两种下发形式。
3. **repository**:新增 `account_repository.dart`(接口 + `HttpAccountRepository`);
在 `app.dart` 的 Provider 树注册,供页面与测试注入(`FakeAccountRepository` 放 `test/support/`)。
4. **页面**(建议 `lib/pages/account/`,沿用 provider binding/logic/view 三层):
- **个人资料**:进入时 `GET /app/me`;可改 `nickname`(去空格后非空、≤30 字符,客户端校验)、
`gender`(male/female/unknown,本地化文案)、`birthday`(日期选择,`yyyy-MM-dd`);
`uid / maskedTarget / countryCode / currency / status` 只读展示;`language / timezone / regionCode` 只读。
保存调 `PUT /app/me`,成功后用响应刷新页面,并**同步本地登录态**(见第 6 条)。
- **登录设备**:`GET /app/me/devices`,列表展示平台、机型、设备号(脱敏显示,保留首尾各 4 位)、
首次/最近活跃时间、状态;空态用现有 `EmptyState`;本页只读。
- **数据导出**:`GET /app/me/export`,展示资料摘要、凭据(`type/masked/verified`)、授权记录数、设备数;
提供「复制完整 JSON」到剪贴板(`Clipboard.setData`)+ SnackBar 提示;不写本地文件。
- **注销账号**:15 天冷静期与「申请后立即登出全部设备」的显著提示;可选注销原因(≤200 字);
「申请注销」必须二次确认弹窗 → 成功后展示 `scheduledAt` 并清空本地会话(服务端已吊销全部会话,
用 `AuthController.clear()`,不要再调 logout),回到未登录态;
「撤销注销申请」常驻,成功/失败均以后端文案提示。
5. **入口**:`lib/pages/mine/mine_view.dart` 在已登录态新增「账号」卡(置于钱包卡之后、菜单卡之前):
个人资料 / 登录设备 / 数据导出 / 注销账号,四项带图标与分隔线;未登录不显示。样式沿用现有 `_menuCard`。
6. **登录态同步**:`AuthController` 增加公开方法(如 `applyProfile(AuthUser)`),资料保存成功后更新
内存 session 的 `uid/nickname/avatarUrl` 并持久化,使「我的」页昵称即时生效;不改令牌与刷新逻辑。
7. **l10n**:新增文案必须同时写入 `app_en.arb / app_zh.arb / app_id.arb` 并重新生成;
印尼语为印尼市场主语言,禁止留英文占位。
8. **测试(TDD,先红后绿)**:
- API/模型:六接口路径与请求体、`Long` 字符串/数字两种解析、缺字段容错、错误文案透传;
- logic:资料保存成功后回调登录态、注销成功后清会话、撤销失败展示后端文案、导出复制内容;
- widget:我的页四个入口仅登录态可见、资料页字段回填与保存、设备空态、注销二次确认、
数据导出页加载/失败/重试。
- 既有测试不得改断言;如需适配只改注入的 Fake。
### 四、约束与门禁
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、文件 ≤500 行、嵌套 ≤4、圈复杂度 ≤10;禁止魔法值。
- public 类/方法必须有文档注释,文件首行统一 `// Author: fruit.com.cn`;禁止裸 `then()`、空 catch。
- 门禁(不达标不提交):`dart analyze lib test` 零告警;`flutter test` 全过;
`flutter build apk --debug` 构建通过。
- 真机验证由用户执行,Codex **不得**自行做设备侧验证,也不得用本机 curl 充当设备证据。
### 五、Git 与交接
- 从干净 `develop`(基线 `7d68699`)拉 `feature/flutter-k19-account`;小步提交,
`type(scope): 描述`(≤50 字符),只 `git add` 本次文件。
- 完成后 `git checkout develop && git merge --no-ff feature/flutter-k19-account && git push origin develop`;
**不动 `main`**;提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
- 任务书与留痕属于 FruitServer 仓库:`docs/plans/PROGRESS.md` 追加命令 + 原始输出 + 解读,
单独提交到 FruitServer `develop`(`docs(plans): ...`),不夹带客户端代码。
### 六、回执要求
完成后停住并回执:分支与提交号、六个接口的调用位置、测试条数与结果、`dart analyze` / `flutter test` /
`flutter build apk --debug` 原始结论、已知缺口(设备下线 / 头像上传 / 注销状态查询),
以及「用户可照做的真机验证步骤 + 每步预期结果」。
### 七、K19 完成回执
- 客户端分支:`feature/flutter-k19-account`
- 功能末提交:`7151bc9`
- 合并回 develop:`9b3ab9d`
- 已推送:`7d68699..9b3ab9d develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
六个接口调用集中在 `ReelShort/lib/core/data/remote/account_api.dart`,分别由:
- `AccountProfilePage` 调 GET/PUT `/app/me`
- `AccountDevicesPage` 调 GET `/app/me/devices`
- `AccountExportPage` 调 GET `/app/me/export`
- `AccountDeactivatePage` 调 POST `/app/me/deactivate` 与 `/deactivate/cancel`
门禁原始结论:
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:26 +335: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 10.0s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
已知缺口:
- `GET /app/me` 不返回注销申请状态,UI 不判断状态;撤销失败展示后端文案。
- 没有设备下线接口,登录设备页只读;`TC-ACCOUNT-10` 的“单点下线”未实现。
- 没有头像上传接口,头像只展示;空值使用占位图标。
- 资料响应不含生日,当前只支持设置生日,不能回填或清空。
用户真机验证步骤(设备侧取证由用户执行):
1. 登录后进入「我的」。
预期:钱包卡之后、菜单卡之前显示「账号」卡,包含个人资料 / 登录设备 /
数据导出 / 注销账号四项。
2. 打开「个人资料」。
预期:昵称、性别、UID、脱敏账号、国家、币种、状态、语言、时区、地区回填;
修改昵称与性别后保存,返回「我的」后昵称立即更新。
3. 打开「登录设备」。
预期:设备号脱敏,展示平台、机型、首次/最近活跃时间和状态;无设备时显示空态。
4. 打开「数据导出」。
预期:展示资料摘要、凭据、授权记录数和设备数;点「复制完整 JSON」出现成功提示。
5. 打开「注销账号」。
预期:显示 15 天冷静期与立即登出提示;输入原因后必须二次确认。
6. 确认注销并重新打开 App。
预期:成功页展示预计注销时间,本地会话已清空,App 回到未登录态。
7. 重新登录后再次进入「注销账号」,点击「撤销注销申请」。
预期:有申请时显示撤销成功;无申请时原样展示后端“没有待处理的注销申请”。
---
## [已处理·待用户验收] 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 <device> --dart-define=API_BASE_URL=http://127.0.0.1:8081`;
- iOS:`flutter run -d <udid> --dart-define=API_BASE_URL=http://<MacIP>: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<String>`,无分页、无回复、无删除、无举报;
- 分享走 [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 <device-id> --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 <haifeng_hello@163.com>`),已推 `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 <device> --dart-define=API_BASE_URL=http://<IP>: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) 全局修改,统一契约」**。
**背景(两方独立复现同一根因,故现在铺全局)**:
- 你在 I1 里实测到:UI 点「下架」后端回 `200002 公告不存在` —— 库里 `2111011206851596290`,浏览器 `JSON.parse` 后变 `2111011206851596300`,回查自然找不到。你把修复**限定在本轮两个模块**并留了待裁定。
- PM 独立复核同样确认:新注册用户 id `2111012753262120962` 由 `/admin/app-users/page` **以裸 JSON 数字**下发;用真 JS 引擎验证 `JSON.parse('{"id":2111012753262120962}').id` → `2111012753262121000`(**不相等**)。根因是 JS `Number.MAX_SAFE_INTEGER = 9007199254740991`,19 位雪花 id 必然超出;而服务端**没有全局 `Long→String` 序列化配置**。
- 种子数据都是小 id(4/5/25)所以老页面一直正常 —— 这是**只要"新建后立刻拿 id 操作"就必然踩**的坑。
### 一、后端:全局序列化(放 `fruitshorts-framework`,两个入口都生效)
1. `Long.class` 与 `Long.TYPE` 一律用 `ToStringSerializer`。
2. ⚠️ **不要动 `Integer` / `int`**(分页 `pageIndex`/`pageSize` 等保持数字)。
3. 必须覆盖:普通字段、**嵌套对象**、`List`/`Map` 的值、分页 `list`。
4. **反序列化方向也要验**:请求体 / 路径变量 / 查询参数里 id 传**字符串**要能正确绑定成 `Long`(前端回传的就是字符串)。
### 二、契约统一(`docs/api-phase1.md` §0 通用约定)
- 写明「**所有 ID 字段一律以字符串下发**」(含主键与外键 id),并给示例。
- 把 I1 里逐模块的描述(商品/公告 VO 各自加 `ToStringSerializer`)**收拢到这条总口径**,不要各写一套。
- 顺手把 §2.11/§2.12 里与总口径重复的表述改成引用 §0。
### 三、两个前端适配
1. **ReelShortWeb**:审计现有页面有没有把 id 当**数字**用 —— `parseInt(id)` / `Number(id)` / `id + 1` / 拿 id 当数组下标 / `===` 与数字比较 / `sort((a,b)=>a.id-b.id)`,逐处改成字符串语义。本轮新加的两个页面已按 `string` 写,**别回退**。
2. **ReelShort(Flutter)**:`fromJson` 里 `json['id'] as int` 这类写法在字符串下发时会**直接抛异常**,必须审计并修。策略二选一(推荐前者,改动小且向前兼容):
- **兼容解析**:统一按"接受字符串或数字"的方式解析(如 `int.tryParse(v.toString())`),或把模型里的 id 保留为 `String`;
- 或全量改成 `String`(改动更大,要与客户端同事对齐)。
- ⚠️ 客户端现由另一同事在推进:**只做"ID 解析"这一类最小改动**,不要顺手改别的;有分歧或改不动就写「【待 Trae 协助】」,并在回执里给出**逐文件清单**。
### 四、验证(必须有证据,不接受"应该没问题")
1. 后端契约测试:`Long` 字段确实以**字符串**输出(含嵌套 / 分页 list / Map)。
2. **端到端复现 I1 那个 bug 并证明已消失**:admin 新建一条记录 → 用响应里的 id 调详情 / 编辑 / 上下架 → **成功**(全局范围内,不再只限商品/公告)。
3. **JS 侧一致性证据**:拿真实响应喂 `node -e 'const d=JSON.parse(...); console.log(String(d.id))'`,证明与库里的值**逐位相等**。
4. C 端:注册 / 播放票据 / 订单等返回的 id 也是字符串,且**客户端解析不崩**(Flutter 侧跑测试)。
### 五、门禁与 git
- `mvn test` 存量 **110 条不得跑红** + 新增测试;Web `pnpm ts:check` + eslint 零错误 + `pnpm build:pro` 通过;若改了客户端则 `dart analyze lib test` 零告警 + `flutter test` 全过。
- FruitServer / ReelShortWeb / ReelShort **各自在 `develop` 提交并推送**;**绝不动 `main`**。
- 新写文件首行 `// Author: fruit.com.cn`(Vue/TS 同;Dart 用 `// Author: fruit.com.cn`)。
### 六、节奏
先写 `FruitServer/docs/plans/2026-10-16-J1.md`(方案 + 影响面清单 + 风险)→ 做 → 跑门禁 → **停在干净交接点写回执**,回执必须含:
- 每个仓库改了哪些文件(**客户端逐文件清单**要单独列)
- 「用户可照做的验证步骤 + 每步预期结果」
- 若客户端有未改完/有分歧的部分,**如实列出**(不许含糊)
---
## [已处理·待用户验收] 2026-10-16 17:30 | I1 后台管理端:商品(金币包)配置 + 系统公告管理(后端接口 + Web 页面)
**用户指令(2026-10-16 17:10)**:「现在做后台管理端,规范还是按照 specs」+「ReelShortWeb 就是这个项目」→ **后台管理端 = `ReelShortWeb`(Vue3 + Element Plus),本批后端接口与页面一起做**。
### 一、PM 已勘察的两个缺口(实测证据,直接用)
| # | 缺口 | 证据 |
| --- | --- | --- |
| 1 | **商品/金币包配置:后台完全没有入口** | `product` 表 **16 个字段**已建(`code/name/type/platform/store_product_id/coin_amount/bonus_coin/price_amount/currency/display_price/region_codes/vip_days/sort/status/effective_at/expire_at`)、**已有 3 行种子数据**、C 端 `/app/orders/verify` 依赖 `store_product_id` 对齐商店商品(错误码 `200002 该商品未在后台配置` 就是为它留的);但 admin-api 29 个 Controller 里**没有任何商品 CRUD 接口**(`grep` 只命中 `WalletAdminController /coins` 的发币补偿)→ 运营改商品只能改库。**PRD §09 明确要求「商品配置与价格分层」** |
| 2 | **系统公告:有读没写** | `ops_notice` 表 **0 行**;E1 已交付 C 端 `GET /app/notices`(匿名只读);契约 §2.7 原话「**系统公告 `ops_notice` 目前没有产出方,等有公告功能再接**」→ 后台没有公告管理接口,运营发不了公告 |
> 除这两项外,admin-api 对照契约 §2.1–§2.10 全部已实现,**不要重做**。
### 二、后端(FruitServer / admin-api)
1. **契约先行**(依据 `12-上线检查清单` §一):先在 `docs/api-phase1.md` 写清两个模块的接口、字段、错误码、权限点(放在 §2.6 订单与权益之后,或新增一节),**再动代码**。
2. **商品配置 `/admin/products`**:
- `GET /admin/products/page`(筛选:`type` / `status` / `platform` / `keyword`,分页沿用既有口径)、`GET /admin/products/{id}`
- `POST /admin/products`、`PUT /admin/products/{id}`、`PUT /admin/products/{id}/status`(上下架)、`DELETE /admin/products/{id}`
- 字段按 `product` 表现有结构,**不要改表**
- **必须有校验**:`code` 在全局唯一、`store_product_id` 在**同一 `platform` 下唯一**(否则同一商店商品被重复配置 → 发币口径混乱);`type=COIN` 时 `coin_amount > 0`;`type=VIP` 时 `vip_days > 0`
- ⚠️ **VIP 商品口径**:B1-1 已定「VIP 订阅本轮不做」→ 允许建但**不允许启用**(启用报明确错误)或直接拒绝创建,**二选一由你定,写进契约并在回执里说明**;拿不准就写「【待 Trae 协助】」
- 权限点命名沿用现有习惯(参考 `commerce:*`),并落 `sys_permission` + `sys_menu`
3. **系统公告 `/admin/notices`**:
- `GET /admin/notices/page`、`GET /admin/notices/{id}`、`POST`、`PUT /admin/notices/{id}`、`PUT /admin/notices/{id}/status`(发布/下架)、`DELETE /admin/notices/{id}`
- 字段按 `ops_notice` 表现有结构,**不要改表**
- **与 C 端 `GET /app/notices` 的筛选口径必须严格对齐**(E1 已定:`status=1` 且 `target ∈ {ALL, USER}`、`publish_at <= now`、`expire_at` 为空或未过期、按 `publish_at desc, id desc`)—— 后台发布后 C 端能查到、过期/下架后查不到
- 权限点 + 菜单同上
4. **菜单与权限点落库**:写进 `sql/data-phase1.sql`(**幂等**),注意**沿用既有保留段**、不要与现有 id 冲突(现有菜单到 id 30)。建议:商品配置挂 `订单与权益`(id 22) 下;系统公告可挂新的运营分组,具体由你按现有约定定。**超管必须可见且可授权**。
5. Swagger 中文注解按 S1 规范补齐(`@Tag` / `@Operation`)。
6. **门禁**:`mvn test` 存量 **91 条不得跑红** + 新增契约测试,至少覆盖:`store_product_id` 同平台唯一、`code` 唯一、`type` 校验、公告发布后 C 端可见 / 过期或下架后不可见、越权(无权限点被拒)。
### 三、后台页面(ReelShortWeb)
> **挂载机制(PM 已勘察)**:路由是**静态注册**在 `src/router/index.ts`(`component: () => import('@/views/...')`),而菜单树来自 DB `sys_menu`,其中 `component` 字段必须**与视图路径一致**(例:`views/contentManagement/DramaManagementView`)。两边都要加。
1. 新增两个页面(放在与现有页面同级的目录,命名沿用 `XxxManagementView.vue` 风格):
- **商品配置**:列表(分页/筛选)+ 新建/编辑弹窗 + 上下架 + 删除;表单字段按 `product` 表;金额/金币用数字输入并做前端校验
- **系统公告**:列表 + 新建/编辑(标题/正文/类型/目标/发布时间/失效时间)+ 发布/下架 + 删除
2. **按现有约定写**:API 封装放 `src/api/admin/`(新增如 `product.ts` / `notice.ts`);权限码走现有的 `can(code)`;**表格状态列用接口返回的 `status` / `statusText`,不要在前端写死中文**;大数值用 `formatCount`。
3. `src/router/index.ts` 静态注册路由(`route_name` 与 DB `sys_menu.route_name` 一致,**注意不要与既有 route_name 重名** —— 之前踩过 `CreatorCenter` 重名导致白屏/404 的坑)。
4. **不要改动已对接的既有页面**(避免与其他人正在做的改动冲突)。
5. **门禁**:`pnpm build:pro` 通过 + eslint 零错误。
### 四、git
- **FruitServer**:在 `develop` 提交并推送;**不动 `main`**。
- **ReelShortWeb**:现在是 `main` 分支且**与远端分叉**(本地 `4564aa8` 领先 1、远端 `1fd0b8b 全量补充作者标识` 领先 1)。请先 `git fetch` 并把远端合进来,**然后按 specs 的 `GIT_WORKFLOW` 新建 `develop` 分支,在 `develop` 上提交并 `git push origin develop`**;**不动 `main`**(合并走 PR、由用户操作)。远端是 `jianghaifeng/ReelShortWeb.git`(有写权限)。
- 作者的署名规范:`// Author: fruit.com.cn`(Vue/TS 文件首行)。
### 五、节奏(红线第 7 条)
1. 先写计划文件 `docs/plans/2026-10-16-I1.md`(后端 + 前端两部分、步骤、风险、待确认)。
2. 做,跑门禁。
3. **停在干净交接点写回执**:逐项列出改了什么;**给你和用户可照做的验证步骤 + 每步预期结果**(含「后台登录 → 菜单里能看到这两个新页面 → 建一个商品 → 用该商品在 C 端 verify 能对上」这样的端到端链路)。
4. **不要连着抢做下一批。**
---
### 回执(Codex,2026-10-16):I1 完成,停在干净交接点
**提交**:FruitServer `8d19a0c`(develop,已推) + ReelShortWeb `db313f4`(develop,已推,含 husky 门禁)。
**留痕**:`docs/plans/PROGRESS.md`(本轮新建,逐动作命令 + 原始输出 + 解读);计划 `docs/plans/2026-10-16-I1.md`。
**没碰 `main`**;01-08 冻结模块未改(改动只在 module-commerce / module-system / admin-api / app-api 的 `AppNoticeService`)。
**门禁数字(原始输出见 PROGRESS 动作 3/6/10)**
| 门禁 | 结果 |
| --- | --- |
| `mvn test` | **BUILD SUCCESS**,commerce 33 / admin-api 16 / app-api 49 / worker 12 = **110 条全过**(存量 91 + 新增 19,无跑红) |
| `mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests` | BUILD SUCCESS |
| `sql/data-phase1.sql` 幂等 | 连跑三遍,`sys_role_menu/permission/menu/role_permission` 行数 54/52/33/79 前后一致 |
| `pnpm ts:check` | 0 错误 |
| `eslint --ext .js,.ts,.vue ./src` | 0 error(首遍 1 个 prettier 折行,改后复跑为空) |
| `pnpm build:pro` | Build successful |
| `:3005` 真浏览器 | 两页渲染正常;1440/1280/1024 三档 `overflowX=false`;截图见下面步骤 |
**交付内容**
1. 后端:`/admin/products` 6 个接口(page/{id}/POST/PUT/{id}/status/DELETE)、`/admin/notices` 6 个接口;字段严格按 `product` / `ops_notice` 表现有结构,**未改表**。
2. 校验:`code` 全局唯一、`storeProductId` **同 platform 唯一**、`COIN_PACK` 需 `coinAmount>0`、`VIP` 需 `vipDays>0`、新建默认下架。
3. 公告可见性与 C 端**共用一份实现**:`OpsNoticeService.pageForApp(...)`,后台与 `/app/notices` 不再各写一套条件。
4. 权限点 `product:list/product:update/notice:list/notice:update` + 菜单 31(商品配置,挂 22「订单与权益」)/ 32(运营管理)/ 33(系统公告),超管已授权,幂等。
5. 前端:`src/api/admin/product.ts`、`src/api/admin/notice.ts`、`ProductManagementView.vue`、`NoticeManagementView.vue`;状态列只用接口 `statusText`,不写死中文。
**两个自己拍的口径(请复核)**
1. **VIP 商品:允许建档、不允许启用** —— `status=1` 时回 `400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)`。理由:上线日可以直接上下架,不必临时补商品数据;已写进契约 §2.11。
2. **⚠️ 雪花 id 以字符串下发(本轮实测抓到的真缺陷)** —— UI 上点「下架」后端回 `200002 公告不存在`,根因是 S0 换雪花主键后 id 超过 JS 安全整数范围:DB 里 `2111011206851596290`,浏览器 `JSON.parse` 后变成 `2111011206851596300`,回查自然找不到。
修复限定在本轮两个模块(`ProductRespVO` / `NoticeRespVO` 的 `id` 加 `ToStringSerializer`,新建接口返回 `String`,前端 `id: string`),**没动全局序列化、没动 app-api**,避免影响 Flutter 客户端;并补了两条防回归测试。
- 遗留:admin-api 其它模块(订单/权益/短剧…)只要"新建后立刻用 id 操作",同样会中招。**是否统一做"admin-api 的 Long → String"、app-api 要不要一起动(Flutter 解析 id 的地方会受影响)——这是平台级改动,请裁定**,我没自行铺开。
3. **菜单挂载机制与条目描述有出入(如实报出)**:实测菜单来自 DB `sys_menu`,前端用 `import.meta.glob` 按 `component` 字符串动态解析;`src/router/index.ts` 的 `asyncRouterMap` 只静态声明 dashboard/content/user/system,commerce/governance/board 都不在里面。所以本轮**没有改 `src/router/index.ts`**,只建页面 + 落菜单(`component` 与视图路径一致)。若你要求"两边都登记",我补一版。
**用户可照做的验证步骤(每步都有预期结果)**
准备(后端):`cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests`,然后
```
java -jar fruitshorts-admin-api/target/fruitshorts-admin-api-1.0.0-SNAPSHOT.jar # 8080
java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar # 8081
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v3/api-docs # 预期 200
```
数据库若在干净机器上:`psql -f sql/data-phase1.sql`(可重复执行)。
1. **菜单**:`pnpm dev` → 打开 `http://127.0.0.1:3005/#/login` → `admin / admin123` 登录。
预期:左侧出现「订单与权益 → 商品配置」与「运营管理 → 系统公告」,其余菜单不变。
2. **商品页**:进 `#/commerce/products`。预期:3 行种子(coin_100/550/1200),列 = 商品编码/名称/类型/平台/商店商品 id/到账金币/价格/排序/状态/创建时间/操作;状态显示「已上架」。
3. **新建商品(正常路径)**:点「新建商品」→ 编码 `uitest_coin_500`、名称随意、平台选 Google Play、商店商品 id `UITEST_COIN_500`、金币 500、赠送 50、价格 9900 → 保存。
预期:toast「商品已创建(默认下架,请确认后再上架)」,列表新增一行且状态「已下架」(**id 是雪花 id,注意这是本轮修过的点**)。
4. **异常路径**:再新建一个同编码商品 → 预期 toast「商品编码已存在:uitest_coin_500」;把类型改成「会员(第二阶段)」、天数 30 → 保存成功(**允许建档**),再点它的状态想上架 → 预期 toast「会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)」,状态不变。
5. **C 端对齐(命令)**:把第 3 步的商品点上架,然后
```
psql -c "select id, code, platform, store_product_id, status from product where store_product_id='UITEST_COIN_500';"
```
预期:查到 1 行、`status=1` —— 这正是 `/app/orders/verify` 找商品的同一条件。
(`POST /app/orders/verify` 本机默认 `STORE_VERIFIER=disabled`,会先回 `200003 商店校验尚未配置`,这是 B1-1 的既定行为,不是商品没配上。)
6. **公告页**:进 `#/operation/notices` → 「新建公告」填标题/正文(默认「全部用户/系统」、不填时间=立即发布)→ 保存。
预期:toast「公告已发布」,行内状态「已发布」、失效时间显示「-」。
```
curl -s "http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=5" # 预期 total 1,能看到刚发的标题
```
7. **下架(异常路径的对照组)**:点该行状态「已发布」→ 预期 toast「公告已下架」、行内变「已下架」;再跑上面的 curl → 预期 `total 0`。
8. **越权**:用 `content.editor01 / admin123`(内容运营,无 `product:*`/`notice:*`)登录后直接敲 `#/commerce/products`。
预期:页面 403/无权限(后端 `{"code":403,"message":"无权限"}`);不带 token 请求 `curl -s "http://127.0.0.1:8080/admin/products/page"` → `{"code":401,...}`。
9. **清理**:把第 3/4 步的商品、第 6 步的公告删掉;核对 `select count(*) from product;` = 3、`select count(*) from ops_notice;` = 0。
(本轮自测数据我已清干净:`product` 3 行种子、`ops_notice` 0 行。)
**门禁/流程说明**:本轮所有子步骤都在同一批内完成,未抢做下一批;`mvn test` 与 `package` 都是先停服务再打包、打包后重新起服务、**对着活着的服务**做的 curl 自测(不是对停掉的服务打)。
服务当前状态:8080/8081 我这边是**已起的**(自测完成时仍在监听);若你这边连不上就是被环境回收了,按上面「准备」两条命令起回来即可。
**禅道同步(红线第 6 条,如实说明)**:我把 `水果短剧APP`(项目 2)的 4 个执行下的任务都拉了一遍,
任务只有 S0/S1/S0-16、A1–A6、B1/B2、C1–C3 —— **没有 I1(后台管理端 商品配置 + 系统公告)这一条**,
所以本轮没有可同步的任务号,我没有动任何既有任务的状态(避免误改 B1/C1/A* 这些别的批次)。
下一步请给 I1 建任务并告知任务号,我补一次状态同步(`codex01` 的 token 已可用)。
---
### 回执(Codex,2026-10-16):J1 完成,停在干净交接点
**提交**(三个仓库各自 `develop`,`main` 未动)
- FruitServer `f35db2c`(develop,已推):全局 Long→String + 契约 §0 + 测试
- ReelShortWeb `3ec9ea6`(develop,已推):4 处 id 数字语义适配
- ReelShort `94ff9f1`(**新建 develop**,已推):客户端 Long 计数兼容解析
**留痕**:`docs/plans/PROGRESS.md`(本轮追加 J1 段,含原始输出);计划 `docs/plans/2026-10-16-J1.md`。
**门禁数字**
| 门禁 | 结果 |
| --- | --- |
| `mvn test` | **BUILD SUCCESS** — commerce 33 / admin-api 21 / app-api 49 / worker 12 = **115 条全过**(存量 110 未跑红,新增 5) |
| `mvn … package -DskipTests` | BUILD SUCCESS |
| `pnpm ts:check` / eslint / `pnpm build:pro` | 0 错误 / 0 error / Build successful |
| `dart analyze lib test` / `flutter test` | No issues found! / All tests passed!(+167) |
**每个仓库改了什么**
FruitServer(后端)
- 新增 `fruitshorts-framework/.../web/JacksonLongToStringConfig.java`(`Long`/`long` → 字符串;
`PageResult.total` 用 mixin 保持数字;`PlainLongSerializer` 供 mixin 使用)
- `docs/api-phase1.md`:§0 新增「ID 字段一律以字符串下发」总口径(含示例、边界、`total` 例外),
§2.11/§2.12 改为引用 §0
- I1 的特例回退:`ProductRespVO` / `NoticeRespVO` 去掉 `@JsonSerialize`,
`ProductAdminController` / `NoticeAdminController` 的 `create` 回到 `Result<Long>`
- 测试:新增 `admin-api/.../web/LongToStringSerializationTest.java`(5 条);
`ControllerContractSupport`(admin + app 两处)挂上模块,避免契约测试假绿;
C 端 4 条 id 断言改成 `value("…") + isString()`
ReelShortWeb(4 个文件)
- `src/utils/creatorImpersonation.ts` — 代看创作者 id 全程字符串(原来 `Number(raw)`,19 位会被四舍五入)
- `src/views/contentManagement/EpisodeManagementView.vue` — `Number(route.query.drama)` → 字符串比较
- `src/views/creator-center/composables/useDramaEdit.js` — `Number(route.params.id)` → 字符串比较
- `src/views/permission/composables/useMenuManagement.js` — 顶级菜单哨兵 `0` → `'0'`(与接口的 `"0"` 对齐)
ReelShort(Flutter)逐文件清单(**只有这些,没夹带别的**)
- `lib/core/data/models/json_numbers.dart`(新增)— `jsonToInt`:接受数字或数字字符串
- `lib/core/data/models/auth_session.dart` — `expiresIn`(后端 `long`)改用 `jsonToInt`
- `lib/core/data/models/drama.dart` — `viewCount`(后端 `Long`)两处改用 `jsonToInt`
- `test/core/json_id_parsing_test.dart`(新增)— 8 条回归用例
**三个需要你确认的口径**
1. **`PageResult.total` 保持数字(唯一例外)** —— 它是条数不是 id,前端分页组件(el-pagination / Flutter 分页)
都要数字。实测 `total = 2 int`。若你要求"Long 一律字符串",删掉 `JacksonLongToStringConfig` 里那一行 mixin 即可。
2. **反向(请求方向)未做限制**:请求体 `"id":"…"`、路径变量、查询参数都接受字符串并转 `Long`(有测试)。
`Integer`/`int`(`pageIndex`/`pageSize`/`Result.code`/`totalEpisodes`/`durationSec`…)保持数字 —— 实测已确认。
3. **客户端适配范围比"只改 id 解析"宽了一点点,原因是物理上必须**:客户端 id 本来就是
`(json['id'] ?? '').toString()`,不会崩;真正会崩的是被全局规则扫到的 **Long 计数**,
所以改了 `expiresIn` 与 `viewCount` 共 3 处(`json_numbers.dart` 是新增工具)。
其余 8 处 `as num?` 对应的 Java 字段都是 `Integer`/`int`,**按最小改动原则没动**。
⚠️ **ReelShort 仓库原来只有 `main`**(无 develop),我按条目要求新建了 `develop` 并在上面提交。
客户端由另一位同事推进,若他继续在 `main` 上走,需要把这笔 `94ff9f1` 带过去。
**用户可照做的验证步骤(每步都有预期结果)**
准备:`cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests`,起两个 jar;
`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/v3/api-docs` → 200。
1. **复现 PM 那条(最直观)**:注册一个 C 端账号
```
curl -s -X POST http://127.0.0.1:8081/app/auth/register -H 'Content-Type: application/json' \
-d '{"targetType":"EMAIL","target":"你的邮箱@example.com","password":"Passw0rd!23","nickname":"手测","deviceId":"manual-1"}'
```
预期:`"expiresIn": "7200"`(字符串)。
2. **用响应里的 user id 调后台**:`curl -s "http://127.0.0.1:8080/admin/app-users/page?pageSize=50" -H "Authorization: Bearer <admin token>"`,
拿新用户的 `id`(预期是 `"2111…"` 这种**带引号的字符串**、`total` 是**不带引号的数字**),再
`curl -s "http://127.0.0.1:8080/admin/app-users/<id>" -H "Authorization: Bearer <token>"` → 预期 `code=0`、`data.id` 与列表一致
(**修复前这里返回 `200002 数据不存在`**)。
3. **JS 逐位一致**:把第 2 步的响应存成 `r.json`,然后
```
node -e 'const d=JSON.parse(require("fs").readFileSync("r.json","utf8"));console.log(String(d.data.id))'
```
预期与 `psql -c "select id::text from app_user where uid='<uid>'"` **逐位相同**。
对照:`node -e 'console.log(String(JSON.parse("2111015748376858626")))'` → `2111015748376858620` 附近(会被改写),说明精度问题真实存在。
4. **后台界面(:3005)**:`admin/admin123` 登录 → 商品配置新建一个商品 → **点它那一行的状态/编辑**,
预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是 `0`。
5. **短剧管理 → 剧集管理带参跳转**:从短剧管理点某剧的「剧集」,URL 带 `?drama=<id>`,
预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。
6. **客户端**:`cd ReelShort && flutter test`(预期 All tests passed);有设备时 `flutter run` 打开首页看播放量/详情
不报解析异常(后端 `viewCount` 现在下发字符串)。
7. **异常路径**:请求体里 id 传**数字**(老客户端行为)也应被接受:`PUT /admin/products/<id>/status?status=1` 用数字 id 路径同样 200。
**禅道**: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`)并叫停。
- 因此**在用户重跑之前,本批一律按「未验证」计**;上面那次的历史输出只作参考,不作为结论。