63 KiB
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 共守)
- 单一写者:同一时刻只有一个 agent 在改代码/写 PROGRESS/跑门禁。
- 留痕:谁干活谁写
docs/plans/PROGRESS.md(命令 + 原始输出 + 解读,逐动作追加)。Trae 接手时用「【Trae 接手】」标记。- git 不冲突:提交前先
git status/git log;只git add自己的文件,禁止git add -A/.;不夹带对方改动。- 文件不覆盖:写 INBOX/PROGRESS 前先读最新,追加不覆盖。
- 求助通道:遇到阻塞/不确定,在本文件顶部写「【待 Trae 协助】+ 问题描述」。
- 禅道同步:任务「开始/完成」时同步更新禅道任务状态。
- 验收归用户:每批做完停在干净交接点,由用户亲自验证;回执必须含「用户可照做的验证步骤 + 每步预期结果」。
- 门禁(不达标不提交):后端 =
mvn test全过(核心接口须有契约测试);Web =pnpm build:pro通过 + eslint 零错误;Flutter =dart analyze lib test零告警 +flutter test全过。- 分支:在
develop提交并git push origin develop;绝不动main(dev→main 走 PR,由用户操作)。- 设备验证必须「设备侧取证」(2026-10-16 新增,血的教训):真机 / 模拟器 / 小程序等目标设备的验证,证据必须产自设备侧(设备侧集成测试、设备日志、设备截图、设备侧抓到的连接)。禁止用"在开发机上
curl某地址返回 200"证明设备可达 —— 本机 curl 本机网卡地址必然 200,这是假阳性(已实际误导过一次:10.157.242.1是开发机 VPN 网卡地址,手机根本不可达,导致真机「剧目列表加载失败」)。拿不到设备侧证据的,一律标「未验证」,不得写成"已验证"。
[待处理] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI
用户指令(2026-10-17):「现在先做客户端 对接相关接口 及ui 严格执行specs 内的要求」。
本批只做 K2,做完停下写回执,不要连做 K3/K4。 规范以 specs 为准(
docs/plans/codex-研发任务清单.md阶段 A3/A5、docs/16-测试用例库.mdCONTENT/RECORD、docs/09-测试计划.md); 接口只认FruitServer/docs/api-phase1.md§1.3 / §1.5 / §1.6,契约里没有的接口一律不许自造。
一、PM 已勘察的现状(实测,直接用)
ReelShort(develop00280ca)已接:/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),不是
GET /app/categories; - "我的-记录"只渲染能在已加载首页/发现目录里匹配到的剧(mine_view.dart),匹配不到就消失;
- 收藏列表嵌在"我的"页签里,不能单独刷新/取消收藏;
- 下载完全没有。
- 发现页分类是本地按 sections 下标硬编码(discover_view.dart),不是
二、本批范围(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); - 三态:加载中 / 空("还没有观看记录")/ 失败(可重试)。
- 每条:封面、剧名、第 N 集、
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/清晰度/已用/剩余/ expirePolicyTextPUT /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_<条件>。
四、门禁(不达标不提交)
dart analyze lib test零告警;flutter test全过(存量 33 个测试文件不得跑红)。- 新增测试至少覆盖:分类→section 映射(含无分区空态)、记录页 N+1 详情请求去重/并发上限/单条失败降级、删除与清空回滚、收藏取消回滚、下载策略/授权列表渲染、未登录 AuthGate。
- 真机/设备侧验证由用户执行(红线 7、10):回执里给"用户可照做的步骤 + 每步预期结果",禁止用开发机
curl冒充设备可达;拿不到设备证据就标「未验证」。 - 在
ReelShortdevelop提交并git push origin develop;不动main。
五、节奏与留痕
- 先写
FruitServer/docs/plans/2026-10-17-K2.md:范围、影响文件清单、上述 3 个契约缺口、风险。 - 做完停下写回执(追加到
docs/plans/PROGRESS.md+ 本条下),回执必须含:- 哪些页面已走真数据、哪些仍是本地/未做;
- 用户可照做的验证步骤与每步预期;
- 测试与 analyze 的原始输出;
- 提交号。
- 禅道:开工/完成各同步一次任务状态。
六、PM 登记:specs 契约缺口(Codex 不要动,反馈给项目负责人)
- 搜索:任务清单 A3 写了"搜索"、TC-CONTENT-05 有搜索用例,但
api-phase1.md没有/app/search接口 → 本批不做搜索。 GET /app/favorites无分页参数,与 TC-RECORD-04"分页正确"不一致。GET /app/watch/recent不返回剧名/封面,记录页需 N+1 详情请求(建议后端补卡片字段)。
[已处理] 2026-10-17 | 三库全量拉取 + main → develop 同步(Trae 执行,无代码改动)
用户指令(2026-10-17):「先拉最新全量代码,三个库全拉,相关规范已经备齐、接口文档完备、INBOX更新、环境地址清单在docs下21-环境地址清单.md 同步main 分支到开发分支」。
一、执行结果(实测)
| 仓库 | 分支 | HEAD | 落后 origin/develop | 领先 | origin/main 未并入 |
工作区 |
|---|---|---|---|---|---|---|
| FruitServer | develop | 2d70858 |
0 | 0 | 0 | 干净 |
| ReelShort | develop | 00280ca |
0 | 0 | 0 | 干净 |
| ReelShortWeb | develop | 3ec9ea6 |
0 | 0 | 0 | 干净 |
- 三库
git fetch --all --prune后均无新提交,无需pull。 - ReelShort 是唯一有
main未同步的库:eaf8fb4 style: 全量补充作者标识与 develop 改名(nontonki→fruitshorts)冲突 15 个文件。 解法:保留 develop 口径(package:fruitshorts)+ 按文件类型补回署名头(.dart→//,.yaml→#,.xml→<!-- -->);main版复活了 develop 已删除的 5 个文件(test/catalog/demo_media_test.dart、test/catalog/home_feed_category_test.dart、lib/pages/home/home_feed_sections.dart、lib/pages/home/widgets/home_feed_header.dart、lib/pages/home/widgets/category_swipe_region.dart)→ 已git rm;lib/l10n/app_localizations*.dart(生成文件)不加署名头,已还原。 门禁:dart analyze lib test→No issues found!;提交00280ca(作者jianghaifeng <haifeng_hello@163.com>),已推origin/develop。
二、⚠️ 待 PM / 用户确认
docs/21-环境地址清单.md在 specs 仓库中不存在。已核对:specsdocs/(仅 01~20 + README + plans + 参考规范材料清单 + 代码现状盘点)、三个代码仓git ls-files、specs 全部远端分支(只有origin/main)—— 均无此文件。请确认是尚未推送,还是在别的路径/仓库。- specs 本地仍有 22 笔提交未推送到远端(无写权限,
origin/main停在c528345)。若以远端为准,需要决定是否把本地回退到origin/main。
三、用户裁定(2026-10-17)
- specs 只有
main,只需同步 → 已将本地specs强制对齐到origin/main(c528345),本地领先的 22 笔未推送提交已丢弃;此后 specs 只拉取,不提交。 - specs 是唯一权威 → 规范、接口契约、任务清单一律以 specs 为准;与本仓库文档冲突时以 specs 为准。
- 执行前置动作 → 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 条)
- 先取真实地址,不要硬编码:
ipconfig getifaddr en0拿开发机局域网 IP(PM 实测当前192.168.31.187;换网络会变)。 - 前置条件:手机与开发机在同一局域网;iPhone 已授权「本地网络」权限(设置 → 隐私与安全性 → 本地网络)。
- 真机跑:
flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://<步骤1得到的IP>:8081 - 证据至少一条来自设备侧(优先级从高到低):
- 首选:加设备侧集成测试(
integration_test依赖 +integration_test/目录), 用flutter test integration_test -d <device> --dart-define=API_BASE_URL=http://<IP>:8081在手机进程里 真实请求/app/home/feed与/app/dramas/{id},断言code=0且列表非空。 —— 这是可重复、可入库的最硬证据,也顺带把"设备验证"变成可回归的。 - 或:App 运行时抓从手机 IP 到 8081 的已建立连接(
lsof -nP -iTCP:8081 | grep <手机IP>), 并同时给出后端侧确实收到该请求的证据。 - 或:手机侧截图(
device截图不支持无线 iPhone —— 那就用设备侧测试或连接证据替代)。
- 首选:加设备侧集成测试(
- 若都拿不到:如实标「未验证」,并说明卡在哪一步(同网段?权限未授权?),不许含糊。
- 顺带确认原故障已消失:首页「剧目列表」能出内容(或由步骤 4 的测试断言覆盖)。
二、留痕要求(只写本仓库,别动 specs)
docs/plans/PROGRESS.md(本仓库 FruitServer 这份)追加逐动作留痕:命令 + 原始输出 + 解读。- 本条下写回执,含用户可照做的验证步骤 + 每步预期结果,并明确标注"证据来自设备侧"。
- 不要去改 specs 仓库(它只拉取;那 4 处 PM 已修正,别重复改)。
三、门禁与 git
dart analyze lib test零告警 +flutter test全过(新增的 integration_test 一起跑;若它依赖真机、无法在纯本机跑,就在回执里说明并给出真机执行命令与输出)。- 在
ReelShortdevelop提交并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:一条完整看剧闭环)
目标:从首页点进一部剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏,全链路走真接口。
- 详情(A3 的一块):新增
catalog_api方法对接GET /app/dramas/{id}(含剧集列表;免费集带已签名播放地址)。 - 播放(A4):新增
lib/core/data/remote/playback_api.dartGET /app/episodes/{id}/play→ 票据 + 签名地址。签名地址原样使用,不要拼接/改写(文档硬要求)。- 起播前取票据;票据/签名地址有有效期,过期要重新取,不要缓存到失效后还用。
- 进度与续看(A5 的一块):
POST /app/watch/progress(每 10s 一次,切集/退出/切后台各补一次)GET /app/watch/progress?dramaId=(进入详情/播放时定位续看位置)GET /app/watch/recent?limit=20("我的"页最近观看)- 把
WatchHistoryRepository的本地实现换成远程实现(保留抽象接口,测试注入内存假实现);mine_logic去掉demoProfileId。
- 收藏(A5 的一块):
POST /app/favorites/toggle、GET /app/favorites、DELETE /app/favorites/{dramaId};收藏状态以服务端为准(不要再用本地EngagementRepository存收藏)。 - 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):跑一次端到端并留证据 —— 首页 → 详情 → 播放 → 上报进度 → 重新进入看到续看位置 → 收藏出现在收藏列表
- 在
ReelShortdevelop提交并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 test110 → 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 意见附后):
- 唯一例外
PageResult.total保持数字 —— PM 意见:接受(它是条数不是 id,前端分页组件要数字);但需在契约 §0「ID 一律字符串」处显式写出这个例外,否则前端会困惑。待 Codex 补一句。- 客户端改动比"只改 id 解析"略宽(物理上必须) —— Codex 查明 Flutter 的 id 本来就是
toString()不会崩,真正会崩的是被全局规则扫到的 Long 计数,故只改了expiresIn、viewCount等 3 处 + 新增json_numbers.dart。PM 认可这个判断与最小改动口径。⚠️ 另需用户知晓:ReelShort原先只有main,Codex 新建了develop并推送;若客户端同事仍在main上开发,这笔需要带过去。- 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(不相等)。根因是 JSNumber.MAX_SAFE_INTEGER = 9007199254740991,19 位雪花 id 必然超出;而服务端没有全局Long→String序列化配置。 - 种子数据都是小 id(4/5/25)所以老页面一直正常 —— 这是只要"新建后立刻拿 id 操作"就必然踩的坑。
一、后端:全局序列化(放 fruitshorts-framework,两个入口都生效)
Long.class与Long.TYPE一律用ToStringSerializer。- ⚠️ 不要动
Integer/int(分页pageIndex/pageSize等保持数字)。 - 必须覆盖:普通字段、嵌套对象、
List/Map的值、分页list。 - 反序列化方向也要验:请求体 / 路径变量 / 查询参数里 id 传字符串要能正确绑定成
Long(前端回传的就是字符串)。
二、契约统一(docs/api-phase1.md §0 通用约定)
- 写明「所有 ID 字段一律以字符串下发」(含主键与外键 id),并给示例。
- 把 I1 里逐模块的描述(商品/公告 VO 各自加
ToStringSerializer)收拢到这条总口径,不要各写一套。 - 顺手把 §2.11/§2.12 里与总口径重复的表述改成引用 §0。
三、两个前端适配
- ReelShortWeb:审计现有页面有没有把 id 当数字用 ——
parseInt(id)/Number(id)/id + 1/ 拿 id 当数组下标 /===与数字比较 /sort((a,b)=>a.id-b.id),逐处改成字符串语义。本轮新加的两个页面已按string写,别回退。 - ReelShort(Flutter):
fromJson里json['id'] as int这类写法在字符串下发时会直接抛异常,必须审计并修。策略二选一(推荐前者,改动小且向前兼容):- 兼容解析:统一按"接受字符串或数字"的方式解析(如
int.tryParse(v.toString())),或把模型里的 id 保留为String; - 或全量改成
String(改动更大,要与客户端同事对齐)。 - ⚠️ 客户端现由另一同事在推进:只做"ID 解析"这一类最小改动,不要顺手改别的;有分歧或改不动就写「【待 Trae 协助】」,并在回执里给出逐文件清单。
- 兼容解析:统一按"接受字符串或数字"的方式解析(如
四、验证(必须有证据,不接受"应该没问题")
- 后端契约测试:
Long字段确实以字符串输出(含嵌套 / 分页 list / Map)。 - 端到端复现 I1 那个 bug 并证明已消失:admin 新建一条记录 → 用响应里的 id 调详情 / 编辑 / 上下架 → 成功(全局范围内,不再只限商品/公告)。
- JS 侧一致性证据:拿真实响应喂
node -e 'const d=JSON.parse(...); console.log(String(d.id))',证明与库里的值逐位相等。 - C 端:注册 / 播放票据 / 订单等返回的 id 也是字符串,且客户端解析不崩(Flutter 侧跑测试)。
五、门禁与 git
mvn test存量 110 条不得跑红 + 新增测试;Webpnpm 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)
- 契约先行(依据
12-上线检查清单§一):先在docs/api-phase1.md写清两个模块的接口、字段、错误码、权限点(放在 §2.6 订单与权益之后,或新增一节),再动代码。 - 商品配置
/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
- 系统公告
/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 端能查到、过期/下架后查不到 - 权限点 + 菜单同上
- 菜单与权限点落库:写进
sql/data-phase1.sql(幂等),注意沿用既有保留段、不要与现有 id 冲突(现有菜单到 id 30)。建议:商品配置挂订单与权益(id 22) 下;系统公告可挂新的运营分组,具体由你按现有约定定。超管必须可见且可授权。 - Swagger 中文注解按 S1 规范补齐(
@Tag/@Operation)。 - 门禁:
mvn test存量 91 条不得跑红 + 新增契约测试,至少覆盖:store_product_id同平台唯一、code唯一、type校验、公告发布后 C 端可见 / 过期或下架后不可见、越权(无权限点被拒)。
三、后台页面(ReelShortWeb)
挂载机制(PM 已勘察):路由是静态注册在
src/router/index.ts(component: () => import('@/views/...')),而菜单树来自 DBsys_menu,其中component字段必须与视图路径一致(例:views/contentManagement/DramaManagementView)。两边都要加。
- 新增两个页面(放在与现有页面同级的目录,命名沿用
XxxManagementView.vue风格):- 商品配置:列表(分页/筛选)+ 新建/编辑弹窗 + 上下架 + 删除;表单字段按
product表;金额/金币用数字输入并做前端校验 - 系统公告:列表 + 新建/编辑(标题/正文/类型/目标/发布时间/失效时间)+ 发布/下架 + 删除
- 商品配置:列表(分页/筛选)+ 新建/编辑弹窗 + 上下架 + 删除;表单字段按
- 按现有约定写:API 封装放
src/api/admin/(新增如product.ts/notice.ts);权限码走现有的can(code);表格状态列用接口返回的status/statusText,不要在前端写死中文;大数值用formatCount。 src/router/index.ts静态注册路由(route_name与 DBsys_menu.route_name一致,注意不要与既有 route_name 重名 —— 之前踩过CreatorCenter重名导致白屏/404 的坑)。- 不要改动已对接的既有页面(避免与其他人正在做的改动冲突)。
- 门禁:
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 条)
- 先写计划文件
docs/plans/2026-10-16-I1.md(后端 + 前端两部分、步骤、风险、待确认)。 - 做,跑门禁。
- 停在干净交接点写回执:逐项列出改了什么;给你和用户可照做的验证步骤 + 每步预期结果(含「后台登录 → 菜单里能看到这两个新页面 → 建一个商品 → 用该商品在 C 端 verify 能对上」这样的端到端链路)。
- 不要连着抢做下一批。
回执(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;截图见下面步骤 |
交付内容
- 后端:
/admin/products6 个接口(page/{id}/POST/PUT/{id}/status/DELETE)、/admin/notices6 个接口;字段严格按product/ops_notice表现有结构,未改表。 - 校验:
code全局唯一、storeProductId同 platform 唯一、COIN_PACK需coinAmount>0、VIP需vipDays>0、新建默认下架。 - 公告可见性与 C 端共用一份实现:
OpsNoticeService.pageForApp(...),后台与/app/notices不再各写一套条件。 - 权限点
product:list/product:update/notice:list/notice:update+ 菜单 31(商品配置,挂 22「订单与权益」)/ 32(运营管理)/ 33(系统公告),超管已授权,幂等。 - 前端:
src/api/admin/product.ts、src/api/admin/notice.ts、ProductManagementView.vue、NoticeManagementView.vue;状态列只用接口statusText,不写死中文。
两个自己拍的口径(请复核)
- VIP 商品:允许建档、不允许启用 ——
status=1时回400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)。理由:上线日可以直接上下架,不必临时补商品数据;已写进契约 §2.11。 - ⚠️ 雪花 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 的地方会受影响)——这是平台级改动,请裁定,我没自行铺开。
- 菜单挂载机制与条目描述有出入(如实报出):实测菜单来自 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(可重复执行)。
- 菜单:
pnpm dev→ 打开http://127.0.0.1:3005/#/login→admin / admin123登录。 预期:左侧出现「订单与权益 → 商品配置」与「运营管理 → 系统公告」,其余菜单不变。 - 商品页:进
#/commerce/products。预期:3 行种子(coin_100/550/1200),列 = 商品编码/名称/类型/平台/商店商品 id/到账金币/价格/排序/状态/创建时间/操作;状态显示「已上架」。 - 新建商品(正常路径):点「新建商品」→ 编码
uitest_coin_500、名称随意、平台选 Google Play、商店商品 idUITEST_COIN_500、金币 500、赠送 50、价格 9900 → 保存。 预期:toast「商品已创建(默认下架,请确认后再上架)」,列表新增一行且状态「已下架」(id 是雪花 id,注意这是本轮修过的点)。 - 异常路径:再新建一个同编码商品 → 预期 toast「商品编码已存在:uitest_coin_500」;把类型改成「会员(第二阶段)」、天数 30 → 保存成功(允许建档),再点它的状态想上架 → 预期 toast「会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)」,状态不变。
- C 端对齐(命令):把第 3 步的商品点上架,然后
预期:查到 1 行、
psql -c "select id, code, platform, store_product_id, status from product where store_product_id='UITEST_COIN_500';"status=1—— 这正是/app/orders/verify找商品的同一条件。 (POST /app/orders/verify本机默认STORE_VERIFIER=disabled,会先回200003 商店校验尚未配置,这是 B1-1 的既定行为,不是商品没配上。) - 公告页:进
#/operation/notices→ 「新建公告」填标题/正文(默认「全部用户/系统」、不填时间=立即发布)→ 保存。 预期:toast「公告已发布」,行内状态「已发布」、失效时间显示「-」。curl -s "http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=5" # 预期 total 1,能看到刚发的标题 - 下架(异常路径的对照组):点该行状态「已发布」→ 预期 toast「公告已下架」、行内变「已下架」;再跑上面的 curl → 预期
total 0。 - 越权:用
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,...}。 - 清理:把第 3/4 步的商品、第 6 步的公告删掉;核对
select count(*) from product;= 3、select count(*) from ops_notice;= 0。 (本轮自测数据我已清干净:product3 行种子、ops_notice0 行。)
门禁/流程说明:本轮所有子步骤都在同一批内完成,未抢做下一批;mvn test 与 package 都是先停服务再打包、打包后重新起服务、对着活着的服务做的 curl 自测(不是对停掉的服务打)。
服务当前状态:8080/8081 我这边是已起的(自测完成时仍在监听);若你这边连不上就是被环境回收了,按上面「准备」两条命令起回来即可。
禅道同步(红线第 6 条,如实说明):我把 水果短剧APP(项目 2)的 4 个执行下的任务都拉了一遍,
任务只有 S0/S1/S0-16、A1–A6、B1/B2、C1–C3 —— 没有 I1(后台管理端 商品配置 + 系统公告)这一条,
所以本轮没有可同步的任务号,我没有动任何既有任务的状态(避免误改 B1/C1/A* 这些别的批次)。
下一步请给 I1 建任务并告知任务号,我补一次状态同步(codex01 的 token 已可用)。
回执(Codex,2026-10-16):J1 完成,停在干净交接点
提交(三个仓库各自 develop,main 未动)
- FruitServer
f35db2c(develop,已推):全局 Long→String + 契约 §0 + 测试 - ReelShortWeb
3ec9ea6(develop,已推):4 处 id 数字语义适配 - ReelShort
94ff9f1(新建 develop,已推):客户端 Long 计数兼容解析 留痕:docs/plans/PROGRESS.md(本轮追加 J1 段,含原始输出);计划docs/plans/2026-10-16-J1.md。
门禁数字
| 门禁 | 结果 |
|---|---|
mvn test |
BUILD SUCCESS — commerce 33 / admin-api 21 / app-api 49 / worker 12 = 115 条全过(存量 110 未跑红,新增 5) |
mvn … package -DskipTests |
BUILD SUCCESS |
pnpm ts:check / eslint / pnpm build:pro |
0 错误 / 0 error / Build successful |
dart analyze lib test / flutter test |
No issues found! / All tests passed!(+167) |
每个仓库改了什么
FruitServer(后端)
- 新增
fruitshorts-framework/.../web/JacksonLongToStringConfig.java(Long/long→ 字符串;PageResult.total用 mixin 保持数字;PlainLongSerializer供 mixin 使用) docs/api-phase1.md:§0 新增「ID 字段一律以字符串下发」总口径(含示例、边界、total例外), §2.11/§2.12 改为引用 §0- I1 的特例回退:
ProductRespVO/NoticeRespVO去掉@JsonSerialize,ProductAdminController/NoticeAdminController的create回到Result<Long> - 测试:新增
admin-api/.../web/LongToStringSerializationTest.java(5 条);ControllerContractSupport(admin + app 两处)挂上模块,避免契约测试假绿; C 端 4 条 id 断言改成value("…") + isString()
ReelShortWeb(4 个文件)
src/utils/creatorImpersonation.ts— 代看创作者 id 全程字符串(原来Number(raw),19 位会被四舍五入)src/views/contentManagement/EpisodeManagementView.vue—Number(route.query.drama)→ 字符串比较src/views/creator-center/composables/useDramaEdit.js—Number(route.params.id)→ 字符串比较src/views/permission/composables/useMenuManagement.js— 顶级菜单哨兵0→'0'(与接口的"0"对齐)
ReelShort(Flutter)逐文件清单(只有这些,没夹带别的)
lib/core/data/models/json_numbers.dart(新增)—jsonToInt:接受数字或数字字符串lib/core/data/models/auth_session.dart—expiresIn(后端long)改用jsonToIntlib/core/data/models/drama.dart—viewCount(后端Long)两处改用jsonToInttest/core/json_id_parsing_test.dart(新增)— 8 条回归用例
三个需要你确认的口径
PageResult.total保持数字(唯一例外) —— 它是条数不是 id,前端分页组件(el-pagination / Flutter 分页) 都要数字。实测total = 2 int。若你要求"Long 一律字符串",删掉JacksonLongToStringConfig里那一行 mixin 即可。- 反向(请求方向)未做限制:请求体
"id":"…"、路径变量、查询参数都接受字符串并转Long(有测试)。Integer/int(pageIndex/pageSize/Result.code/totalEpisodes/durationSec…)保持数字 —— 实测已确认。 - 客户端适配范围比"只改 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。
- 复现 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"(字符串)。 - 用响应里的 user id 调后台:
curl -s "http://127.0.0.1:8080/admin/app-users/page?pageSize=50" -H "Authorization: Bearer <admin token>", 拿新用户的id(预期是"2111…"这种带引号的字符串、total是不带引号的数字),再curl -s "http://127.0.0.1:8080/admin/app-users/<id>" -H "Authorization: Bearer <token>"→ 预期code=0、data.id与列表一致 (修复前这里返回200002 数据不存在)。 - JS 逐位一致:把第 2 步的响应存成
r.json,然后预期与node -e 'const d=JSON.parse(require("fs").readFileSync("r.json","utf8"));console.log(String(d.data.id))'psql -c "select id::text from app_user where uid='<uid>'"逐位相同。 对照:node -e 'console.log(String(JSON.parse("2111015748376858626")))'→2111015748376858620附近(会被改写),说明精度问题真实存在。 - 后台界面(:3005):
admin/admin123登录 → 商品配置新建一个商品 → 点它那一行的状态/编辑, 预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是0。 - 短剧管理 → 剧集管理带参跳转:从短剧管理点某剧的「剧集」,URL 带
?drama=<id>, 预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。 - 客户端:
cd ReelShort && flutter test(预期 All tests passed);有设备时flutter run打开首页看播放量/详情 不报解析异常(后端viewCount现在下发字符串)。 - 异常路径:请求体里 id 传数字(老客户端行为)也应被接受:
PUT /admin/products/<id>/status?status=1用数字 id 路径同样 200。
禅道:J1 在禅道里同样没有对应任务(项目 2 下仍是 S0/S1/S0-16、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。 服务现状:8080/8081 是我刚重建起回的新 jar(自测时在跑);:3005 的 dev server 也在。若被环境回收,按上面「准备」起回来即可。
回执(Codex,2026-10-16):K1 完成,停在干净交接点
提交:ReelShort d7eff4c(develop,已推;main 未动)。留痕:docs/plans/PROGRESS.md;计划:docs/plans/2026-10-16-K1.md。
门禁:dart analyze lib test → No issues found!;flutter test → All tests passed!(188 条,存量 27 个测试文件未跑红);真实后端联调(8081)见下方原始输出。
先纠正条目里的一处勘察:客户端详情接口早已接上(CatalogApi.dramaDetail + CatalogController.requireDetail,详情页在用),
票据调用 CatalogApi.playTicket 也存在但没有任何页面调用。所以本轮真正做的是:把票据接进播放链路、进度从本地换服务端、
收藏换服务端、补三态 UI。
改了什么(ReelShort,只列这一批)
新增:remote/playback_api.dart、remote/watch_api.dart、remote/library_api.dart、models/watch_progress.dart、
repositories/playback_repository.dart、repositories/favorite_repository.dart、services/favorite_controller.dart、
formatting/error_text.dart。
改:app.dart(注入三个仓库 + 收藏控制器 + 登录/登出联动)、remote/api_client.dart(补 delete)、
remote/catalog_api.dart 与 repositories/catalog_repository.dart、services/catalog_controller.dart(票据职责搬走)、
models/play_ticket.dart(补 dramaId/number/title/previewSeconds)、repositories/watch_history_repository.dart(换服务端模型)、
pages/{discover,drama_detail,mine,player}/*、l10n/app_{en,zh,id}.arb + 4 个生成文件。
测试:新增 5 个(含 live 联调),改 8 个(含 fake 与 widget_test 的离线仓库注入)。
怎么改的(要点)
- 起播前取票:
PlaybackRepository.ticketFor()带 TTL 缓存(默认 10 分钟 < 后端 15 分钟签名),过期或forceRefresh时重取; 回前台发现票据过期会重新取。签名地址原样交给播放器(AppConfig.mediaUrl只补 host,path 与?e=&s=不动,有测试钉住)。 - accessType 驱动 UI:
COIN_REQUIRED/VIP_REQUIRED→ 锁定提示 + "金币解锁即将开放";NO_MEDIA→ 暂无片源; 前端不推断能不能播(契约 §1.4 的硬要求)。付费解锁仍属 B2。 - 进度:服务端每剧一条记录,
POST /app/watch/progress节流从 5 秒改成10 秒,切集/退出/切后台各补一次,播完带finished; 续看位置只在"记录里的那一集"生效(其它集从头播);demoProfileId与本地 SharedPreferences 实现已删除。 - 收藏:
FavoriteController以服务端返回为准,乐观更新 + 失败回滚 + 失败提示;退出登录清空(不跨账号串联)。 - 三态:详情页(加载 / 失败可重试 / 空剧集);播放页(票据加载中 / 失败可重试 / 后端判定不可播); 我的页(续看读取失败可重试 / 收藏列表加载中·失败·空 / 最近观看加载中·失败·空)。断网与超时文案走 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)。
- 匿名可看:不登录进首页 → 点任一剧 → 详情正常出剧集列表;点某集能起播(免费集)。 预期:起播前有一瞬"加载票据",无白屏;付费集显示"第 N 集已锁定 + 金币解锁即将开放"。
- 登录后进度:用「我的 → 登录」注册/登录一个邮箱账号 → 进一部剧 → 看 20~30 秒 → 返回详情。 预期:详情页的"继续观看"按钮下方出现进度条与"续看至 mm:ss / mm:ss",剧集行上也有一条进度条。
- 跨进程续看(真数据验证):杀进程重开 → 进同一部剧 → 直接点"继续观看"。
预期:从上次的位置继续播(不是从头)。也可以直接查库:
psql -c "select drama_id, episode_id, position_ms from watch_progress order by updated_at desc limit 3;"预期有该剧记录。 - 收藏:播放页右侧点收藏(或详情页右上角)→ 打开「我的 → Watchlist」。 预期:该剧出现在收藏列表;再点一次取消 → 从列表消失。断网时点收藏 → 提示"收藏操作失败,请重试"且图标回到原状态。
- 异常路径:把后端停掉(
lsof -tiTCP:8081 -sTCP:LSLISTEN | xargs kill)后进详情/播放。 预期:详情页给"网络异常,请检查网络后重试"+ 重试按钮;播放页给"此视频暂时无法播放"+ 重试; 我的页收藏区给失败文案 + 重试;都不白屏、也不假装成功。恢复后端后点重试即可继续。 - 一套命令验证(不用模拟器):
cd ReelShort && flutter test test/integration/live_watch_flow_test.dart→ 预期 All tests passed(服务没起会 skip)。
待确认 / 如实说明
- 票据过期用的是客户端 TTL(10 分钟),因为契约里
play响应没有expiresAt字段。后端若补上过期时间,改成按字段判断即可(一行)。 /app/watch/recent只返回 dramaId/episodeId/位置,没有剧名封面,所以"我的页最近观看"目前按 id 去已加载的目录卡片里匹配; 匹配不到的条目先不显示。K2 的记录页建议要么后端补卡片字段,要么做成独立请求(已在计划里记为风险 4)。WatchHistoryRepository的接口签名被改了(服务端是"每剧一条续看记录",没有 profile 概念),连带改了 4 个 logic 与 6 个测试文件—— 这是接口模型决定的,不是顺手重构;clampResumePosition等本地工具函数保留。- 未登录时进度与收藏接口一律不发(匿名浏览/试看不受影响),收藏按钮沿用"先引导登录"。
- 禅道里仍没有 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):两个设备侧用例- 在手机进程里请求
/app/home/feed与/app/dramas/{id},断言列表非空、详情 id 与卡片一致; - 用真 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)
# 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;若换网络请以命令输出为准。)
前置条件
- 手机与开发机在同一局域网(例如都在 192.168.31.x)。
- iPhone 已授权「本地网络」权限:设置 → 隐私与安全性 → 本地网络 → 打开本项目(App 名 FruitShorts)。
没授权时 App 请求局域网会失败;
Info.plist里NSLocalNetworkUsageDescription与NSAllowsLocalNetworking都已配好(上一批加的),所以系统应当会弹权限询问。 - 同一时刻只跑一个 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)并叫停。 - 因此在用户重跑之前,本批一律按「未验证」计;上面那次的历史输出只作参考,不作为结论。