# 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 | K23 后台视觉深化:主色、菜单、编辑弹窗与锁屏
**用户指令(2026-10-19,附三张截图)**:「现在着重优化 UI 及交互」。
截图问题:
1. 左侧菜单子项无图标、缩进/行高/选中态不统一;
2. 短剧编辑弹窗里封面直接暴露超长 URL,数字输入框布局松散;
3. 锁定屏幕弹窗标题被分隔线穿过、右上角图标错位、缺少取消入口。
> 只改 `ReelShortWeb` 视觉/交互 + `FruitServer` 菜单图标种子 SQL;不改接口契约、后端业务、客户端、main。
### 一、主色统一为品牌辣椒红
- `src/styles/business/theme.css` 的 `--el-color-primary` 由 `#409eff` 改为 `#c4322a`,
同步 `light-3/5/7/8/9`、`dark-2`(参考值:`#d3615b / #e19995 / #f0c2c0 / #f5d9d8 / #faeeed / #9d2822`)。
- 按钮、开关、链接、选中菜单、标签页激活态、焦点环统一走 `var(--el-color-primary)`;
检查白字/红底、红字/白底对比度 ≥ 4.5:1;成功/警告/危险语义色保持不变。
- 登录页品牌色已用 `--app-brand`,保持不变;全站不再出现默认 Element 蓝。
### 二、左侧菜单(口径更正:子菜单统一不用图标)
> 用户 2026-10-19 更正:**不是子菜单图标为空,而是子菜单统一不要图标;当前问题是没对齐、视觉不好看。**
> 不要给子菜单加图标,也不要在子菜单预留图标位置。
1. **子菜单图标一律清空**:
- `sys_menu` 中所有 `type='MENU'` 的子菜单 `icon` 置为空串(含 id 4/5/7/8/9/10/11/12/13/14/16/18/19/21/23/25/27/29/30/31/33);
- 同步 `sql/data.sql` 与 `sql/data-phase1.sql`,并实际执行到开发库;
- 菜单管理页保存子菜单时也不落图标(或保存后清空),避免以后又出现图标;
- 渲染层对嵌套菜单项强制不渲染图标(即使库里有历史值也只显示文字)。
2. **对齐与视觉**(新增 `src/styles/business/menu.css`,作用域 `#v-menu`):
- 一级菜单:行高 44px,左侧 padding 16px,图标 18px,图标与文字间距 8px;
- 二级菜单:行高 40px,**文字左边缘与一级菜单文字左边缘对齐**(即一级 padding + 图标 18 + 间距 8 = 42px),
不出现“子项文字比父项文字更靠左/更靠右”的错位;
- 二级菜单不渲染图标,也不保留空白图标位;选中/悬浮背景从文字左边缘开始,圆角 8px;
- 一级/二级的选中态、悬浮态、展开箭头、折叠态、popper 子菜单保持一致;
- 深色模式选中态用深主色底 + 白字,浅色模式浅主色底 + 主色文字;行距紧凑,不出现大块空白。
### 三、短剧编辑弹窗(`DramaManagementView.vue`)
1. 弹窗宽 680px,`close-on-click-modal=false`,内容区可滚动、底部按钮固定;
表单分三块:`基础信息`(名称/类型/简介)、`付费设置`(免费集数/单集解锁金币/是否热门)、`封面`。
2. 数字输入:`el-input-number` 固定宽 180、`controls-position="right"`,两列等宽对齐。
3. **封面编辑器重做**:
- 左侧 2:3 预览(约 96×128),无封面时显示虚线占位;
- 右侧显示状态 chip(已上传/未上传)、文件名(只显示 basename,不显示完整路径)、
`更换封面` / `移除` 操作;上传中显示进度遮罩;
- **不再展示原始 URL 输入框**;如需查看,提供“查看原图”链接在新窗口打开;
- 上传前校验类型(jpg/png/webp)与大小(≤5MB),失败在弹窗内提示并可重试;
- `移除` 只清空本地表单,保存后生效。
4. 表单加 `:model` + `:rules`(名称必填、类型必选),保存时 `validate()`,错误显示在字段下方;
保留现有保存接口与字段,不改 payload。
5. 保存按钮 loading/disabled 防重复(已有 `saving`,补齐禁用与关闭保护)。
### 四、锁定屏幕弹窗(`LockDialog.vue`)
- 宽度 440,`max-height` 调整到 320–360,去掉全屏按钮(该弹窗不需要全屏)。
- 重做头部:标题左对齐、无穿过的分隔线、关闭按钮右对齐;标题下加一行说明“输入密码解锁当前会话”。
- 内容居中:头像 64、姓名 14、密码框 100% 宽;密码框支持回车提交。
- 底部:`取消`(关闭)+ `锁定`(primary,宽度不小于 96)。
- 删除 `console.log('set: ', val)`,不改变锁定逻辑。
### 五、交互与一致性
- 弹窗统一:标题/内容/底部间距、圆角、按钮高度(36/40)、暗色模式可读。
- 破坏性操作不新增逻辑;本批只做视觉与上述表单交互。
- 不修改接口、不新增权限点、不改业务流程。
### 六、验收与门禁
- `pnpm lint:eslint` 零错误;`pnpm build:pro` 通过。
- 开发库子菜单 `icon` 已全部置空;`GET /admin/auth/menus` 自检:一级菜单有图标,二级菜单 icon 为空。
- 浏览器自查(不作为用户验收):登录页/工作台/短剧管理/锁定屏幕;1024 与 375 宽度;
暗色/浅色;控制台 0 error、0 404。
- 用户浏览器验收:菜单图标与行距、红色主色、短剧编辑弹窗封面、锁屏弹窗。
### 七、Git 与回执
- `ReelShortWeb` 从干净 `develop`(基线 `3e8312e`)拉 `feature/web-k23-admin-polish`;
`FruitServer` 只改菜单图标 SQL 种子,单独提交并 push。
- 完成后各自 `--no-ff` 合并回 `develop` 并 push;作者 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K23.md`;留痕写 `PROGRESS.md`。
- 回执:主色变量、子菜单图标落库结果、弹窗改动、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 2026-10-19 | K22.1 后台收尾:登录文案、图标残留与文件清理
**用户反馈(2026-10-19,附登录页截图)**:
1. 「这个下面的文字可以不要」——登录页标题下方的说明段落与三个标签(短剧与剧集 / 发布状态 / 角色权限)删掉。
2. 「还有图标没有改过来」——仍有旧品牌图/旧图标来源:浏览器 favicon、首屏 loading 的 `/logo.png`、mock 路由里的旧图标、在线图标开关。
3. 「有些文件该删除的删除掉」——删除已确认无引用的文件与 mock 体系。
> 只改 `ReelShortWeb`;不改接口契约、后端业务、客户端、main。用户直接在浏览器验收。
### 一、登录页文案
- `src/views/Login/Login.vue` 删除 `
` 中的说明段落 `
{{ t('login.message') }}
`
与 `
` 三个标签;保留 `hero-kicker` 与 `h1`。
- 删除随之不再使用的 l10n key(`login.message` / `login.pointDrama` / `login.pointPublish` / `login.pointAccess`)
与对应样式;若 key 仍被其他页面引用则保留并说明。
### 二、图标与品牌残留(重点)
1. **favicon**:用客户端品牌图替换 `public/favicon.ico`:
- 复制 `src/assets/imgs/brand-mark.png` 为 `public/brand-mark.png`;
- `index.html` 改 ``;
- 删除 `public/favicon.ico`。
2. **首屏 loading**:`index.html` 的 `
` 改为 `/brand-mark.png`,加圆角;
首屏背景改为客户端深色 `#100C0A`、标题文字浅色,避免闪白。
删除 `public/logo.png`。
3. **在线图标开关**:`.env.base`、`.env.dev`、`.env.pro`、`.env.test` 的 `VITE_USE_ONLINE_ICON` 全部改为
`false`,由 `PurgeIcons` 本地打包 Material Symbols,避免在线 API 不可达导致图标空白/旧缓存。
构建后必须确认菜单/顶栏/表格工具栏图标真实渲染。
4. **mock 旧图标**:`.env.dev`、`.env.pro`、`.env.test` 的 `VITE_USE_MOCK` 全部改为 `false`;
删除整个 `mock/` 目录;`vite.config.mts` 移除 `viteMockServe` 的 import 与条件配置。
目的:不再把 `mock/role/index.mock.ts` 里的 `ant-design:` / `ep:` 旧图标带进任何构建。
### 三、文件清理(删除前先全仓 grep 确认零引用)
必须删除:
- `src/assets/imgs/logo.png`(已无引用)
- `src/assets/imgs/avatar.jpg`(UserInfo 已改读 store,无引用)
- `public/logo.png`、`public/favicon.ico`(被 brand-mark 替代)
- `src/views/permission/CustomMenuView.vue`(无路由/菜单/组件引用)
- `src/views/Login/components/RegisterForm.vue`(后台无注册接口,假提交)
- 同时移除 `src/views/Login/components/index.ts` 的导出、`Login.vue` 的注册切换状态与 `LoginForm.vue`
的「还没有账号?注册」入口及 `to-register` 事件;相关 l10n key 若不再使用一并删除。
可选删除(仅在 grep 证明零引用后):
- 其他未挂路由、未被 import 的 demo 页面/组件/资源。
**禁止**删除仍被路由、菜单、组件或构建配置引用的文件;删除清单与 grep 证据写入回执。
### 四、验收
- 全仓扫描结果:旧图标前缀 0、`` 0、`@element-plus/icons-vue` 0、`/logo.png` 0、`mock` 引用 0。
- `pnpm lint:eslint` 零错误;`pnpm build:pro` 通过。
- 用 `pnpm dev`(base 模式)打开登录页与后台:
- 登录 hero 只剩 kicker + 标题;
- 浏览器标签页图标、首屏 loading、侧边栏品牌图三者一致;
- 菜单/顶栏/表格工具栏图标全部为 Material Symbols Rounded 且无空白;
- 控制台无 404(尤其无 `/logo.png`、mock 请求)。
### 五、Git 与回执
- 从干净 `develop`(ReelShortWeb 当前 `5494190`)拉 `feature/web-k22-cleanup`,小步提交,
`--no-ff` 合并回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K22.1.md`;留痕写 `PROGRESS.md`。
- 回执:删除文件清单 + grep 证据、图标/品牌修复、env 变更、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 2026-10-19 | K22 后台管理视觉与交互统一(图标对齐客户端)
**用户指令(2026-10-19)**:「先优化后台管理 UI 及交互;后台管理图标统一成客户端的;严格执行 specs 文档规范」。
> 只改 `ReelShortWeb` 的视觉/交互 + `FruitServer` 的菜单图标种子 SQL;不改接口契约、业务逻辑、客户端、main。
> 用户会直接在浏览器验收结果。
### 一、必读规范(严格执行)
按顺序读:`AGENTS.md` → `CODING_STANDARDS.md` → `GIT_WORKFLOW.md` → `FruitServer/docs/api-phase1.md` §2 →
`docs/plans/codex-研发任务清单.md` → 本 INBOX。
Vue/TS 硬约束:沿用 vue-element-plus-admin 结构;eslint 零错误;`pnpm build:pro` 通过;
新增手写源码首行 `// Author: fruit.com.cn`;函数 ≤50 行、参数 ≤5、类/文件规模红线、禁止魔法值;
提交作者统一 `Zhou Weidong <502333560@qq.com>`。
### 二、图标统一(核心)
**目标**:后台所有图标统一为客户端同款 **Material Symbols Rounded**(客户端使用 Material `*_rounded`),
不再混用 `ep:`、`ant-design:`、`mdi:`、`zmdi:`、`ion:`、`bi:`、`radix-icons:` 与 Element Plus 图标组件。
1. 新增 `src/constants/icons.ts`(语义名 → Iconify 全名)与 `src/components/AppIcon`:
- `AppIcon` 基于现有 `Icon` 组件,默认 `material-symbols:*`,统一尺寸/颜色/描边风格;
- 图标名禁止散落在页面里,统一从 `AppIcons` 取。
2. 菜单图标(`sys_menu.icon`)按以下映射落库,并同步 `FruitServer/sql/data.sql` 与 `sql/data-phase1.sql`:
| 菜单 | id | 新图标 |
| --- | --- | --- |
| 工作台 | 1 / 2 | `material-symbols:dashboard-rounded` |
| 内容管理 | 3 | `material-symbols:video-library-rounded` |
| 后台管理 | 6 | `material-symbols:settings-rounded` |
| 用户管理 | 15 | `material-symbols:group-rounded` |
| 创作者管理 | 17 | `material-symbols:movie-edit-rounded` |
| 内容治理 | 20 | `material-symbols:policy-rounded` |
| 订单与权益 | 22 | `material-symbols:payments-rounded` |
| 数据看板 | 24 | `material-symbols:monitoring-rounded` |
| 创作者中心 | 26 | `material-symbols:video-camera-front-rounded` |
| 运营管理 | 32 | `material-symbols:campaign-rounded` |
3. 更新完 SQL 后**实际执行到开发库**(`psql -f sql/data.sql`、`psql -f sql/data-phase1.sql`,脚本必须幂等),
并用管理员登录后的 `GET /admin/auth/menus` 自检返回的 icon 全部为 `material-symbols:` 前缀。
4. 菜单管理页图标候选改为上述合法 Iconify 名;保存时统一存全名;旧的短名(`Document` 等)渲染时做映射兜底。
5. 顶栏、标签页、表格工具栏、弹窗等共享组件 **以及 `src/views/**` 页面内所有图标**
(含 ``)统一替换为 `AppIcon`;移除页面内对 `@element-plus/icons-vue` 的直接引用。
语义映射至少包含:`menu-rounded` / `menu-open-rounded`、`fullscreen-rounded` / `fullscreen-exit-rounded`、
`format-size-rounded`、`language-rounded`、`dark-mode-rounded` / `light-mode-rounded`、
`refresh-rounded`、`close-rounded`、`chevron-left-rounded` / `chevron-right-rounded`、
`search-rounded`、`restart-alt-rounded`、`view-column-rounded`、`help-rounded`、`add-rounded`、
`edit-rounded`、`delete-rounded`、`upload-rounded`、`download-rounded`、`visibility-rounded`、
`arrow-upward-rounded` / `arrow-downward-rounded`、`check-rounded`、`cancel-rounded`。
6. **品牌图标**:把客户端 `ReelShort/assets/icon/splash_mark.png` 复制为
`ReelShortWeb/src/assets/imgs/brand-mark.png`,侧边栏(展开/收起)与登录页统一使用该图;
不再使用框架默认 logo 或自绘图标。保留原 `logo.png` 文件但不再引用。
### 三、登录页(`src/views/Login/**`)
1. 删除默认账号/密码占位提示(`admin` / `admin123`),改走 i18n 占位文案。
2. “记住我”默认关闭;只记住用户名,**不把密码写入 localStorage**;同步清理 user store 中登录信息的密码字段。
3. 表单支持回车提交;登录/注册按钮在请求与动态路由生成完成前保持 loading,防止重复提交。
4. 窄屏(≤960px)压缩品牌装饰区,保证登录卡在首屏完整可见。
5. 登录页品牌名/副标题与侧边栏共用同一来源(环境变量 + i18n),不再写死 `FruitShorts Studio`。
6. 视觉与客户端一致:深色暖底、辣椒红强调只用于按钮/焦点(**不整体改后台主题色**),品牌 mark 与侧边栏同尺寸同圆角。
### 四、全局壳与主题
1. 修复深色模式:菜单/头部浅色主题下的文字对比度、标签页关闭图标、主题开关轨道/边框、
面包屑当前项颜色;所有颜色走 `var(--el-*)` 或业务 token。
2. 去掉 `body` 的 `min-width: 980px` 强约束,补 1200/768 断点;保证 1024、800、375 宽度下顶栏右侧与内容区不被裁切。
3. 顶栏、标签页、表格工具栏、弹窗图标全部换成 Material Symbols Rounded;同一语义只用一个图标。
4. 清理全局 CSS 冲突:
- 只保留一处 `:root` 字体声明(`--font-display` / `--font-body`);
- 圆角收敛为 `--app-radius-sm/md/lg`(4/6/12),登录卡可单独 18;
- 把硬编码 `#409eff !important` 改为 `var(--el-color-primary)`;
- 删除/改名未使用的 `.app-shell/.sidebar/.main-area/.topbar/.brand/...` 死选择器,避免“改了不生效”。
5. 用户信息区头像/姓名从 `userStore` 读取,不再写死 `avatar.jpg` 与 `Archer`。
6. “清除缓存并重置”增加二次确认。
### 五、明确不做(留给 K23)
业务页的表格/筛选/分页统一、拦截器 reject、删除/上下架二次确认、行内防重复、表单 rules、
权限按钮补齐、上传校验、状态标签/日期格式统一。**本批不碰这些业务交互逻辑**,避免与视觉批次混在一起。
### 六、验收与门禁
- `pnpm build:pro` 通过;`pnpm lint:eslint` 零错误(只跑改动文件也可,但最终全量通过)。
- 开发库菜单 icon 已更新;`GET /admin/auth/menus` 自检全部为 `material-symbols:`。
- 浏览器自查(Codex 自检,不作为用户验收):登录页、侧边栏折叠/展开、顶栏、标签页、
深色/浅色、1024/800/375 宽度;控制台无报错。
- 用户浏览器验收:图标风格、登录页、全局壳、深色模式、窄屏。
### 七、Git 与回执
- `ReelShortWeb` 从干净 `develop`(基线 `5c087f4`)拉 `feature/web-k22-admin-visual`;
`FruitServer` 只改 SQL 种子,单独提交并 push。
- 完成后各自 `--no-ff` 合并回 `develop` 并 push;不动 main。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K22.md`;留痕写 `PROGRESS.md`。
- 回执:图标映射与落库结果、登录页/全局壳改动、扫描前后对比、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 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/尺寸清单、测试条数与门禁原始输出、真机验收步骤。
### 六、K21 完成回执
- 客户端分支:`feature/flutter-k21-auth-ui`
- 功能提交:`8a96f34`
- 合并回 develop:`b1c69e2`
- 已推送:`e44fbdf..b1c69e2 develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
已完成:居中可滚动布局、顶部浮层返回、图片品牌 hero、模式独立副标题、
注册密码规则提示、字段级红框、busy/mounted 保护、双层光晕和提交阴影。
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:24 +343: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 7.5s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
---
## [已处理·待用户验收] 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(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 --dart-define=API_BASE_URL=http://127.0.0.1:8081`;
- iOS:`flutter run -d --dart-define=API_BASE_URL=http://:8081`;
- **debug 下可在「我的 → 设置 → 服务器地址」运行时修改**(K13),换网络不必重编。
6. 环境注意:
- Xcode 16.2 / Swift 6.0.3 与 `shared_preferences_foundation 2.5.6` 的固定原因(2.6.0 需 Swift 6.1);
- iOS SPM 报 `Missing package product` 时的恢复步骤(清 DerivedData、`flutter pub get`、`xcodebuild -resolvePackageDependencies`);
- Android `libdartjni.so` 加载失败时卸载重装。
7. 门禁命令:`dart analyze lib test`、`flutter test`、`flutter build apk --debug`;测试命名约定。
8. 当前边界:下载不落盘/不续传/不离线播放;无搜索接口;通知无已读态。
#### K14-B 新增 `ReelShort/docs/TASKS.md`
表格形式,列:批次 / 范围 / 状态 / 关键提交或契约章节 / 备注。至少覆盖:
- K1 详情+播放+进度+收藏;K2 分类/记录/收藏页/下载授权;K3 评论/点赞/举报/分享;K5 克莱因蓝主题+印尼语;K6 登录页+底部导航+震动;K7 轮播+站内通知;K8 登录页视觉;K9 首页网格;K10 空态;K11 3 列+全部首位;K12 去标题+空态重做;K13 运行时服务器地址。
- 待办:K4 账号剩余(Google 登录需凭据)、B1 支付/金币、搜索(契约缺失)、下载落盘(另立批次)。
- 状态以 `develop` 实际合并情况为准,不确定的标注「需核对」,不要编造提交号。
#### K14-C 新增 `ReelShort/docs/FILES.md`
- 目录树(到两级),每目录一句话职责。
- 关键文件表:`lib/core/config/app_config.dart`、`server_endpoint`(如已实现)、`core/data/remote/*`、`repositories/*`、`services/*`、`core/theme/*`、`core/widgets/empty_state.dart`、`pages/*/`、`l10n/*.arb`。
- `test/` 结构说明(按模块目录)。
- 只写实际存在的文件;生成物(`*.g.dart`、`app_localizations*.dart`)标注为生成、勿手改。
### 三、约束
- 中文书写,简洁准确;不使用 AI 腔(“赋能/一站式/旨在”等)。
- 不泄露任何密钥、账号、服务器凭据。
- 只新增/修改 `README.md`、`docs/TASKS.md`、`docs/FILES.md`;不删既有内容以外的文件。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 四、门禁与 Git
- 文档改动仍需 `dart analyze lib test` 与 `flutter test` 通过(确认未误改代码)。
- 从干净 `develop` 拉 `docs/k14-client-docs`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 在 `FruitServer/docs/plans/PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」,完成后停住写回执。
### 五、K14 完成回执(2026-10-17)
**结论**:客户端 README 已重写,并新增任务清单与文件说明。只改 ReelShort 的
三份文档,未改代码、specs、后端或 Web。
- 文档分支 `docs/k14-client-docs` 从 `f95325d` 完成并 `--no-ff` 合并;
主提交 `5a28845`,接口统计修正 `213c86e`,最终合并 `9fe62d1`,已推送
`f95325d..9fe62d1 develop`。
- `README.md` 已覆盖印尼市场定位、Flutter 3.47.7/Dart 3.13.5 技术栈、
实际调用的 26 个 `/app/**` 路径(31 个方法级调用)、Android USB/iOS/运行时地址、iOS SPM、
shared_preferences 固定、Android libdartjni、门禁和当前边界。
- 新增 `docs/TASKS.md`:K1-K13 状态表、K4/支付/搜索/下载落盘待办;
未写不确定提交号,未确认项标「需核对」。
- 新增 `docs/FILES.md`:目录树、关键文件职责、test 结构和生成物说明。
- 仓库 `.gitignore` 排除了 `/docs/`,本次仅对 TASKS/FILES 使用
`git add -f`,没有修改 `.gitignore`。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:20 +309 ~1: All tests passed!`;本轮仅文档,不需要 APK 构建。
原始命令、输出与内容核对见 `docs/plans/PROGRESS.md`
「30. K14 客户端文档」。
---
## [待处理] 2026-10-17 | K13 运行时服务器地址设置(仅 debug)
**用户反馈(2026-10-17,已重复 3 次)**:每次 Codex 门禁构建出的 APK 不带 `--dart-define`,App 就回退到 `http://jiangdeMacBook-Air.local:8081`,真机不可用。
> 这是开发效率问题,不是业务功能。只做 debug 工具,不改 release 行为、不改接口。
### 一、现状(实测)
- `lib/core/config/app_config.dart`:`apiBaseUrl = dart-define(API_BASE_URL) 非空 ? 它 : 默认值`;默认值在 iOS/Android 真机上是 `http://jiangdeMacBook-Air.local:8081`(常量)。
- 构建参数由 `--dart-define` 决定,编译期固化;Xcode/门禁构建不带参数时必然回退。
- 客户端已有 `SharedPreferencesAsync` 使用先例(`LocaleController`)。
### 二、本批范围
1. **新增运行时配置** `lib/core/config/server_endpoint.dart`(命名可调整):
- 读取 `SharedPreferencesAsync` 的 `dev.api_base_url`;
- 优先级:**运行时设置 > `--dart-define(API_BASE_URL)` > 现有默认值**;
- 启动时在 `main()` 里 `await` 加载完成后再创建 `ApiClient`(若现有结构不允许,可在 `AppConfig` 初始化时同步设置);
- URL 规范化:去首尾空格、去末尾 `/`;仅接受 `http://` 或 `https://` 开头,非法值忽略并保留原值。
2. **设置入口(仅 `kDebugMode`)**:
- 位置:我的页 → 设置(现有 `showLanguage` 弹窗内增加「服务器地址」一行),或登录页底部一行小字入口;二选一,保持克制;
- 交互:弹窗显示当前实际 baseUrl,可编辑并保存;保存后提示「重启 App 生效」(文案走 l10n,三语);
- 提供「恢复默认」操作,清除 `dev.api_base_url`。
3. **release 行为不变**:`kDebugMode == false` 时完全不显示入口,也不读取本地覆盖(仍用编译期配置)。
4. **测试**:
- 优先级:本地设置 > dart-define > 默认;
- URL 规范化与非法输入拒绝;
- debug 下入口可见、release 下不可见(可用 `debugShowSetting` 之类的可注入开关测试);
- 不得删改既有测试致绿。
### 三、明确不做
- 不做多环境切换 UI(dev/staging/prod 列表)、不做远程配置下发。
- 不改后端、不改接口、不改其他页面业务逻辑。
- 不在 release 包暴露任何服务器地址修改能力。
### 四、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 从干净 `develop` 拉 `feature/flutter-k13-dev-server-setting`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K13.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K13 完成回执(2026-10-17)
**结论**:新增 debug-only 运行时服务器地址覆盖。启动优先级为本地设置 >
`--dart-define(API_BASE_URL)` > 现有默认;release 不读取本地覆盖且不显示入口。
未改接口、后端或其他页面业务逻辑;未跑真机。
- 客户端分支 `feature/flutter-k13-dev-server-setting` 从 `b762289` 完成并
`--no-ff` 合并;功能提交 `907562a`,合并提交 `f95325d`,已推送
`b762289..f95325d develop`。
- 新增 `ServerEndpoint`,使用 `SharedPreferencesAsync` key
`dev.api_base_url`;URL 会 trim、校验 http(s) 和 host,并去掉末尾 `/`。
- `main()` 在 `runApp` 前 debug 加载覆盖;ApiClient 创建时读取已解析地址。
- 我的页设置弹窗新增服务器地址行、编辑/保存、非法值提示、重启生效提示和
恢复默认;入口可通过 `debugShowSetting` 注入,默认 `kDebugMode`。
- 新增三语文案;既有语言设置流程保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:19 +309 ~1: All tests passed!`;不带 `dart-define` 的 Android debug
APK 在隔离副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「29. K13 运行时服务器地址设置」。
设备侧修改地址、重启和网络连通性验收由用户执行。
---
## [待处理] 2026-10-17 | K12 首页去标题 + 空态视觉重做
**用户指令(2026-10-17)**:「有了全部分类后 全部剧目不要显示了 而且现在空状态UI太丑了」。
> 在 K9/K10/K11 基础上做视觉修订,不改接口、数据与业务逻辑。
### 一、现状(实测)
- 首页 `lib/pages/discover/discover_view.dart`:分类 chips 下方有 `Text(l10n.allDramas)`(`Key('home-all-dramas-title')`),再往下才是 `_HomeDramaGrid`。
- 空态组件 `lib/core/widgets/empty_state.dart`:96×96 圆角 28 **实心方块** + 42px 图标 + 16px 标题 + 13px 副标题 + TextButton。用户反馈“太丑”。
### 二、本批范围
#### K12-A 去掉首页「全部剧目」标题
- 删除 `Key('home-all-dramas-title')` 的 `Text` 及其下间距;分类 chips 与网格间距调整为 12–14px。
- 网格、数据、空态逻辑不变;相关测试中“标题存在”的断言改为“不存在”,不得删测试文件。
#### K12-B 空态视觉重做(`empty_state.dart`)
目标:轻盈、精致、留白多,去掉“大色块 + 图标”的模板感。
图形结构改为**双层同心圆**:
- 外层圆:直径 84(compact 60),填充 `blue` 透明度 **0.05–0.06**,并加 1px 描边 `blue` 透明度 0.10–0.12;
- 内层圆:直径 56(compact 40),填充 `blue` 透明度 **0.10–0.12**;
- 中央图标:26–28px(compact 20–22),颜色深色主题 `blueSoft`、浅色主题 `blue`;
- 外层与内层同心居中,用 `Stack` 或两层 `Container` 嵌套实现。
文字与间距:
- 标题 15px / w600 / `onSurface`,与图形间距 14(compact 10);
- 副标题 12.5px / `muted` / 行高 1.45,与标题间距 6;
- 操作按钮:保留 `TextButton`,字号 13、主色、点击区 ≥44pt,与副标题间距 8;
- 整体 `Padding` 上下 28(compact 16),最大宽度 260;
- 不新增图片资源、不加动画。
#### K12-C 测试
- 更新 `empty_state` 相关断言:存在 `empty-state-art` 与内层圆(可用新 Key `empty-state-art-inner`)、图标尺寸、标题/副标题/按钮;
- 首页测试:`home-all-dramas-title` 不存在;chips 后直接出现 `home-all-dramas-grid`;
- 既有测试不得删改致绿。
### 三、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(构建前确认无并发 `flutter run`/`build`)。
- 从干净 `develop` 拉 `feature/flutter-k12-home-empty-polish`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K12.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K12 完成回执(2026-10-17)
**结论**:首页「全部剧目」标题已删除,chips 与网格间距为 14px;空态改为
双层同心圆视觉。未改接口、数据、轮播、三态或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k12-home-empty-polish` 从 `b5a8656` 完成并
`--no-ff` 合并;功能提交 `2dd4627`,合并提交 `b762289`,已推送
`b5a8656..b762289 develop`。
- 外层圆 84(compact 60),blue 5.5% + 11% 描边;内层圆 56(compact 40),
blue 11%;图标 27(compact 21)。
- 标题 15/w600、副标题 12.5、按钮 13;上下 padding 28(compact 16),
最大宽 260。
- 新增 `empty-state-art-inner`、`empty-state-padding` Key;外层
`empty-state-art` 及图标/标题/按钮 Key 保留。
- 首页删除 `home-all-dramas-title`,chips 后直接进入网格;既有壳层中的
`All Dramas` 锚点改为稳定网格 Key,测试流程和用例均保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:17 +302 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「28. K12 首页去标题与空态重做」。
设备侧视觉、深浅主题与小屏验收由用户执行。
---
## [待处理] 2026-10-17 | K11 首页网格 3 列 + 分类首位「全部」
**用户指令(2026-10-17)**:「现在改首页,一行显示 3 个,而且我要的全部是显示在分类的第一个」。
> 在 K9 已实现的 2 列网格基础上做最小修订,不改接口、不改业务逻辑、不动后端。
### 〇、开工前必读
1. K9 计划与实现(`lib/pages/discover/widgets/home_drama_grid.dart`、`discover_view.dart`)
2. K5 主题 token
3. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- `lib/pages/discover/widgets/home_drama_grid.dart`:`SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 16, crossAxisSpacing: 12, childAspectRatio: 2/3)`。
- `lib/pages/discover/discover_view.dart:273`:分类 chips 第一项为 `(code: CategoryController.allCode, label: l10n.forYou)`(文案「为你推荐」)。
- 其余(轮播、空态、三态)保持 K9/K10 现状。
### 二、本批范围
1. **网格改 3 列**:`crossAxisCount: 3`;`mainAxisSpacing: 14`、`crossAxisSpacing: 10`,`childAspectRatio` 保持 `2/3`;3 列下标题最多 2 行、分类 1 行,避免溢出。
2. **分类首位改「全部」**:chips 第一项仍使用 `allCode`,文案改为新增 l10n `all`(中文「全部」/ English「All」/ Bahasa Indonesia「Semua」);保留「热门 / 最新 / 服务端分类」顺序不变。
3. **同步测试**:
- 更新/新增断言 `crossAxisCount == 3`;
- 断言分类 chips 第一项文案为「全部」(按 locale);
- 既有 K9/K10 测试不得删改致绿。
4. **不做**:不改轮播、空态、三态、数据源、接口、其他页面;不新增分页。
### 三、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(当前有 Android 真机 `flutter run` 在跑,构建前先确认没有并发构建占用 `build/`)。
- 从干净 `develop` 拉 `feature/flutter-k11-home-grid-3col`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K11.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K11 完成回执(2026-10-17)
**结论**:首页网格已改为 3 列,分类首项仍绑定 `CategoryController.allCode`,
文案改为「全部 / All / Semua」。未改轮播、空态、三态、数据源、接口或其他页面;
未跑真机。
- 客户端分支 `feature/flutter-k11-home-grid-3col` 从 `0466b4b` 完成并
`--no-ff` 合并;功能提交 `f0db631`,合并提交 `b5a8656`,已推送
`0466b4b..b5a8656 develop`。
- `crossAxisCount: 3`、`mainAxisSpacing: 14`、`crossAxisSpacing: 10`,
`childAspectRatio: 2 / 3` 保持不变。
- 三份 ARB 原本已有 `all` key,分别为 All / 全部 / Semua,本轮直接复用,
未插入重复 JSON key。
- K9 网格测试更新为 3 列并断言「全部」位于「热门」之前;两个既有首页壳层
用例只把旧分类文案断言改为「All/全部」,导航流程和用例均保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「27. K11 首页 3 列与全部分类」。
设备侧 3 列布局、分类切换与详情跳转验收由用户执行。
---
## [待处理] 2026-10-17 | K10 统一空列表占位(EmptyState)
**用户指令(2026-10-17)**:「做空列表的占位图,所有的列表,要求简单大方、符合审美」。
> 依赖 K5 主题 token;**不引入图片资源**(用内置 `Icons` + 柔和圆形底),保证离线、体积与风格统一。
> 只改空态呈现,不改列表数据、接口与业务逻辑。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 主题 token(`AppColors` / `ColorScheme`)
3. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,10 处空态)
| 页面/组件 | 代码位置 | 现有文案 |
|---|---|---|
| 首页 | `lib/pages/home/home_view.dart:56` | `noStories` |
| 首页剧目网格 | `lib/pages/discover/widgets/home_drama_grid.dart:52-53` | `noCategoryDramas` / `noSearchResults` |
| 收藏 | `lib/pages/favorites/favorites_page.dart:107` | `noFavorites` |
| 通知 | `lib/pages/notices/notices_page.dart:115` | `noNotices` |
| 详情剧集 | `lib/pages/drama_detail/drama_detail_view.dart:130` | `noEpisodesYet` |
| 我的-片单 | `lib/pages/mine/mine_view.dart:676` | `noSavedShows` |
| 我的-剧目 | `lib/pages/mine/mine_view.dart:736` | `noStories` |
| 下载 | `lib/pages/downloads/downloads_page.dart:151` | `noDownloads` |
| 观看记录 | `lib/pages/watch_records/watch_records_page.dart:97` | `noWatchRecords` |
| 评论弹窗 | `lib/pages/player/widgets/drama_comments_sheet.dart:225` | `noComments` |
现状均为纯文字(部分带按钮),风格不统一。
### 二、本批范围
#### K10-A 新建统一组件 `lib/core/widgets/empty_state.dart`
规格(简单大方):
- 布局:垂直居中,`Padding` 24;默认最大宽度 280,避免宽屏拉伸。
- 图形占位:96×96 圆角 28 的容器,背景 `AppColors.blue` 透明度 0.10–0.12;中央 `Icons` 40–44px,颜色 `AppColors.blueSoft`(深色主题)/ `AppColors.blue`(浅色主题按 `Theme.brightness` 取)。
- 标题:15–16px、w600、`onSurface`;居中,最多 2 行。
- 副标题(可选):13px、`muted`、居中,最多 2 行。
- 操作按钮(可选):`TextButton`/`OutlinedButton`,主色文字,最小点击区域 44pt;用于「重试」「去发现」等。
- `compact` 变体:图形 64×64、图标 28、字号缩小,用于评论弹窗等内嵌区域。
- 语义:外层 `Semantics(label: title)`;纯装饰图形 `excludeFromSemantics`。
- 参数建议:`icon`、`title`、`subtitle?`、`actionLabel?`、`onAction?`、`compact=false`、`key`。
#### K10-B 替换 10 处空态
- 按上表逐一替换为 `EmptyState`,图标对应语义:
- 首页/剧目:`movie_outlined`;分类:`grid_view_rounded`;搜索:`search_off_rounded`;
- 收藏:`favorite_border_rounded`;下载:`download_outlined`;记录:`history_rounded`;
- 通知:`notifications_none_rounded`;评论:`chat_bubble_outline_rounded`;剧集:`video_library_outlined`;
- 首页加载失败仍用错误态(`error_outline_rounded`),不要用空态冒充失败。
- 保留现有空态文案 key;如需副标题,优先复用已有 l10n,新增文案必须 en/zh/id 三份齐全(例如通用 `emptyHint`)。
- 保留现有按钮行为(重试/去发现等);评论弹窗使用 `compact`。
- 空态出现在原有滚动容器内时,保持可滚动与居中,不破坏三态切换。
#### K10-C 测试
- 组件单测:标题/副标题/按钮渲染、`compact` 尺寸、图标存在、Semantics。
- 页面测试:至少覆盖 收藏/下载/记录/通知/评论 五处空态渲染为 `EmptyState`(可通过 `find.byType(EmptyState)` + 文案);既有测试不得删改致绿。
- 深色/浅色、小屏不溢出。
### 三、明确不做
- 不引入图片/SVG/字体资源;不做动画插画。
- 不改列表数据、接口、三态逻辑、页面结构。
- 不改错误态与加载态,只统一空态视觉。
### 四、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 从干净 `develop` 拉 `feature/flutter-k10-empty-states`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K10.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K10 完成回执(2026-10-17)
**结论**:新增统一 `EmptyState` 组件,任务书表格中的 10 处空态已全部替换;
错误/加载态保持原逻辑,未引入图片、SVG、字体或接口改动。未跑真机。
- 客户端分支 `feature/flutter-k10-empty-states` 从 `19babb8` 完成并
`--no-ff` 合并;功能提交 `6d08330`,合并提交 `0466b4b`,已推送
`19babb8..0466b4b develop`。
- 默认图形 96x96、圆角 28,blue 11% 背景,图标 42;compact 为 64x64、
图标 28;标题 16/w600,副标题 13,操作按钮至少 44pt。
- 深色主题图标 `blueSoft`,浅色主题图标 `blue`;外层 Semantics 只读标题,
图形和文本不重复朗读,操作按钮保持可访问。
- 10 处替换完成:首页、首页分类/搜索、收藏、通知、详情剧集、我的两处、
下载、观看记录、评论 compact。
- 新增三语 `emptyHint`;原有空态 l10n key、页面文案和操作行为保留。
- 新增 `test/empty_state/empty_state_test.dart` 4 个组件用例;收藏、下载、
记录、通知、评论五处页面测试已断言 `EmptyState`,评论还校验 compact。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「26. K10 统一空列表占位」。
设备侧深色/浅色、小屏与各页面空态验收由用户执行。
---
## [待处理] 2026-10-17 | K9 首页剧目列表改造(2 列全部剧目)
**用户指令(2026-10-17)**:「优化首页,要有全部剧目的列表,一行 2 个,不用显示继续观看的」。
> 只改首页(发现页)信息流布局与卡片,不改接口、不改业务逻辑、不动后端。
> 依赖 K5 主题 token 与现有 `RemoteImage` / 卡片视觉。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. 当前首页 `lib/pages/discover/discover_view.dart`(649 行)
3. K5 主题 token、K7 轮播实现
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- 首页结构(`discover_view.dart`):顶部 header → 轮播(home feed banners)→ 分类 chips(为你推荐/热门/最新/服务端分类)→「继续观看」标题 + 横向卡片列表(高 196、卡片宽 132,`_dramaContent` / `_continueHeader` / `_continueTile`)。
- 数据源:`CategoryController.visibleDramas`(当前选中分类的剧目);`_cardsFor(...)` 生成展示卡片;分类切换已有过滤逻辑。
- 「继续观看」区块使用 l10n `continueWatching`;横向列表不展示播放进度,本质是分类剧目横滑。
### 二、本批范围(首页单页改造)
1. **移除「继续观看」区块**:删除 `_continueHeader` 调用与其标题;`_continueTile` 若不再被使用则一并删除(不留死代码)。
2. **新增「全部剧目」2 列网格**:
- 位置:分类 chips 下方,占据原「继续观看」位置;
- 标题:新增 l10n `allDramas`(中文「全部剧目」/ English「All Dramas」/ Bahasa Indonesia「Semua Drama」);
- 网格:每行 2 个,左右边距 16、列间距 12、行间距 16;封面竖版比例 2:3,圆角 14;
- 卡片内容:封面、标题(最多 2 行)、分类/类型(最多 1 行),沿用现有深色渐变与文字层级;
- 数据:当前选中分类下的全部剧目(沿用 `visibleDramas`,去重后全量渲染);「为你推荐」显示全部;
- 点击:沿用 `logic.openCard(...)` 进详情;
- 长列表性能:外层改为 `CustomScrollView` + `SliverGrid`(或等价的懒构建方案),不要用 `GridView(shrinkWrap: true)` 嵌套整页;图片沿用 `RemoteImage` 懒加载。
3. **保留**:header、轮播、分类 chips、底部导航、现有三态(加载/失败重试/空态)与空态文案(无分类剧目/无搜索结果)。
4. **明确不做**:不动接口与数据模型、不改观看记录/收藏/下载、不改轮播与通知、不新增分页(feed 一次返回全量)、不改底部导航。
### 三、测试与门禁
- 新增/更新 widget 测试:首页不再出现「继续观看」;出现 `allDramas` 标题;网格为 2 列(断言 `SliverGridDelegateWithFixedCrossAxisCount.crossAxisCount == 2`);分类切换后网格内容更新;空态与失败态保持。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过;构建前确认无并发 `flutter run`/`build` 占用 `build/`。
- 真机验收由用户执行:首页滚动、2 列布局、分类切换、点击进详情。
### 四、Git 与留痕
- 从干净 `develop` 拉 `feature/flutter-k9-home-grid`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K9.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K9 完成回执(2026-10-17)
**结论**:首页「继续观看」已移除,分类下方改为 `CustomScrollView + SliverGrid`
的 2 列「全部剧目」列表;分类切换、空态、失败态、全量测试与 APK 构建均完成。
未改接口、数据模型、轮播、通知或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k9-home-grid` 从 `99f64ab` 完成并
`--no-ff` 合并;功能提交 `cf03178`,合并提交 `2c381be`,已推送
`99f64ab..2c381be develop`。
- `discover_view.dart` 从 649 行降到 487 行;新增
`widgets/home_drama_grid.dart` 167 行。已删除 `_continueHeader`、
`_dramaContent`、`_continueTile`、`_ShowcaseCard` 与 `_cardsFor`。
- `SliverGridDelegateWithFixedCrossAxisCount` 为 2 列、列距 12、行距 16;
左右边距 16;封面 2:3、圆角 14,标题最多 2 行、类型最多 1 行。
- 新增三语 `allDramas`:`全部剧目` / `All Dramas` / `Semua Drama`。
- 新增 `test/discover/home_all_dramas_grid_test.dart` 5 个用例,覆盖标题、
无继续观看、2 列参数、2:3 封面、分类切换、空态、失败态和 320px 小屏。
- 既有 `test/widget_test.dart` 的 5 处「Continue Watching」可见文案断言原位
更新为「All Dramas」,所有既有用例与导航流程保留,未删除测试用例。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +297 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「24. K9 首页 2 列全部剧目」。
设备侧滚动、布局、分类切换和详情跳转验收由用户执行。
---
## [已处理·待用户验收] 2026-10-17 | K8 登录页视觉重设计
**用户指令(2026-10-17)**:「接下来做登录页面 ui 优化,现在这个 ui 不好看」。
> K6 已完成功能层优化(品牌区、autofill、密码显隐、键盘流转),本批只做**视觉重设计**,不改业务逻辑与接口。
> 依赖 K5 主题 token(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)。
> specs 无设计稿;以下为 PM 按「深色 + 克莱因蓝 + 印尼市场」给出的可执行视觉规格,按此实现。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 主题 token(`lib/core/theme/app_colors.dart`、`app_theme.dart`)
3. 当前登录页 `lib/pages/auth/auth_view.dart`(356 行,K6 后版本)
4. 本 INBOX 条目
### 一、本批范围(登录页单页视觉重设计)
目标:从「表单堆叠」升级为有品牌感的深色登录页,保持现有逻辑与 Key 不变。
**布局结构(自上而下)**
1. 全屏背景:`AppColors.ink`,顶部叠加一层克莱因蓝径向/线性渐变光晕(透明度 ≤0.18,不抢文字对比度)。
2. 品牌区:44–52px 克莱因蓝圆角方块(内含白色播放/剧集图形图标,用 `Icons.` 即可,不引入图片资源)+ App 名 + 印尼市场 tagline(现有 `authBrandTagline`)。
3. 表单卡片:`AppColors.card` 背景、圆角 20、1px 边框(用 K5 `outlineVariant`)、内边距 20;卡片内为标题、副标题、输入框、错误提示、主按钮。
4. 主按钮:全宽、高 52、圆角 26、`AppColors.blue`、`onPrimary` 文字 16/w700;按下态 `bluePressed`;loading 时 18px 白色环且禁用输入。
5. 登录/注册切换:改为**分段控件(Segmented)或 pill 双段**(登录 | 注册),选中段主色底/白字,未选中透明/`muted`;点击切换现有 `toggleMode()`,不再用文字链接。
6. 底部辅助:保留当前模式说明或语言提示一行小字(`muted`,可选);不新增协议勾选。
**输入框规格**
- 填充式背景 `AppColors.bar`,圆角 14,高 52,无外边框;聚焦时 1.5px `AppColors.blue` 边框 + 前缀图标变主色;
- 前缀图标:邮箱 `mail_outline_rounded`、密码 `lock_outline_rounded`;密码后缀显隐按钮保留;
- 标签与错误文案沿用现有 l10n;错误提示改为浅红底胶囊(红 10% 透明度 + 圆角 12 + 图标),不再裸文本。
**动效与可用性**
- 首帧淡入 + 上滑(≤220ms,`TweenAnimationBuilder` 即可,不引第三方库);
- 小屏(iPhone SE)+ 键盘弹出不遮挡:沿用 `auth-scroll` 滚动与 `viewInsets` padding;
- 所有点击区域 ≥44pt;对比度:正文 ≥4.5:1;
- 保留 Key:`auth-scroll`、`auth-email`、`auth-password`;如新增控件需有稳定 Key 便于测试。
**明确不做**
- 不改 `AuthLogic`、校验规则、`AuthController`、接口调用、登录/注册流程;
- 不做忘记密码、Google/Apple 登录、协议勾选、第三方 SDK;
- 不新增图片/字体资源;不改其他页面。
### 二、测试与门禁
- 新增/更新 widget 测试:分段切换仍改变模式、邮箱→密码→提交焦点流转、密码显隐、busy 禁用、错误胶囊展示、品牌区文案存在;既有 `test/auth/auth_page_test.dart` 不得删改用例。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(注意当前有真机构建在跑,避免并发写 `build/`,先确认无构建进程)。
- 真机验收由用户执行:视觉、键盘、小屏、深色对比度。
### 三、Git 与留痕
- 从当前干净 `develop`(`6cc5bfc`)拉 `feature/flutter-k8-auth-visual`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K8.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 四、K8 完成回执(2026-10-17)
**结论**:登录页视觉重设计、交互回归、全量测试和 Android debug 构建均完成。
未改 AuthLogic、校验、接口、登录/注册流程或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k8-auth-visual` 已从 `6cc5bfc` 完成并
`--no-ff` 合并;修复提交 `45c3c64`,合并提交 `99f64ab`,已推送
`6cc5bfc..99f64ab develop`。
- 深色背景、≤0.18 克莱因蓝光晕、48px 品牌方块、20px 圆角卡片、52px
输入框与主按钮、pill 分段切换、错误胶囊、220ms 淡入上滑均按规格实现。
- 既有 `auth-scroll/auth-email/auth-password/auth-switch-mode` Key、autofill、
next→done→提交、busy 禁用、密码显隐与 Haptics 保留;新增视觉控件 Key。
- 新增 `test/auth/auth_visual_test.dart` 6 个用例;既有
`test/auth/auth_page_test.dart` 9 个用例未修改且全过。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:19 +292 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始命令输出见
`docs/plans/PROGRESS.md`「23. K8 登录页视觉重设计」。
设备侧截图、键盘与小屏验收由用户执行。
---
## [待处理] 2026-10-17 | K7 首页轮播图 + 站内信通知
**用户指令(2026-10-17)**:「现在做首页轮播图及站内信通知」。
> **依赖 K5/K6**:轮播与通知页的配色、控件、震动反馈沿用 K5 主题与 K6 `Haptics`。
> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.9,**契约里没有的接口一律不许自造**。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. `FruitServer/docs/api-phase1.md` §1.3(feed banners)/ §1.9.2(notices)
3. K5 主题 token、K6 `Haptics` 封装
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- **轮播图已有基础实现**:发现页 `lib/pages/discover/discover_view.dart` 的 `_carousel` 使用 `lib/pages/discover/widgets/cover_carousel.dart`(3D 翻转风格,手动滑动)。
- 数据来自 `GET /app/discover/feed` 的 `banners[]`;**契约无独立 banner 接口**。
- 现状缺口:无自动轮播、无页码/圆点指示器、单张时无特殊处理、图片失败无占位。
- **站内信完全未接**:客户端没有 `notice_api` / 通知页;`GET /app/notices` 契约已实现。
- 契约:`GET /app/notices?pageIndex=1&pageSize=20`(匿名),返回 `{total, list:[{id,title,content,type,publishAt,expireAt}]}`,`pageSize` 最大 50,空数据返回 `total=0,list=[]`。
- 契约**没有已读/未读接口**,本批不做未读红点。
- 我的页菜单 `lib/pages/mine/mine_view.dart` 的 `_menuCard` 已有「观看历史 / 我的收藏 / 下载 / 设置」列表,可新增「通知」入口。
- 客户端尚未接 `GET /app/config`,本批**不接** `noticeEnabled` 开关(登记为后续项)。
### 二、本批范围(2 块 + 测试验收,按顺序做)
#### K7-A 首页轮播图增强
- 数据源保持不变:`feed.banners`,不新增接口、不改后端。
- 在现有 `CoverCarousel` 上做最小增强,**保留 3D 翻转风格**:
- 自动轮播:间隔 4.5s;用户拖拽/触摸期间暂停,松手后恢复;`items.length <= 1` 时不启动定时器;
- 页面不可见/组件 dispose 时取消定时器(避免后台耗电与 `setState after dispose`);
- 指示器:底部圆点或 `当前/总数`(二选一,克制);颜色用 K5 主题 token,未选中用 `muted` 低透明度;
- 图片加载失败显示占位底色 + 图标(复用 `RemoteImage` 的错误处理,不白屏);
- 点击行为沿用现有 `onOpen`(跳详情),不改变跳转逻辑;
- 无障碍:指示器与轮播容器提供语义标签(走 l10n)。
- `items` 为空时保持现状 `SizedBox.shrink()`,不占位。
#### K7-B 站内信通知
- 新增数据层:`notice.dart`(model)、`notice_api.dart`、`notice_repository.dart`、`notices_controller.dart`,ID 按字符串解析。
- 新增独立页 `lib/pages/notices/notices_page.dart`:
- 分页:首屏 `pageSize=20`,`list.length < total` 时展示「加载更多」并用 `pageIndex+1` 续拉;
- 三态:加载 / 空(「暂无通知」)/ 失败(可重试);支持下拉刷新;
- 列表项:标题、类型(`type` 原样展示或统一「公告」,不自造枚举映射)、`publishAt` 按 locale 格式化、内容摘要(最多 2 行);
- 点击打开详情弹窗(标题 + 发布时间 + 完整 `content`,保留换行);关闭按钮无障碍标签走 l10n。
- 入口:我的页 `_menuCard` 新增「通知」ListTile(图标 `notifications_none_rounded`,主题色),点击打开 `NoticesPage`;不新增首页铃铛,避免改首页信息架构。
- 匿名可访问,不要求登录;不做未读态、不做通知设置。
- 文案全部进 en/zh/id 三份 arb。
#### K7-C 测试与验收
- 轮播:`pump` 时间推进后自动切页、单张不启动定时器、指示器数量与当前页、触摸暂停、点击回调。
- 通知:首屏加载、分页追加、空态、失败重试、详情展示、时间格式化(id locale)。
- 深色主题、小屏、长标题/长内容不溢出。
- 真机验收由用户执行:轮播自动切换与指示器、通知列表/详情、三态与下拉刷新。
### 三、明确不做
- 不新增 banner 接口、不改 `/app/notices` 契约、不改后端/Web/specs。
- 不做已读/未读、通知红点、推送(FCM)、通知设置。
- 不接 `/app/config` 的 `noticeEnabled`(登记后续)。
- 不新增首页铃铛入口、不改首页信息架构。
### 四、规范约束
- 颜色只用 K5 token;震动仅用于关键操作(K6 `Haptics`),轮播滑动不加震动。
- 新增文案三语齐全;新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。
### 六、Git 与留痕
- 从当前干净 `develop` 拉 `feature/flutter-k7-carousel-notices`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K7.md`(范围、影响文件、契约缺口、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 七、K7 完成回执(2026-10-17)
**结论**:K7-A 轮播增强、K7-B 站内通知、K7-C 测试验收均完成并停止。
未新增接口,未改后端/Web/specs。
#### 7.1 首页轮播
- 保持 `feed.banners` 数据源和 `CoverCarousel` 3D 翻转风格。
- 4.5s 自动轮播;单张不启动 Timer。
- 触摸/拖拽期间暂停,松手恢复。
- `active=false` 或 `TickerMode` 不可见时暂停;dispose 取消 Timer。
- 底部圆点指示器使用 `onPrimary` / `muted` K5 token,带三语语义标签。
- 图片失败复用 `RemoteImage` 并显示图标占位。
- 点击继续调用原有 `onOpen`。
#### 7.2 站内通知
- 新增 `notice.dart`、`notice_api.dart`、`notice_repository.dart`、
`notices_controller.dart` 和 `NoticesPage`。
- `GET /app/notices?pageIndex&pageSize=20`,匿名访问。
- ID 按字符串解析,`total` 按数字解析。
- 首屏、加载更多、空态、失败重试、下拉刷新均已接入;
已加载数量 `< total` 时才展示加载更多。
- 列表展示标题、原始 `type`、locale 时间、两行摘要。
- 详情弹窗展示完整 `content`,保留换行并支持长内容滚动。
- 我的页新增“通知”入口;匿名可进入,无登录闸。
- `noticeEnabled` 属 `/app/config`,本批未接,登记后续。
真实 8081 匿名接口验证:
```
$ curl -sS 'http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=20'
{"code":0,"message":"ok","data":{"total":0,"list":[]}}
```
#### 7.3 门禁原始结果
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test test/discover/cover_carousel_test.dart test/notice
All tests passed!
$ flutter test
00:14 +284 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.9s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过。
新增覆盖:
- 自动轮播、单张无 Timer、交互暂停、active/TickerMode、圆点、图片占位、onOpen。
- 通知 API 字符串 ID、controller 分页/空/失败/刷新。
- NoticesPage 首屏、加载更多、加载/空/失败三态、详情换行、印尼时间。
- 320×568 长标题/长内容不溢出;我的页匿名通知入口。
#### 7.4 Git
- 功能提交:`b870956665f5268070c691533cc2a334f2aa7f59`
`feat(flutter): 增加轮播自动播放与站内通知`
- `--no-ff` 合并:`3e8703b`,已推送
`b7b1c50..3e8703b develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.5 用户真机验证步骤与预期
1. 打开发现页顶部轮播,等待 4.5 秒。
预期:自动翻到下一张,圆点同步变化;单张 banner 不会自动循环。
2. 按住/拖动轮播超过 4.5 秒后松手。
预期:按住期间不自动翻页;松手后恢复自动轮播;拖拽仍保持 3D 翻转。
3. 暂时断开图片源或使用失败封面。
预期:显示深色占位和图片失败图标,不白屏。
4. 点击当前 banner。
预期:按原有 DRAMA 跳转逻辑进入详情。
5. 进入“我的 → 通知”,未登录也可访问。
预期:显示通知列表;空数据时显示“暂无通知”;断网时显示失败及重试。
6. 有超过 20 条通知时点击“加载更多”。
预期:下一页追加到列表,无重复;已加载数量达到 total 后按钮消失。
7. 下拉刷新,再点击一条长通知。
预期:刷新后列表回到第一页;详情显示标题、发布时间和完整正文;
正文换行保留且长内容可滚动。
8. 切换 Bahasa Indonesia 查看通知时间。
预期:时间按印尼 Material 格式显示。
设备侧截图与真机结果由用户执行。
#### 7.6 已知边界
- 无已读/未读、红点、FCM、通知设置。
- 不接 `/app/config noticeEnabled`,通知入口当前常驻。
- 无独立 banner 接口,未新增首页铃铛。
---
## [已处理·待用户验收] 2026-10-17 | K6 客户端 UI:登录页 + 底部导航优化
**用户指令(2026-10-17)**:「接下来优化登录页面 UI 及底部导航 UI」。
> **依赖 K5 完成**:必须基于 K5 的克莱因蓝主题(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)与统一 `ColorScheme` 再做,不得沿用旧 `#2F7BFF`。
> specs 无 UI 设计稿(已确认),本批以「视觉统一 + 可用性」为目标,不自行改变信息架构。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 条目与 K5 计划文档(色值 token)
3. specs `docs/02-技术架构.md`(印尼首发)、`docs/19-术语表.md`
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- 登录页 `lib/pages/auth/auth_view.dart`(246 行):AppBar + ListView,两个 `TextField`、`FilledButton`、错误行、登录/注册切换链接。
- 密码**无显隐切换**;无 `autofillHints`;无键盘 `next/done` 流转;错误提示写死 `#FF6B6B`。
- 主色仍直接引用 `AppColors.blue`,无品牌区;小屏键盘遮挡只靠 ListView 自然滚动。
- 底部导航在 `lib/pages/home/home_view.dart` 的 `_HomeTabs`(约 182–260 行):自定义 `Container` + `Row` + 4 个 `_TabButton`。
- 高度 63,背景 `#0D0D11`、上边框 `#242329` 均为硬编码;只有选中/未选中图标,无指示条;无统一主题 token。
- 现有 Key:`home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`,**测试依赖,不得改名**。
- 播放沉浸模式(`videoImmersive`)下隐藏导航,需保留。
### 二、本批范围(3 块 + 测试验收,按顺序做)
#### K6-A 登录页 UI 优化
- 品牌区:登录页顶部增加 App 名与一句印尼市场定位副标题(文案走 l10n 三语),使用 K5 主题色;不引入图片资源。
- 表单体验:
- 邮箱 `autofillHints: [AutofillHints.email]`、密码 `[AutofillHints.password]`;
- 密码框增加显隐切换(图标按钮,语义标签走 l10n);
- 键盘动作:邮箱 `next` → 密码 `done` → 提交;提交中禁用输入与按钮;
- 错误提示统一样式,颜色取主题语义错误色,不再硬编码 `#FF6B6B`。
- 布局与可用性:小屏 + 键盘弹出不遮挡输入与提交按钮(`resizeToAvoidBottomInset` + 滚动/SafeArea);所有点击区域 ≥44pt;主按钮用 `blue`,按下态用 `bluePressed`。
- **不改**登录/注册业务逻辑、接口调用、字段校验规则与切换逻辑;**不做**忘记密码(契约无接口)、Google/Apple 登录(属 K4)、隐私协议勾选(无契约要求)。
#### K6-B 底部导航 UI 优化
- 统一使用 K5 主题 token:背景、边框、选中/未选中色全部来自 `AppColors` / `ColorScheme`,移除 `#0D0D11`、`#242329` 等硬编码。
- 视觉:选中项为主色图标 + 主色标签 + 明确的指示条/背景胶囊(二选一,保持克制);未选中为 `muted`;图标 22–24、标签 10–11,间距一致。
- 可用性:整体高度与底部安全区适配(含 iPhone home indicator),每个 tab 点击区域 ≥48pt;切换动效 ≤200ms,不引入第三方动画库。
- **保持**:4 个 tab 的数量、顺序、导航逻辑与全部 Key 不变;播放沉浸模式仍隐藏导航。
- **不做**:新增 tab、改信息架构、改页面内容。
#### K6-C 点击震动反馈
> specs 无触觉反馈规范;此为用户 2026-10-17 明确指令,按下方口径执行。客户端当前 `HapticFeedback` 调用数为 0。
- 使用 Flutter 内置 `HapticFeedback`(`package:flutter/services.dart`),**不引入第三方插件**。
- 统一封装 `lib/core/platform/haptics.dart`(如 `Haptics` 抽象 + 默认实现),页面通过依赖注入调用,便于 widget 测试替换为 fake;禁止在页面里散落直接调用静态 `HapticFeedback.*`。
- 触发范围(克制,只做关键操作):
- 底部导航 tab 切换:`selectionClick()`;
- 主操作按钮成功触发时:登录/注册提交、收藏/取消收藏、点赞/取消点赞、评论发送、举报提交、下载授权申请:`lightImpact()`;
- 危险操作确认(清空观看记录、清空下载、删除记录/授权):`mediumImpact()`。
- 不在普通列表项点击、滚动、页面切换上震动;不新增「震动开关」设置项(避免超出本批)。
- 调用不阻塞 UI:`unawaited(...)`;异常不得影响业务(触觉不可用时静默忽略,禁止空 catch 之外的吞错写法)。
- 真机验收:iPhone 上逐项可感知;iOS 低电量模式会自动禁用触觉,验收时确认未处于低电量模式。
#### K6-D 测试与验收
- widget 测试至少覆盖:
- 底部导航 4 个 Key 存在、选中态随 tab 切换、沉浸模式下隐藏;
- 登录页密码显隐切换、提交中按钮禁用、错误提示展示、键盘 next/done 焦点流转(可用 `tester.testTextInput`);
- 震动反馈:用 fake `Haptics` 断言 tab 切换触发 `selectionClick`、主操作触发 `lightImpact`、危险操作确认触发 `mediumImpact`,且普通列表点击不触发;
- 无新增硬编码色值(沿用 K5 的扫描测试口径)。
- 深色主题、小屏(iPhone SE 级别)、键盘弹出三种场景不溢出。
- 真机验收由用户执行:登录页视觉与交互、底部导航选中态与安全区。
### 三、规范约束
- 颜色只允许来自 K5 的 `AppColors` / `ColorScheme`;页面禁止硬编码色值。
- 新增文案必须 en/zh/id 三份 arb 齐全并重新生成;禁止硬编码文案。
- 保持现有 Key 与业务逻辑;函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
### 四、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。
### 五、Git 与留痕
- 从 K5 合并后的干净 `develop` 拉 `feature/flutter-k6-auth-nav-ui`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K6.md`(范围、影响文件、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 六、K6 完成回执(2026-10-17)
**结论**:K6-A 登录页、K6-B 底部导航、K6-C 点击震动反馈与 K6-D 测试验收均已完成
并停止。未做 K4,未接新接口,未改后端/Web/specs,未跑真机。
#### 6.1 登录页
- 新增 App 品牌区、印尼市场副标题与管理入口一致的三语文案。
- 邮箱/密码 autofill;密码显隐按钮与三语语义标签。
- 键盘 next → done → 提交,提交中禁用输入、显隐、提交和模式切换。
- 错误提示使用 `ColorScheme.error`,不再硬编码错误红。
- submit、显隐按钮、模式切换点击区均 ≥44pt;小屏键盘场景可滚动到 submit。
- 登录/注册接口、校验规则和模式切换逻辑未改。
#### 6.2 底部导航
- 背景 `AppColors.bar`、边框 `ColorScheme.outlineVariant`,移除旧深色边框/背景硬编码。
- 选中项使用 `bluePressed` 胶囊、`onPrimary` 图标、`blueSoft` 标签;
未选中使用 `muted`。
- 160ms 内置动画;64pt 容器 + SafeArea 底部适配;每个 tab 点击区 ≥48pt。
- 4 个 tab 数量、顺序、回调和 Key 不变:
`home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`。
- `videoImmersive` 为 true 时导航继续隐藏。
#### 6.3 点击震动反馈
- 新增 `lib/core/platform/haptics.dart`:`Haptics` 抽象 +
`FlutterHaptics` 默认实现;通过 `LumaDramaApp` Provider 注入,测试使用 fake。
- 页面没有直接调用静态 `HapticFeedback.*`;静态扫描限定封装文件内部调用。
- 触发范围:
- tab 真正切换:`selectionClick()`;重复点击当前 tab 不触发。
- 登录/注册提交、收藏/取消收藏、剧集/评论点赞、评论发送、举报提交、
下载授权申请:`lightImpact()`。
- 清空观看记录、清空下载、删除记录、删除下载授权:`mediumImpact()`。
- 所有页面调用均为 `unawaited`;平台异常由默认实现静默忽略,不影响业务。
- 普通列表点击、滚动、页面跳转不触发。
#### 6.4 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:14 +263 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.6s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。
新增测试覆盖:
- 登录品牌、autofill、密码显隐、next/done、busy 禁用、语义错误色、
44pt 点击区和小屏键盘滚动。
- 底栏 4 Key、选中态切换、主题背景/边框、48pt 点击区、160ms 动效和沉浸隐藏。
- Fake Haptics 的 selection/light/medium 触发、重复 tab 不重复触发、
普通列表点击不触发。
- 静态调用扫描与平台触觉异常静默测试。
- Klein Blue 旧色值扫描继续为零。
#### 6.5 Git
- 登录页/底部导航提交:`e609e3a`
`feat(flutter): 优化登录页与底部导航`
- 震动反馈提交:`8a481d5`
`feat(flutter): 增加关键操作震动反馈`
- `--no-ff` 合并提交:`0cedaab`
`merge: 合并 K6 登录导航与震动反馈`
- 已推送:`e52d818..0cedaab develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 6.6 用户真机验证步骤与预期
前置:iPhone 关闭低电量模式并确认系统触觉正常;安装最新 debug APK。
以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 打开登录页,检查品牌名、印尼市场副标题、输入框和主按钮。
预期:三语显示正确;按钮为克莱因蓝白字;密码眼睛按钮可切换显隐;
错误提示为系统语义红色;所有点击区易按。
2. 小屏手机打开键盘,使用邮箱键盘 next,密码键盘 done。
预期:焦点从邮箱进入密码;done 触发登录提交;键盘弹出时 submit 可滚动到可见区域。
3. 使用延迟网络或错误账号观察提交过程。
预期:提交时两个输入框、眼睛、submit 和模式切换全部禁用;加载指示可见;
完成后恢复或展示后端错误,不崩溃。
4. 在浅色/深色系统主题与小屏下检查底部 4 个 tab。
预期:4 个 Key 对应入口均存在且顺序不变;选中胶囊和指示清晰;
底部有安全区留白;无文字/图标重叠。
5. 逐一点击非当前 tab。
预期:每次真正切换触发轻微 selection 触觉;重复点击当前 tab 不再触发。
6. 登录提交、收藏/取消、剧集或评论点赞、发送评论、提交举报、申请下载授权。
预期:关键操作触发 lightImpact;触觉不影响请求速度或业务结果。
7. 清空观看记录、清空下载、删除记录或下载授权并确认。
预期:确认后触发更明显的 mediumImpact;取消确认不触发。
8. 点击普通列表项(如“我的”中的语言入口)并滚动列表。
预期:不产生震动;页面切换和滚动保持无触觉。
9. 临时开启 iOS 低电量模式后重复上述操作。
预期:系统可能禁用触觉,但业务、页面和无异常行为保持正常;
正式验收请关闭低电量模式。
设备侧截图、触觉感知结果与系统低电量测试由用户执行。
#### 6.7 iOS Xcode SPM 构建修复
- `Runner.xcscheme` 新增 “Bootstrap Flutter SPM Package” PreAction:
按 `FLUTTER_ROOT → PATH → $HOME/flutter → /opt/homebrew` 兜底解析
Flutter,执行 `flutter pub get`,找不到时明确报错并非 0 退出。
- 原有 “Run Prepare Flutter Framework Script” 保留,并改为同一兜底链解析
Flutter root 后执行 `xcode_backend.sh prepare`。
- `xmllint --noout` 通过;`xcodebuild -list` 可识别 Runner scheme 与全部
SPM source packages。
- 在已有 SPM 包时完整执行:
`xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build`
结果为 `BUILD SUCCEEDED`;日志确认执行 PreAction,输出
`Resolving dependencies...`、`Got dependencies!`。
- 删除 `ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage`
后立即运行同一 `xcodebuild`,Xcode 在 “Resolve Package Graph” 阶段先失败,
尚未运行 PreActions:
`Could not resolve package dependencies ... folder doesn’t exist`。
因此 **scheme-only 修复无法覆盖“包从未生成”的首次冷启动**;
该冷启动自动生成场景标记为未通过验证。
- 当前可靠口径:新克隆、`flutter clean` 或手动删除 ephemeral 后,先执行一次
`flutter pub get`,再从 Xcode GUI 运行;正常构建期间 PreAction 会继续刷新包。
- 修复提交:`cc037a2956848128c4899df1f5092fea44d3620b`
`fix(ios): 刷新 Flutter SPM 包后再准备框架`
- 合并:`ac706a4`,已推送 ReelShort `develop`。
#### 6.8 导航白线与观看历史缺陷修复
- 底部导航白线:M3 默认深色 `outlineVariant` 实测亮度为 `1.0`。
已新增显式 token:
`outlineDark=#3A3C45`、`outlineVariantDark=#242329`、
`outlineLight=#8A8E99`、`outlineVariantLight=#D6D9E2`;
light/dark ColorScheme 均显式赋值,底栏边框使用主题
`outlineVariant`。
- 新增测试断言导航边框不是白色、深色亮度 `<0.2`,
并覆盖 light/dark ColorScheme token。
- 观看历史诊断:
- 只读查询 `UKZBA9IBE` 得到两条正常记录:剧 1/集 1、剧 5/集 7;
- 真实 8081 recent 返回结构正常,客户端诊断账号解析出 2 条记录并成功补详情;
- 未复现用户设备上的 401/100000、JSON 异常或详情失败;
- 当前代码缺陷是 `WatchRecordsController` 把 recent 异常统一折叠为
`progressError`,后端原文无法定位。
- 修复:controller 对 recent/详情异常打印真实类型、异常与堆栈;
`ApiException.message` 透传到页面错误态,页面新增稳定 retry Key。
- 临时真实联调诊断文件已删除,不进入提交。
- 用户设备上的具体错误仍待设备侧日志确认,未宣称根因已完全复现。
定向门禁:
```
$ flutter test test/theme test/home/home_nav_ui_test.dart test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart
00:01 +21: All tests passed!
```
全量门禁:
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:13 +266 ~1: All tests passed!
$ flutter build apk --debug
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
Git:
- 修复提交:`732483465ced2b28438585a4b2f1573e9a0529cb`
`fix(flutter): 修复导航白线与观看记录错误透传`
- `--no-ff` 合并:`b7b1c50`,已推送 `ac706a4..b7b1c50 develop -> develop`
#### 6.9 已知边界
- 未增加震动开关;触觉遵循系统设置。
- 不做忘记密码、Google/Apple 登录、隐私勾选或新 tab。
- Xcode 首次包图解析早于 scheme PreActions;完全无 SPM 包时仍需先运行
`flutter pub get`,本轮未通过 scheme 实现冷启动自动生成。
- 观看历史缺陷未在用户真机上复现;需用户更新后抓
`[WatchRecordsController] recent failed` / `detail failed` 日志与页面原文。
- K4 未开始。
---
## [已处理·待用户验收] 2026-10-17 | K5 客户端视觉:克莱因蓝主题 + 印尼市场适配
**用户指令(2026-10-17)**:「改 UI 及 App 整体颜色,克莱因蓝,针对印尼市场,认真阅读文档严格执行」。
> **本批只做 K5 视觉与本地化,不做 K4,不接新接口。**
> **颜色口径**:specs 全文没有成文色板或设计稿(已检索 `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`、`docs/01~20`、`docs/plans/*`、`FruitServer/docs/*`);
> 颜色值来自用户 2026-10-17 明确裁定的 **方案 A**,按下方色值执行,不得自行另选。
### 〇、开工前必读(按顺序)
1. `AGENTS.md`(目标市场:首发印尼)
2. `CODING_STANDARDS.md`(Dart/Flutter 红线、门禁)
3. `GIT_WORKFLOW.md`(Git Flow)
4. specs `docs/02-技术架构.md`(印尼首发)、`docs/15-成本清单与预算.md`(印尼语/Android 主导)、`docs/19-术语表.md`、`docs/16-测试用例库.md`
5. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,直接用)
- 主题集中在 `lib/core/theme/app_colors.dart`:`blue #2F7BFF`、`blueSoft #8FB4FF`、`ink/bar/card/muted`。
- `lib/app.dart` 浅色与深色两套 `ColorScheme.fromSeed` 都硬编码 `seedColor: 0xFF2F7BFF`,另有 `scaffoldBackgroundColor`。
- **42 处硬编码蓝色**散落 14 个文件,分布:`mine_view.dart` 13、`discover_view.dart` 5、`player_view.dart` 4、`comment_tile.dart` 3、`drama_detail_view.dart` 3、`home_view.dart` 2、`auth_view.dart` 2、`app.dart` 2,其余 7 个文件各 1。
- `lib/core/config/locale_controller.dart` **只支持 `en` / `zh`**,选 `id` 会落成 null(跟随系统)——印尼首发缺陷。
- `app.dart` 的 `localeResolutionCallback` 在系统语言不支持时回退 `supported.first`(当前为 en),与「首发印尼」不符。
- 三语 arb 齐全:`app_en.arb` / `app_zh.arb` / `app_id.arb` 各 243 个 key。
### 二、本批范围(4 块,按顺序做)
#### K5-A 主题色系统(克莱因蓝)
色值(用户裁定,方案 A):
| Token | 值 | 用途 |
|---|---|---|
| `blue` | `#002FA7` | 主色(Klein Blue / IKB) |
| `bluePressed` | `#00227A` | 按下/激活态 |
| `blueSoft` | `#5B7BE0` | 深色背景上的高亮/次级文字与图标 |
| `onPrimary` | `#FFFFFF` | 主色上的文字/图标 |
| `ink/bar/card/muted` | 保持现值 | 深色底不改成蓝色,不破坏现有暗色风格 |
- `app_colors.dart` 增加上述 token,并把 `blue` 改为 `#002FA7`;`blueSoft` 若与现有语义冲突,明确注释用途。
- `app.dart` 两套主题改为显式 `ColorScheme`(或 `fromSeed` + 覆盖 `primary/onPrimary`),确保浅色与深色下主色一致为 `#002FA7`;`seedColor` 不得再出现其他蓝色字面量。
- **对比度硬要求**:正文文字 ≥ 4.5:1、大字号/图标 ≥ 3:1;主色大面积背景使用 `bluePressed`,主色上的文字用 `onPrimary`。
- 新增单测:至少断言 `blue == Color(0xFF002FA7)`、`onPrimary` 在白底/主色上的对比度达标。
#### K5-B 全量 UI 替换
- 把 42 处硬编码蓝色全部改为 `AppColors.*` 或 `Theme.of(context).colorScheme.*`,页面层禁止再出现 `0xFF2F7BFF` / `Colors.blue`。
- 覆盖页面:首页、发现、详情、播放(右栏/进度条)、我的、评论弹窗与评论项、观看记录、收藏、下载、登录、`AuthGate`、快捷抽屉。
- 统一控件:FilledButton/TextButton、Tab 指示器、Switch、进度条、选中态、链接、SnackBar 主色。
- **不改语义色**:错误红、成功绿、点赞红(如有)保持原语义,不因主题替换改成蓝色。
#### K5-C 印尼市场适配
- `LocaleController` 支持 `id`:`'id' => Locale('id')`,读写 `ui.locale` 持久化。
- 默认语言规则:用户未显式选择时跟随系统;系统语言不是 `en/zh/id` 时**回退 `id`**(首发印尼),不得回退 `en`。
- 语言选择入口增加 **Bahasa Indonesia** 选项(现有入口位置沿用,不新造页面)。
- 校验三语 arb:新增文案必须 en/zh/id 三份齐全;禁止硬编码文案。
- 日期/数字本地化:检查现有格式化是否按 locale(`intl`),`id` 下日期/数字格式正确。
- 不接 Google 登录、不做支付(属 K4/后续)。
#### K5-D 回归与验收
- 深色/浅色两套主题、加载/空/失败三态、小屏(iPhone SE 级别)均不破版。
- 新增 widget 测试:主题主色生效、页面无硬编码蓝色(可用静态扫描测试)、`LocaleController` 支持 id 与回退 id。
- 真机验收由用户执行:主题色、按钮/进度条/选中态、语言切换与印尼语显示。
### 三、明确不做
- 不改业务逻辑、接口调用、后端、`ReelShortWeb`、specs。
- 不做 Google 登录、支付、下载落盘。
- 不改 App 图标/启动图(无设计资源;如后续提供 Stitch 出稿再做)。
- 不替换信息架构与页面布局,只做颜色、主题与本地化;布局仅在换色后出现对比度/溢出问题时做最小修正。
### 四、规范约束
- 颜色只允许在 `app_colors.dart` 与主题层定义;页面禁止硬编码色值(测试断言除外)。
- l10n 三语齐全并重新生成;所有新增文案进三份 arb。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试覆盖:主题 token 与对比度、无硬编码蓝色扫描、locale id 支持与回退、语言切换。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」,禁止用开发机 `curl` 冒充设备证据。
### 六、Git 与留痕
- 开工前先处理上一条诊断分支 `feature/flutter-ios-home-diagnostics`:按实际情况提交为诊断增强或清理回滚,确保 K5 从**干净的 `develop`** 拉分支(当前 develop 为 `e1147cf`)。
- 分支:`feature/flutter-k5-klein-blue`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K5.md`(范围、影响文件、色值表、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执(含用户验证步骤与预期)。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 七、K5 完成回执(2026-10-17)
**结论**:K5-A 主题 token/ColorScheme、K5-B 蓝色收敛、K5-C 印尼语适配、
K5-D 回归均已完成并停止,未开始 K4,未接新接口,未跑真机。
#### 7.1 诊断分支收口
- iOS 首页诊断插桩已保留在未合并分支
`feature/flutter-ios-home-diagnostics`,提交
`7efbed97587dd3ed02967dcbbaa6fb85693c1883`。
- K5 从干净的 ReelShort `develop e1147cf` 拉出,未夹带诊断插桩。
#### 7.2 色值、主题与蓝色收敛
- `AppColors.blue = #002FA7`、`bluePressed = #00227A`、
`blueSoft = #5B7BE0`、`onPrimary = #FFFFFF`。
- `AppTheme.light()` / `AppTheme.dark()` 使用显式 ColorScheme,两套主题
`primary` 均为 `#002FA7`、`onPrimary` 均为白色;SnackBar 使用深蓝背景白字。
- 深色表面的小图标、链接和轨道使用 `blueSoft`;大面积选中背景使用
`bluePressed`;主按钮使用 `blue + onPrimary`。
- 页面层旧蓝 `0xFF2F7BFF`、`0xFF8FB4FF`、`Colors.blue` 扫描结果为空。
- 错误红、成功绿、点赞红等语义色未改;App 图标/启动图未改。
#### 7.3 印尼市场适配
- `LocaleController` 支持选择、持久化和恢复 `id`。
- 未显式选择时跟随系统;系统语言非 `en/zh/id` 时回退 `id`。
- “我的”语言弹层与首页快捷抽屉均增加 Bahasa Indonesia;
快捷抽屉改为 Wrap,避免 320px 小屏溢出。
- 新增 `bahasaIndonesia` 三语文案并重新生成 l10n。
- 观看记录与下载授权的日期/时间改由 `MaterialLocalizations` 按当前 locale
和系统 12/24 小时设置格式化。
#### 7.4 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:12 +251 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 26.6s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。
新增测试覆盖:
- Klein Blue token、白字/主色对比度、图标/深底对比度。
- 浅色/深色 ColorScheme 与 widget 实际主题值。
- `lib` 旧蓝字面量静态扫描。
- locale `id` 选择、持久化、系统语言跟随与未知语言回退 `id`。
- Bahasa Indonesia 选项、语言切换、印尼语日期/时间格式。
- 320×568 深色小屏首页与“我的”页不溢出。
#### 7.5 Git
- 功能提交:`10d448c8ba93aa2762e14babdb528805897b3992`
`feat(flutter): 应用克莱因蓝主题与印尼语适配`
- `--no-ff` 合并提交:`e52d81881c19202c7adc8d13a6f78f78e4f6182f`
`merge: 合并 K5 克莱因蓝主题与印尼语适配`
- 已推送:`e1147cf..e52d818 develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.6 用户真机验证步骤与预期
前置:安装最新 debug APK,分别准备一套 Android 与 iOS 设备,保留系统深色/浅色
切换条件。以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 浅色与深色系统下分别打开首页、发现、详情、播放、我的、收藏、记录和下载。
预期:页面可读、无溢出;主按钮为克莱因蓝白字;选中态/进度条可见;
深色底上的链接和图标使用较亮的蓝紫高亮;旧亮蓝 `#2F7BFF` 不再出现。
2. 对主按钮、进度条、选中 Tab 截图并用取色工具检查。
预期:主色接近 `#002FA7`;按下/激活大面积背景接近 `#00227A`;
主色按钮文字为 `#FFFFFF`。
3. 在 iPhone SE 级小屏打开首页与“我的”,展开语言入口。
预期:三个语言选项自动换行,不挤出屏幕;底栏文字和图标不重叠。
4. 从“我的 → 语言”选择 Bahasa Indonesia,再冷启动 App。
预期:界面切为印尼语,重启后仍保持印尼语。
5. 从首页快捷抽屉选择 Bahasa Indonesia。
预期:同样切为印尼语,两个入口行为一致。
6. 清除 App 数据/重新安装,把系统语言设为法语,且不手动选择语言。
预期:App 自动使用印尼语,不回退英语。
7. 在印尼语下查看观看记录与下载授权时间。
预期:日期/时间使用印尼地区 Material 格式;系统 12/24 小时设置变化时格式跟随。
8. 未登录点评论发布/点赞/举报,登录页按钮与错误/成功提示仍清晰可辨。
预期:业务流程不变;Klein Blue 按钮、蓝色 SnackBar 与红色错误提示对比度正常。
设备侧截图、系统语言切换日志与真机结果由用户执行;开发机检查不能替代这些证据。
#### 7.7 已知边界
- 现有主体页面保持 dark-first 设计,并显式使用 `AppTheme.dark()`;
根部同时提供浅色/深色 ColorScheme,未重做信息架构或页面布局。
- App 图标、启动图、Google 登录与支付不在本批范围。
- K4 未开始。
---
## [已处理·待用户验收] 2026-10-17 | K3 客户端第三批:互动(评论 + 点赞 + 举报 + 分享)
**用户指令(2026-10-17)**:客户端继续对接接口及 UI,严格执行 specs。
> **本批只做 K3,做完停下写回执,不要连做 K4。**
> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A6、`docs/16-测试用例库.md` INTERACT、`docs/09-测试计划.md`);
> 接口只认 `FruitServer/docs/api-phase1.md` §1.7,**契约里没有的接口一律不许自造**。
### 〇、开工前必读(按 specs 顺序,缺一不可)
1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏)
2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表)
3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛)
4. `FruitServer/docs/api-phase1.md` §0 / §1.7(接口唯一权威)
5. `docs/plans/codex-研发任务清单.md`(A6)、`docs/16-测试用例库.md`(INTERACT-01~09)、`docs/09-测试计划.md`
6. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,直接用)
- ReelShort `develop` `45f5f25`(K2 已合并推送),互动现状全部是**本地演示数据**:
- [engagement_controller.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/engagement_controller.dart#L8-L8) 注释明确 `Local demo interactions ... No public/social backend`,内部维护 `liked` / `comments` 两个本地集合;
- 播放页右栏已有三个入口:点赞 `rail-like`、评论 `rail-comments`、分享 `rail-share`([player_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/player_view.dart#L339-L366));
- 评论弹窗 [drama_comments_sheet.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/widgets/drama_comments_sheet.dart#L28-L33) 直接 `addComment` 到本地 `List`,无分页、无回复、无删除、无举报;
- 分享走 [platform_share_service.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/platform_share_service.dart) 的系统分享,**不需要后端接口**;
- 全工程没有举报入口。
- 后端 §1.7 五个接口**均已实现**(`AppCommentController`、`AppInteractionController`),本批**不改后端**。
**契约字段(实测自后端 VO,直接用)**
- `CommentRespVO`:`id,dramaId,episodeId,parentId,rootId,userId,uid,nickname,avatarUrl,content,status,auditReason,likeCount,replyCount,reportCount,createdAt,mine,liked`(ID 按字符串解析)
- `CommentPublishReqVO`:`dramaId`(必填)、`episodeId`、`content`(必填)、`parentId`(回复时传)
- 列表 `GET /app/comments?dramaId=&episodeId=&rootId=&cursor=&size=` → `CursorResult{list,nextCursor,hasMore}`;**匿名可读**
- `POST /app/likes/toggle`:`{targetType: DRAMA|COMMENT, targetId}` → `{liked, count}`
- `POST /app/reports`:`{targetType: COMMENT|USER|DRAMA, targetId, reasonCode(必填), description?}` → 工单 id
- 敏感词三档:`BLOCK` 直接报错、`REPLACE` 替换后放行、`REVIEW` 转待审(`status=pending`,仅自己可见)
- 限流:同一剧集每分钟 5 条评论(后端判定,客户端只展示返回的 `message`)
### 二、本批范围(4 块,按顺序做)
#### K3-A 评论
- 评论列表弹窗改为服务端数据:首屏 `size=20`,`hasMore` 时展示“加载更多”,用 `nextCursor` 续拉;三态(加载/空/失败可重试)。
- 发布顶层评论:`dramaId + content`;发布回复:额外传 `parentId`(**只做一层楼层回复**,回复列表用 `rootId` 拉取,不做无限嵌套)。
- 敏感词三档处理:
- `BLOCK`:原样展示后端 `message`,不清空输入框;
- `REPLACE`:展示后端替换后的内容(以响应为准);
- `REVIEW`:该条以 `status=pending` 出现在自己的列表里,并显示“审核中”标识(文案走 l10n)。
- 删除自己的评论:仅 `mine=true` 显示删除入口,二次确认后调 `DELETE /app/comments/{id}`;失败回滚并提示。
- 未登录:列表照常可看(匿名),**发布/点赞/举报前拦截**并弹现有 `AuthGate` 登录提示,不发业务请求。
- 限流/重复提交:直接展示后端 `message`,前端不自造判断。
#### K3-B 点赞
- 剧集点赞:播放页 `rail-like` 接 `POST /app/likes/toggle`(`targetType=DRAMA`),乐观更新,失败回滚并提示;未登录先走 `AuthGate`。
- 评论点赞:评论列表项内接 `targetType=COMMENT`,用返回 `{liked, count}` 更新该项。
- **契约缺口(按此处理,不许改后端)**:`AppDramaDetailVO` / `AppDramaCardVO` 没有 `likeCount` / `liked`,客户端拿不到剧集点赞初始值。
本批**不展示剧集点赞数字**(避免假数据),只展示/切换当前会话内的 liked 状态;并在计划文档登记
「建议后端详情与卡片补 `likeCount` / `liked`(需 specs 负责人改契约)」。
#### K3-C 举报
- 入口:评论项“更多”菜单(举报评论)、详情页更多菜单(举报短剧)。**不做举报用户**(本批无用户主页入口)。
- 弹窗:原因单选 + 可选描述,提交 `POST /app/reports`;成功后提示并关闭。
- 失败透传:重复举报 `你已举报过该内容,我们正在处理`、举报自己 `不能举报自己发布的内容`,一律展示后端 `message`。
- 未登录先走 `AuthGate`。
- **契约缺口(登记并按下述降级)**:§1.7 未定义 `reasonCode` 枚举,后端不校验。本批前端固定 5 项:
`SPAM`(垃圾广告)、`ABUSE`(辱骂骚扰)、`PORN`(色情低俗)、`AD`(引流广告)、`OTHER`(其他);文案进三语 arb。
计划文档登记「建议后端补 `reasonCode` 枚举与后台映射」。
#### K3-D 分享
- 保持现有系统分享(`PlatformShareService`),**不新增后端接口**;只做回归验收(标题/简介/footer 走 l10n)。
### 三、明确不做(避免误判“没做完”)
- 拉黑(TC-INTERACT-06):§1.7 无接口,本批不做。
- 审核联动闭环(TC-INTERACT-09):属后台 `2.5 内容治理`,已实现,本批不做。
- 后台敏感词管理、评论审核、举报工单处理:后台已有,本批不动。
- 评论图片/表情/@提及/无限嵌套、我的评论列表:本批不做。
- 并发点赞(TC-INTERACT-07):由后端幂等保证,客户端不压测,只做防连点。
### 四、UI 与规范约束(严格执行 specs)
- **l10n**:所有新增文案(评论/回复/审核中/举报原因/失败提示)进 `lib/l10n/app_{en,zh,id}.arb` 三份齐全并重新生成;禁止硬编码。
- **视觉**:沿用现有深色主题、`AppColors`、播放页右栏与底部弹窗样式;不自行改主题。
- **三态**:评论列表必须有加载、空、失败(可重试);禁止白屏或假装成功。
- **分层**:新增 `comment_api / interaction_api`(放 `lib/core/data/remote/`)、`comment_repository`、`comment_controller`;页面只依赖 controller/repository 抽象;沿用 K2 的分层与注入方式。
- **ID**:全部按字符串解析(`json_numbers.dart`),禁止 `as int`。
- **不要动**:K2 的分类/记录/收藏/下载逻辑、登录注册、播放进度核心;不改 `ReelShortWeb`、`FruitServer` 后端、specs。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法必须有文档注释;无 AI 痕迹;测试命名 `should_<期望>_when_<条件>`。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁(不达标不提交)
1. `dart analyze lib test` 零告警;`flutter test` 全过(存量不得跑红)。
2. `flutter build apk --debug` 通过;若缺 SDK,贴原始输出并标「未验证」。
3. 新增测试至少覆盖:评论游标翻页与 hasMore、发布成功/敏感词 BLOCK/REPLACE/REVIEW、删除自己的评论与失败回滚、点赞乐观更新与回滚、举报成功/重复举报 message 透传、未登录 AuthGate、评论点赞计数更新。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给“用户可照做步骤 + 每步预期”,禁止用开发机 `curl` 冒充设备证据。
5. Git:从 `develop` 拉 `feature/flutter-k3-interaction`,完成后 `git merge --no-ff` 回 `develop` 并 push;**不动 `main`**;只 `git add` 自己的文件。
### 六、节奏与留痕
- 先写 `FruitServer/docs/plans/2026-10-17-K3.md`:范围、影响文件、上述 3 个契约缺口、风险、待确认。
- TDD:每块先红灯 → 实现 → 绿灯 → 重构;每完成一块跑一次 `dart analyze lib test` + 定向测试。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`。
- 做完停下写回执:真数据/本地数据页面清单、用户验证步骤与预期、门禁原始输出、提交号。
### 七、K3 完成回执(2026-10-17)
**结论**:K3-A 评论、K3-B 点赞、K3-C 举报、K3-D 分享回归均已完成并停止,
未开始 K4。后端、`ReelShortWeb`、specs 均未修改。
#### 7.1 真数据 / 本地数据页面清单
真数据(走契约 §1.7):
- 播放页评论弹窗:匿名评论列表、游标加载更多、顶层评论、一层回复、
BLOCK/REPLACE/REVIEW 结果、本人删除、评论点赞。
- 播放页剧集点赞:`POST /app/likes/toggle` 切换,乐观更新与服务端状态校准;
**不展示剧集点赞数字**。
- 评论更多菜单与详情页更多菜单:评论/短剧举报,固定五项原因,可选描述。
本地/系统能力:
- 剧集 liked 仅保存在当前 App 会话内。因详情/卡片契约没有 `likeCount/liked`,
App 重启或换账号后不展示服务端初始点赞状态。
- 分享继续走 `PlatformShareService` 系统分享,不新增后端接口。
- “我的 → 喜欢”只过滤当前 feed 的会话内 liked,没有契约外加“我的点赞列表”接口。
按降级方案未做:拉黑、举报用户、举报后台处理、无限嵌套、图片/表情/@、我的评论列表。
#### 7.2 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:12 +242 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.8s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 `live_watch_flow_test` 在 8081 未启动时按测试机制跳过,不是失败。
#### 7.3 Git
- 功能提交:`d361034226ec2bc431548d45068dc60046e5e473`
`feat(flutter): 接入互动评论点赞举报`
- `--no-ff` 合并提交:`9db6935308e65342be46c81419c2992ed2df1df0`
`merge: 合并 K3 客户端互动能力`
- 举报本人错误透传修复:`1488ee8f85671de96c70347c1b17fb9c4556bd88`
`fix(flutter): 透传举报本人评论错误`
- 修复合并提交:`e1147cfcbabcdf457e427bbd118c32d37f8d9b15`
`merge: 合并 K3 举报错误透传修复`
- 已推送:`45f5f25..9db6935`、`9db6935..e1147cf develop -> develop`
- author 与 committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.4 用户真机验证步骤与预期
前置:启动 8081 客户端接口服务,准备可登录测试账号;在 Android 设备安装
`build/app/outputs/flutter-apk/app-debug.apk`,确认 App 使用设备可访问的后端地址。
以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 未登录打开任意有评论的剧集,点播放页评论入口。
预期:评论列表正常加载;空列表显示“还没有评论”;断网后显示失败与重试,不白屏。
2. 未登录点发布、评论点赞或举报,再点剧集点赞。
预期:均先出现登录提示;关闭提示后不发业务请求;登录后可重试。
3. 登录后发表顶层评论,再输入命中 `REPLACE` 或 `REVIEW` 策略的词。
预期:顶层评论出现在列表;REPLACE 展示服务端替换后的内容;REVIEW 显示“审核中”;
BLOCK 原样显示后端 message 且输入框内容保留。
4. 在顶层评论点“回复”并发布。
预期:请求带 `parentId`;回复归入该楼层,只做一层回复;点“查看回复”可展开。
5. 连续加载超过首屏 20 条的评论。
预期:按 `nextCursor` 续拉,不重复、不跳页;没有下一页时加载更多消失。
6. 点赞一条评论和当前剧集,再点一次取消。
预期:图标立即切换;评论计数在服务端响应后校准;断网时状态回滚并提示;
剧集点赞不显示数字。
7. 删除自己的评论:更多 → 删除评论 → 二次确认。
预期:成功后从列表移除;制造断网/服务失败时恢复原位置并提示。其他人的评论不显示删除项。
8. 举报评论与短剧:更多 → 举报;分别选择 `SPAM/ABUSE/PORN/AD/OTHER` 和可选描述。
预期:成功后提示“举报已提交,感谢反馈。”;重复举报原样显示后端
“你已举报过该内容,我们正在处理”;举报自己发布内容原样显示后端 message。
9. 点播放页分享。
预期:打开系统分享面板;文本顺序为标题、简介、`FruitShorts` footer;
切换 en/zh/id 后三类文案同步切换。
10. 在 Android logcat 保存评论/点赞/举报请求与响应,并保留关键页面截图。
预期:可见 `/app/comments*`、`/app/likes/toggle`、`/app/reports` 的真实响应;
设备侧取证由用户执行,开发机 `curl` 或本机返回 200 不替代本步骤。
#### 7.5 未验证与已知边界
- 真机、模拟器、设备日志、设备截图均未由 Codex 执行,状态为“设备侧取证待用户执行”。
- 剧集点赞初始状态和数字因契约缺口无法展示,App 重启后当前会话状态丢失。
- 敏感词具体词表、限流阈值、后台审核联动由后端/运营环境决定,本轮只做客户端透传与状态展示。
- 拉黑与 K4 不在本轮范围。
---
## [已处理·待用户验收] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI
**用户指令(2026-10-17)**:「现在先做客户端 对接相关接口 及ui 严格执行specs 内的要求」。
> **本批只做 K2,做完停下写回执,不要连做 K3/K4。**
> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A3/A5、`docs/16-测试用例库.md` CONTENT/RECORD、`docs/09-测试计划.md`);
> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.5 / §1.6,**契约里没有的接口一律不许自造**。
### 〇、开工前必读(按 specs 顺序,缺一不可)
1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏)
2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表)
3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛)
4. `FruitServer/docs/api-phase1.md` §0 / §1.3 / §1.5 / §1.6(接口唯一权威)
5. `docs/plans/codex-研发任务清单.md`(A3/A5)、`docs/16-测试用例库.md`(CONTENT/RECORD)、`docs/09-测试计划.md`
6. 本 INBOX 条目
**硬性执行口径(摘自上述规范,违反即打回)**
- **Git**:从 `develop` 拉 `feature/flutter-k2-library` 开发;提交信息 `type(scope): 描述`(`type` ∈ feat/fix/docs/test/refactor/style/perf/chore,≤50 字符,一个 commit 只做一件事);完成后 `git merge --no-ff` 回 `develop` 再 push;**禁止直接改 `main`**;只 `git add` 自己的文件,禁止 `git add -A`/`.`。
- **TDD**:先写测试(红灯)→ 实现(绿灯)→ 重构;测试命名 `should_<期望>_when_<条件>`;核心类(repository/service/logic)必配 `*_test.dart`。
- **Dart**:`provider` binding/logic/view 三层;`dio`;全程 `async/await`,**禁裸 `then()`**;IO 失败用结果对象/控制器错误字段承接,**不把异常直接抛到 UI**;禁止空 catch。
- **代码规模**:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10;除 `0/1/-1/null/空串/布尔` 外禁止魔法值。
- **注释**:每个 public 类/方法必须有文档注释;文件头写一行中文职责;新文件首行 `// Author: fruit.com.cn`;代码/注释/提交信息**不得出现任何 AI 痕迹**。
- **留痕**:每步「叙述 + 工具命令 + 原始输出 + 解读」,追加到 `docs/plans/PROGRESS.md`;做前先写 `docs/plans/2026-10-17-K2.md`(步骤/影响文件/风险/待确认)。
- **l10n 生成物**(`lib/l10n/app_localizations*.dart`)豁免作者头,但 `.arb` 必须手写三份。
### 一、PM 已勘察的现状(实测,直接用)
- `ReelShort`(`develop` `00280ca`)已接:`/app/home/feed`、`/app/discover/feed`、`/app/dramas/{id}`、`/app/episodes/{id}/play`、`/app/watch/progress`(POST/GET)、`/app/watch/recent`、`DELETE /app/watch/progress/{dramaId}`、`/app/favorites`(toggle/list/delete)、账号 4 个接口。
- **未接**:`GET /app/categories`、观看记录删除/清空、下载全部 8 个接口、`/app/me*`(属 K4)、互动(属 K3)。
- 现状缺陷:
- 发现页分类**是本地按 sections 下标硬编码**([discover_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/discover/discover_view.dart#L363)),不是 `GET /app/categories`;
- "我的-记录"只渲染**能在已加载首页/发现目录里匹配到**的剧([mine_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/mine/mine_view.dart#L634-L716)),匹配不到就消失;
- 收藏列表嵌在"我的"页签里,不能单独刷新/取消收藏;
- 下载**完全没有**。
### 二、本批范围(4 块,按顺序做)
#### K2-A 分类(A3 剩余)
- 接口:`GET /app/categories` → `[{ id: string, name: string }]`(J1:**id 是字符串**)。
- UI:发现页分类条改为**服务端分类**;点分类展示该分类的剧。
- 内容来源只允许 `GET /app/discover/feed` 的 `sections[]` 中 `code == "category-{id}"` 的分区(后端已按分类生成,见 `AppCatalogService#discoverFeed`)。
- 某分类在 discover 里没有对应分区 → 显示空态,**不要自己拼请求/自造接口**。
- 保留现有 `hot` / `new` 分区的入口与样式。
#### K2-B 观看记录页(A5 剩余)
- 接口:
- `GET /app/watch/recent?limit=20` → `[{ dramaId, episodeId, positionMs, durationMs, finished, playCount, updatedAt }]`(全部 ID 为字符串)
- `DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`(清空)
- **契约缺口(必须按下面方式处理,不许改后端)**:`recent` **不返回剧名/封面**。做法:
- 先去重 `dramaId`,再对每个 id 调 `GET /app/dramas/{id}` 补卡片;
- **并发上限 4**,结果按 dramaId 缓存(同页内不重复请求);
- 单条详情失败**不能拖垮整页**:该条显示占位(剧名"加载失败"+重试),其余正常渲染;
- 在计划文档里记为「契约缺口:建议后续在 `ProgressRespVO` 补卡片字段(需 specs 负责人改契约)」。
- UI:独立"观看记录"页(入口:我的 → 观看记录)
- 每条:封面、剧名、第 N 集、`positionMs/durationMs` 进度、更新时间;点击进详情并续看;
- 单条删除、清空(二次确认);删除**乐观更新**,失败回滚并提示;
- 未登录 → 复用现有 `AuthGate` 登录引导(接口需要登录,见 `SecurityConfig`);
- 三态:加载中 / 空("还没有观看记录")/ 失败(可重试)。
#### K2-C 收藏列表页(A5 剩余)
- 接口:`GET /app/favorites`(返回 `AppDramaCardVO[]`,**与首页卡片同结构**)、`DELETE /app/favorites/{dramaId}`、`POST /app/favorites/toggle`。
- UI:独立"我的收藏"页(我的 → 收藏),复用首页卡片渲染:
- 取消收藏(按钮或长按/左滑,二选一但要能发现)→ **乐观更新 + 失败回滚**;
- 点击进详情;空/错/载三态;未登录 → `AuthGate`。
- **契约缺口**:TC-RECORD-04 写"分页正确",但 `GET /app/favorites` **没有分页参数**、返回全量数组 → 本批按全量实现,计划文档登记该缺口,**不许自造 `page/size`**。
#### K2-D 下载授权与管理(A5 最后一块,**不做实际落盘**)
- 接口(§1.6,全部需登录;设备号优先请求体、其次 `X-Device-Id`,现有 `ApiClient` 已带):
| 方法 | 路径 | 本批用途 |
|---|---|---|
| GET | `/app/downloads/policy` | 配额/授权天数/wifiOnly/清晰度/已用/剩余/`expirePolicyText` |
| PUT | `/app/downloads/settings` | 仅 Wi-Fi、最高清晰度 |
| POST | `/app/downloads` | 申请授权(body `episodeId`,`quality` 可省=AUTO) |
| GET | `/app/downloads` | 我的下载授权列表 |
| GET | `/app/downloads/{episodeId}/url` | 续传前刷新短时效地址(本批只提供 API 方法) |
| POST | `/app/downloads/{episodeId}/progress` | 进度上报(本批只提供 API 方法) |
| DELETE | `/app/downloads/{episodeId}` | 删除单条 |
| DELETE | `/app/downloads` | 清空,返回条数 |
- UI:
- 详情页/播放页加"下载"入口:调 `POST /app/downloads`;**付费/未解锁/配额满时直接展示后端 `message`**,不要自造判断(判定归服务端);
- 我的 → 下载:策略卡(剩余配额、授权天数、`expirePolicyText`)、授权列表(剧名、集号、`status`、`taskStatus`、`progressPercent`、到期时间)、单条删除、清空、仅 Wi-Fi/清晰度设置;
- 状态枚举全部**后端返回值驱动**(`ACTIVE/EXPIRED/REVOKED`、`QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED`),前端不新增枚举;
- 空/错/载三态 + 未登录 `AuthGate`。
- **明确不做**(写进计划文档,避免误判"没做完"):文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验 —— 需要存储/权限/加密播放方案,另立批次。本批只交付"授权与状态管理"。
### 三、UI 与规范约束(严格执行 specs)
- **语言**:所有新增文案必须进 `lib/l10n/app_{en,zh,id}.arb` **三份齐全**并重新生成;禁止硬编码中文/英文。印尼语为首发语言。
- **视觉**:沿用现有深色主题与 `AppColors`/组件风格(specs 里未提供 Stitch 出稿;若用户后续给出稿,再单独做视觉替换,本批不自行改主题)。
- **三态**:每个新页面/列表都要有加载、空、失败(可重试);失败文案走 `errorText` + l10n,禁止白屏/假装成功。
- **分层**:新增 `category_api / download_api`(放 `lib/core/data/remote/`),页面只依赖 repository/controller 抽象;沿用 K1 的分层与依赖注入方式。
- **ID**:19 位雪花 ID 一律按字符串解析(`json_numbers.dart`),**禁止 `as int`**;计数/时长同理按契约类型处理。
- **不要动**:首页/发现已接 feed 逻辑(分类来源替换除外)、登录注册、播放/进度/收藏已有逻辑(除必要接线)、`ReelShortWeb`、`FruitServer` 后端。
- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。
### 四、门禁(不达标不提交)
1. `dart analyze lib test` **零告警**;`flutter test` **全过**(存量 33 个测试文件不得跑红)。
2. `flutter build apk --debug` 通过(CODING_STANDARDS §6 门禁总表含构建);若本机缺 Android SDK,回执里贴失败原始输出并标「未验证」,不得静默跳过。
3. 新增测试至少覆盖:分类→section 映射(含无分区空态)、记录页 N+1 详情请求去重/并发上限/单条失败降级、删除与清空回滚、收藏取消回滚、下载策略/授权列表渲染、未登录 AuthGate;核心类覆盖率 ≥80%。
4. **真机/设备侧验证由用户执行**(红线 7、10):回执里给"用户可照做的步骤 + 每步预期结果",**禁止用开发机 `curl` 冒充设备可达**;拿不到设备证据就标「未验证」。
5. Git:`feature/flutter-k2-library` 开发 → `git merge --no-ff` 回 `develop` → `git push origin develop`;**不动 `main`**;合并/推送前再跑一次门禁。
### 五、节奏与留痕
- 先写 `FruitServer/docs/plans/2026-10-17-K2.md`:范围、影响文件清单、上述 3 个契约缺口、风险、待确认。
- **TDD 节奏**:每块先写测试(红灯)→ 实现(绿灯)→ 重构;每完成一个模块跑一次门禁,零告警才继续。
- 每步在公司 `PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」(PM 验收口径,缺一即打回)。
- 做完**停下写回执**(追加到 `docs/plans/PROGRESS.md` + 本条下),回执必须含:
- 哪些页面已走真数据、哪些仍是本地/未做;
- 用户可照做的验证步骤与每步预期;
- 测试与 analyze 的原始输出;
- 提交号。
- 禅道:开工/完成各同步一次任务状态。
### 六、PM 登记:specs 契约缺口(Codex 不要动,反馈给项目负责人)
1. **搜索**:任务清单 A3 写了"搜索"、TC-CONTENT-05 有搜索用例,但 `api-phase1.md` **没有 `/app/search` 接口** → 本批**不做搜索**。
2. `GET /app/favorites` 无分页参数,与 TC-RECORD-04"分页正确"不一致。
3. `GET /app/watch/recent` 不返回剧名/封面,记录页需 N+1 详情请求(建议后端补卡片字段)。
> **用户裁定(2026-10-17)**:**先按现状契约执行,上述缺口后期再补**。本批不得因缺口停工,也不得临时改后端或自造接口;按第二节的降级方案实现,缺口留到后续批次。
### 七、Codex 回执(2026-10-17)
**状态**:K2-A/B/C/D 代码、自动化测试、Android debug 构建与 develop 推送已完成;
真机/模拟器侧业务验证**未验证**,按红线交用户执行。用户照做清单与测试跳过口径见
`docs/plans/PROGRESS.md`「动作 17:用户真机验收清单」。K3/K4 未开始。
**提交**
```
feature: cc4d7c5a9dc6a4f6f0565a32503cce5973af703c
merge: 45f5f25640a274edb51bd90b3b5e52c43ba9d9cd
branch: feature/flutter-k2-library -> develop(--no-ff,已 push,feature 已删除)
main: 未改动
```
**已接真实契约数据**
- 发现页分类:`GET /app/categories` + `GET /app/discover/feed`;
服务端分类只匹配 `category-{id}`,无分区显示空态。
- 独立观看记录页:`GET /app/watch/recent?limit=20`、按 dramaId 去重并发上限 4 补详情、
`DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`。
- 独立我的收藏页:`GET /app/favorites`、`POST /app/favorites/toggle`、
`DELETE /app/favorites/{dramaId}`。
- 我的下载页:§1.6 全部 8 个 `/app/downloads*` 接口;详情页/播放页下载入口调用
`POST /app/downloads`,拒绝时直接展示后端 `message`。
- 三页面均为加载 / 空 / 失败可重试三态;未登录显示登录引导。
**仍本地或未做**
- 搜索未做:契约没有搜索接口。
- 文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验未做;本批只交付授权与状态管理。
- 点赞/评论仍沿用既有本地状态,属 K3,不在本批。
- 发现 feed 当前只下发 `category-{id}`;若后端没有 `hot`/`new` 分区,保留的入口显示空态。
- 收藏接口仍返回全量数组,本批未发送 `page/size`。
**门禁原始输出**
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:11 +220 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 2,377ms
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有真实后端联调用例因本机 8081 服务未启动而按设计跳过,不是失败。
首次冷构建也成功,耗时 `705.7s`;最终修复后缓存构建 `2,377ms`。
**用户验证步骤与预期**
1. 在 ReelShort 执行 `git checkout develop && git pull origin develop`,确认 HEAD 为
`45f5f25`;后端 8081 已启动,手机与开发机在同一局域网。
2. 取局域网地址并运行:`IP=$(ipconfig getifaddr en0)`,
`flutter run -d --dart-define=API_BASE_URL=http://$IP:8081`。
预期:App 启动、首页/发现能加载;地址必须按当前网络重新获取。
3. 打开发现页:分类条应出现 `/app/categories` 返回的分类。选择一个有内容分类,
预期显示 discover feed 对应 `code=category-{id}` 的剧;选择无分区分类显示空态。
4. 播放任意免费集约 10 秒后返回,进入“我的 → 观看记录”。
预期:出现该剧封面/剧名/集号/进度/更新时间;即使该剧不在首页分区也能显示。
单条删除先立即消失;断网重试删除时,记录应恢复并提示失败。清空应先弹二次确认。
5. 在详情或播放页收藏一部剧,进入“我的 → 我的收藏”。
预期:卡片来自服务端;点粉色收藏按钮取消后立即消失;断网取消应恢复卡片并提示失败。
6. 登录后在详情页/播放页点下载入口。
预期:成功时提示“下载授权已就绪”;付费、未解锁或配额满时,直接出现后端 `message`。
进入“我的 → 下载”,预期策略卡显示剩余配额/授权天数/`expirePolicyText`;
列表显示后端 `status`、`taskStatus`、`progressPercent`、到期时间;
仅 Wi-Fi、清晰度、单删、清空可操作。
7. 退出登录后分别进入观看记录、我的收藏、我的下载。
预期:三页都显示登录引导,不显示网络错误或白屏。
**证据口径**
- 以上设备侧步骤尚未执行,因此真机/模拟器验证统一标记「未验证」。
- 没有用开发机 `curl` 冒充设备可达证据。
- 本批不包含实际文件下载,因此不能据此验收“离线播放”。
---
## [已处理] 2026-10-17 | 三库全量拉取 + `main` → `develop` 同步(Trae 执行,无代码改动)
**用户指令(2026-10-17)**:「先拉最新全量代码,三个库全拉,相关规范已经备齐、接口文档完备、INBOX更新、环境地址清单在docs下21-环境地址清单.md 同步main 分支到开发分支」。
### 一、执行结果(实测)
| 仓库 | 分支 | HEAD | 落后 origin/develop | 领先 | `origin/main` 未并入 | 工作区 |
|---|---|---|---|---|---|---|
| FruitServer | develop | `2d70858` | 0 | 0 | 0 | 干净 |
| ReelShort | develop | `00280ca` | 0 | 0 | 0 | 干净 |
| ReelShortWeb | develop | `3ec9ea6` | 0 | 0 | 0 | 干净 |
- 三库 `git fetch --all --prune` 后均无新提交,无需 `pull`。
- **ReelShort** 是唯一有 `main` 未同步的库:`eaf8fb4 style: 全量补充作者标识` 与 develop 改名(`nontonki`→`fruitshorts`)冲突 15 个文件。
解法:保留 develop 口径(`package:fruitshorts`)+ 按文件类型补回署名头(`.dart` → `//`,`.yaml` → `#`,`.xml` → ``);
`main` 版复活了 develop 已删除的 5 个文件(`test/catalog/demo_media_test.dart`、`test/catalog/home_feed_category_test.dart`、
`lib/pages/home/home_feed_sections.dart`、`lib/pages/home/widgets/home_feed_header.dart`、`lib/pages/home/widgets/category_swipe_region.dart`)→ 已 `git rm`;
`lib/l10n/app_localizations*.dart`(生成文件)不加署名头,已还原。
门禁:`dart analyze lib test` → `No issues found!`;提交 `00280ca`(作者 `jianghaifeng `),已推 `origin/develop`。
### 二、⚠️ 待 PM / 用户确认
1. **`docs/21-环境地址清单.md` 在 specs 仓库中不存在**。已核对:specs `docs/`(仅 01~20 + README + plans + 参考规范材料清单 + 代码现状盘点)、三个代码仓 `git ls-files`、specs 全部远端分支(只有 `origin/main`)—— 均无此文件。请确认是尚未推送,还是在别的路径/仓库。
2. specs 本地仍有 22 笔提交未推送到远端(无写权限,`origin/main` 停在 `c528345`)。若以远端为准,需要决定是否把本地回退到 `origin/main`。
### 三、用户裁定(2026-10-17)
1. **specs 只有 `main`,只需同步** → 已将本地 `specs` 强制对齐到 `origin/main`(`c528345`),本地领先的 22 笔未推送提交已丢弃;此后 specs **只拉取,不提交**。
2. **specs 是唯一权威** → 规范、接口契约、任务清单一律以 specs 为准;与本仓库文档冲突时以 specs 为准。
3. **执行前置动作** → Codex 每次开始任务前**必须先读本文件(INBOX)**,按条目顺序执行;新条目由 PM 追加,不覆盖历史。
---
## [已交接 · 未验证(真机验证改由用户执行)] 2026-10-16 20:30 | K1-verify:用**正确地址**重做真机验证(设备侧取证)
**用户指令(2026-10-16 20:20)**:「让 Codex 用正确地址重做一次真机验证」。
**背景(为什么必须重做)**:你 1-3/1-4 那批的真机验证用的是
`--dart-define=API_BASE_URL=http://10.157.242.1:8081` —— 该地址是**开发机某块 VPN 网卡(utun)的地址**,
现已不存在,**手机侧没有这块网卡**,所以按它构建、安装到手机上的 App **在真机上必然请求失败**
(用户实测提示「**剧目列表加载失败**」,报错里就显示着这个地址)。
更关键的是:当时那条"证据"是**在开发机上 `curl` 本机网卡地址拿到 200** —— 本机 curl 本机必然 200,
**不构成真机可达的证据(假阳性)**。PM 已把 specs 里 4 处记录修正并作废该结论(`481d4bb`)。
### 一、重做验证(**必须设备侧取证**,见红线第 10 条)
1. **先取真实地址,不要硬编码**:`ipconfig getifaddr en0` 拿开发机局域网 IP(PM 实测当前 `192.168.31.187`;**换网络会变**)。
2. **前置条件**:手机与开发机在**同一局域网**;iPhone 已授权「**本地网络**」权限(设置 → 隐私与安全性 → 本地网络)。
3. 真机跑:`flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://<步骤1得到的IP>:8081`
4. **证据至少一条来自设备侧**(优先级从高到低):
- **首选:加设备侧集成测试**(`integration_test` 依赖 + `integration_test/` 目录),
用 `flutter test integration_test -d --dart-define=API_BASE_URL=http://:8081` 在**手机进程里**
真实请求 `/app/home/feed` 与 `/app/dramas/{id}`,断言 `code=0` 且列表非空。
—— 这是可重复、可入库的最硬证据,也顺带把"设备验证"变成可回归的。
- 或:App 运行时抓**从手机 IP 到 8081 的已建立连接**(`lsof -nP -iTCP:8081 | grep <手机IP>`),
并**同时**给出后端侧确实收到该请求的证据。
- 或:手机侧截图(`device` 截图不支持无线 iPhone —— 那就用设备侧测试或连接证据替代)。
5. **若都拿不到:如实标「未验证」**,并说明卡在哪一步(同网段?权限未授权?),**不许含糊**。
6. 顺带确认原故障已消失:首页「剧目列表」能出内容(或由步骤 4 的测试断言覆盖)。
### 二、留痕要求(**只写本仓库,别动 specs**)
- `docs/plans/PROGRESS.md`(本仓库 FruitServer 这份)**追加**逐动作留痕:命令 + 原始输出 + 解读。
- 本条下写回执,含**用户可照做的验证步骤 + 每步预期结果**,并**明确标注"证据来自设备侧"**。
- **不要去改 specs 仓库**(它只拉取;那 4 处 PM 已修正,别重复改)。
### 三、门禁与 git
- `dart analyze lib test` 零告警 + `flutter test` 全过(新增的 integration_test 一起跑;若它依赖真机、无法在纯本机跑,就在回执里说明并给出真机执行命令与输出)。
- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`**;新文件首行 `// Author: fruit.com.cn`。
### 四、节奏
做完**停下写回执**,不要连做下一批。
---
## [已处理·待用户验收] 2026-10-16 19:40 | K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整
**用户指令(2026-10-16 19:30)**:「现在你要做的事对接客户端接口及 UI 调整,仍然严格按照 specs 执行」→ 转到 **客户端 `ReelShort`(Flutter)**,按 specs 阶段 A 推进。
### 一、PM 已勘察的现状(实测,直接用)
**客户端目前只调了 6 个 C 端接口**(契约里已实现 **44 个**):
```
/app/home/feed /app/discover/feed
/app/auth/register /app/auth/login /app/auth/refresh /app/auth/logout
```
- `lib/core/data/remote/` 下**只有** `api_client / api_exception / api_interceptors / auth_api / catalog_api` —— **没有** playback / watch / library / interaction / me 的 api 类。
- `WatchHistoryRepository` 是 **`SharedPreferencesWatchHistoryRepository`(本地存储)**,`mine_logic` 还在用 **`demoProfileId`** 假数据 —— 观看进度/续看位置**全在本机**,没接 `/app/watch/*`。
- `EngagementRepository` 是 `SharedPreferences` / `InMemory` —— 点赞/收藏状态**也是本地假数据**。
- 已有:`dio` 网络层 + 拦截器(A1 ✅)、改名 + 印尼语(✅)、`json_numbers.dart` 兼容字符串 id(J1 ✅)。
### 二、本批范围(K1:一条完整看剧闭环)
> 目标:**从首页点进一部剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏**,全链路走真接口。
1. **详情(A3 的一块)**:新增 `catalog_api` 方法对接 `GET /app/dramas/{id}`(含剧集列表;**免费集带已签名播放地址**)。
2. **播放(A4)**:新增 `lib/core/data/remote/playback_api.dart`
- `GET /app/episodes/{id}/play` → 票据 + 签名地址。**签名地址原样使用,不要拼接/改写**(文档硬要求)。
- 起播前取票据;票据/签名地址有有效期,**过期要重新取**,不要缓存到失效后还用。
3. **进度与续看(A5 的一块)**:
- `POST /app/watch/progress`(**每 10s 一次**,切集/退出/切后台各补一次)
- `GET /app/watch/progress?dramaId=`(进入详情/播放时定位续看位置)
- `GET /app/watch/recent?limit=20`("我的"页最近观看)
- **把 `WatchHistoryRepository` 的本地实现换成远程实现**(保留抽象接口,测试注入内存假实现);`mine_logic` **去掉 `demoProfileId`**。
4. **收藏(A5 的一块)**:`POST /app/favorites/toggle`、`GET /app/favorites`、`DELETE /app/favorites/{dramaId}`;收藏状态**以服务端为准**(不要再用本地 `EngagementRepository` 存收藏)。
5. **UI 调整**(本批必须一起做):
- 详情页 / 播放页 / "我的"页:**加载中 / 空 / 失败**三态齐备(失败要能重试,不要白屏或假装成功)
- 进度条与**续看定位**、收藏按钮的真实状态与乐观更新回滚
- 断网/超时的提示文案走 l10n(en / zh / id 三份都要有)
### 三、口径与约束(照 specs)
- **ID 是字符串**(J1 刚统一):解析走 `json_numbers.dart`,不要直接 `as int`。**计数类**(`viewCount`/`expiresIn`)也是字符串。
- `accessType` 等枚举**用后端返回值驱动 UI**(免费/付费/试看),**不要在前端写死**;付费集解锁不在本批(属 B2)。
- 接口层与业务层分离:**新增 api 类**、controller/logic 只依赖 repository 抽象(沿用现有 `catalog_api` + `catalog_controller` 的分层做法)。
- **不要动**已对接的首页/发现/登录注册逻辑(除必要的最小接线)。
- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。
### 四、门禁
- `dart analyze lib test` **零告警** + `flutter test` **全过**(存量 27 个测试文件不得跑红;新增测试要覆盖:票据过期重取、进度上报节流、续看定位、收藏切换回滚、失败态)
- **真实后端联调**(8081):跑一次端到端并留证据 —— 首页 → 详情 → 播放 → 上报进度 → 重新进入看到续看位置 → 收藏出现在收藏列表
- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`**
### 五、节奏
先写 `docs/plans/2026-10-16-K1.md`(计划 + 影响面 + 风险)→ 做 → 停下写回执(含**用户可照做的验证步骤 + 每步预期结果**,以及"哪几个页面现在走真数据、哪几个还是假的"的清单)。**不要连做 K2。**
> **后续批次(预告,不用现在做)**:**K2** = 记录/最近观看页 + 收藏列表页 + 分类/搜索 + 下载授权(A5/A3 剩余);**K3** = 互动(评论/点赞/举报/分享,A6);**K4** = 账号剩余(资料/设备/注销;**Google 登录需 Google OAuth 凭据**,属待你提供的付费/密钥类)。
---
## [已处理 · 待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一
> **PM 独立复核(Trae 2026-10-16 19:05)—— 交付 `FruitServer f35db2c` / `ReelShortWeb 3ec9ea6` / `ReelShort 94ff9f1`(三仓均在 `develop` 且与远端同步),验证通过。**
> - **门禁复跑**:`mvn test` **110 → 115 条全过**(commerce 33 / admin-api **16→21** / app-api 49 / worker 12)+ BUILD SUCCESS ✅
> - **全局配置落位核实**:`fruitshorts-framework/.../web/JacksonLongToStringConfig.java` 存在(一处生效两端,未改 yml)✅
> - **端到端真实验证(PM 亲自跑,非采信回执)**:新注册用户雪花 id `2111018722574602241` →
> ① `/admin/app-users/page` 返回 **`"id":"2111018722574602241"`(字符串)**、**无裸数字大 id**、`"total":2` **仍是数字**(符合约定);
> ② 用该 id 调 `/admin/app-users/{id}` → **`code=0`**(修复前此场景即 `200002`);
> ③ **真 node 逐位比对**:`typeof parsed.id === 'string'`、`String(parsed.id)` 与库值**逐位相等 = true**;测试数据已清理(残留 0)。
> - **按红线第 7 条,最终验收权仍归用户。**
>
> ⚠️ **三处口径待用户点头(PM 意见附后)**:
> 1. **唯一例外 `PageResult.total` 保持数字** —— PM 意见:**接受**(它是条数不是 id,前端分页组件要数字);但需在契约 §0「ID 一律字符串」处**显式写出这个例外**,否则前端会困惑。待 Codex 补一句。
> 2. **客户端改动比"只改 id 解析"略宽(物理上必须)** —— Codex 查明 Flutter 的 id 本来就是 `toString()` 不会崩,真正会崩的是被全局规则扫到的 **Long 计数**,故只改了 `expiresIn`、`viewCount` 等 3 处 + 新增 `json_numbers.dart`。PM 认可这个判断与最小改动口径。⚠️ **另需用户知晓**:`ReelShort` 原先**只有 `main`**,Codex 新建了 `develop` 并推送;若客户端同事仍在 `main` 上开发,这笔需要带过去。
> 3. **Codex 自我纠正了一处统计错误** —— 它第一版清理脚本用 `psql ... returning 1 | wc -l` 数行数,数到的是 psql 命令标签(删 0 行也输出 1),那轮"删除 1 行"是假数字;随后用**全库扫描**复核(uid/邮箱/雪花 id 零命中)确认无残留。PM 认可其如实披露。
>
> 📌 **仍挂着**:禅道里同样**没有 J1 任务**(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),无可同步状态。
>
> ⚠️ **PM 顺手说明**:为做端到端验证,8080(PID 80375)/ 8081(80383)/ 3005 均为 Codex 重启后的新 jar,接口与页面正常。
**用户裁定(2026-10-16 18:10)**:雪花 id 精度问题 **「按方案 (a) 全局修改,统一契约」**。
**背景(两方独立复现同一根因,故现在铺全局)**:
- 你在 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`
- 测试:新增 `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 "`,
拿新用户的 `id`(预期是 `"2111…"` 这种**带引号的字符串**、`total` 是**不带引号的数字**),再
`curl -s "http://127.0.0.1:8080/admin/app-users/" -H "Authorization: Bearer "` → 预期 `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=''"` **逐位相同**。
对照:`node -e 'console.log(String(JSON.parse("2111015748376858626")))'` → `2111015748376858620` 附近(会被改写),说明精度问题真实存在。
4. **后台界面(:3005)**:`admin/admin123` 登录 → 商品配置新建一个商品 → **点它那一行的状态/编辑**,
预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是 `0`。
5. **短剧管理 → 剧集管理带参跳转**:从短剧管理点某剧的「剧集」,URL 带 `?drama=`,
预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。
6. **客户端**:`cd ReelShort && flutter test`(预期 All tests passed);有设备时 `flutter run` 打开首页看播放量/详情
不报解析异常(后端 `viewCount` 现在下发字符串)。
7. **异常路径**:请求体里 id 传**数字**(老客户端行为)也应被接受:`PUT /admin/products//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`)并叫停。
- 因此**在用户重跑之前,本批一律按「未验证」计**;上面那次的历史输出只作参考,不作为结论。