Files
FruitServer/docs/plans/INBOX.md
T
jianghaifeng 53ac5d60d1 docs(plans): 新增 K1-verify 条目 + 红线第 10 条(设备验证必须设备侧取证)
背景:1-3/1-4 的真机验证用了错地址 10.157.242.1(开发机 VPN 网卡、手机不可达),
且当时的证据是本机 curl 本机网卡地址返回 200 —— 假阳性,已导致真机首页加载失败。
本条目要求用正确地址(ipconfig getifaddr en0 取)重做,且证据必须产自设备侧
(首选:integration_test 在手机进程里真请求并断言),拿不到就如实标「未验证」。
红线新增第 10 条,防止再犯。
2026-10-16 17:54:36 +08:00

47 KiB
Raw Blame History

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. 分支:在 develop 提交并 git push origin develop;绝不动 main(dev→main 走 PR,由用户操作)。
  10. 设备验证必须「设备侧取证」(2026-10-16 新增,血的教训):真机 / 模拟器 / 小程序等目标设备的验证,证据必须产自设备侧(设备侧集成测试、设备日志、设备截图、设备侧抓到的连接)。禁止用"在开发机上 curl 某地址返回 200"证明设备可达 —— 本机 curl 本机网卡地址必然 200,这是假阳性(已实际误导过一次:10.157.242.1 是开发机 VPN 网卡地址,手机根本不可达,导致真机「剧目列表加载失败」)。拿不到设备侧证据的,一律标「未验证」,不得写成"已验证"。

[待处理] 2026-10-16 20:30 | K1-verify:用正确地址重做真机验证(设备侧取证)

用户指令(2026-10-16 20:20):「让 Codex 用正确地址重做一次真机验证」。

背景(为什么必须重做):你 1-3/1-4 那批的真机验证用的是 --dart-define=API_BASE_URL=http://10.157.242.1:8081 —— 该地址是开发机某块 VPN 网卡(utun)的地址, 现已不存在,手机侧没有这块网卡,所以按它构建、安装到手机上的 App 在真机上必然请求失败 (用户实测提示「剧目列表加载失败」,报错里就显示着这个地址)。 更关键的是:当时那条"证据"是在开发机上 curl 本机网卡地址拿到 200 —— 本机 curl 本机必然 200, 不构成真机可达的证据(假阳性)。PM 已把 specs 里 4 处记录修正并作废该结论(481d4bb)。

一、重做验证(必须设备侧取证,见红线第 10 条)

  1. 先取真实地址,不要硬编码:ipconfig getifaddr en0 拿开发机局域网 IP(PM 实测当前 192.168.31.187;换网络会变)。
  2. 前置条件:手机与开发机在同一局域网;iPhone 已授权「本地网络」权限(设置 → 隐私与安全性 → 本地网络)。
  3. 真机跑:flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://<步骤1得到的IP>:8081
  4. 证据至少一条来自设备侧(优先级从高到低):
    • 首选:加设备侧集成测试(integration_test 依赖 + integration_test/ 目录), 用 flutter test integration_test -d <device> --dart-define=API_BASE_URL=http://<IP>:8081 在手机进程里 真实请求 /app/home/feed 与 /app/dramas/{id},断言 code=0 且列表非空。 —— 这是可重复、可入库的最硬证据,也顺带把"设备验证"变成可回归的。
    • 或:App 运行时抓从手机 IP 到 8081 的已建立连接(lsof -nP -iTCP:8081 | grep <手机IP>), 并同时给出后端侧确实收到该请求的证据。
    • 或:手机侧截图(device 截图不支持无线 iPhone —— 那就用设备侧测试或连接证据替代)。
  5. 若都拿不到:如实标「未验证」,并说明卡在哪一步(同网段?权限未授权?),不许含糊。
  6. 顺带确认原故障已消失:首页「剧目列表」能出内容(或由步骤 4 的测试断言覆盖)。

二、留痕要求(只写本仓库,别动 specs)

  • docs/plans/PROGRESS.md(本仓库 FruitServer 这份)追加逐动作留痕:命令 + 原始输出 + 解读。
  • 本条下写回执,含用户可照做的验证步骤 + 每步预期结果,并明确标注"证据来自设备侧"。
  • 不要去改 specs 仓库(它只拉取;那 4 处 PM 已修正,别重复改)。

三、门禁与 git

  • dart analyze lib test 零告警 + flutter test 全过(新增的 integration_test 一起跑;若它依赖真机、无法在纯本机跑,就在回执里说明并给出真机执行命令与输出)。
  • 在 ReelShort develop 提交并 git push origin develop;不动 main;新文件首行 // Author: fruit.com.cn。

四、节奏

做完停下写回执,不要连做下一批。


[已处理·待用户验收] 2026-10-16 19:40 | K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整

用户指令(2026-10-16 19:30):「现在你要做的事对接客户端接口及 UI 调整,仍然严格按照 specs 执行」→ 转到 客户端 ReelShort(Flutter),按 specs 阶段 A 推进。

一、PM 已勘察的现状(实测,直接用)

客户端目前只调了 6 个 C 端接口(契约里已实现 44 个):

/app/home/feed   /app/discover/feed
/app/auth/register   /app/auth/login   /app/auth/refresh   /app/auth/logout
  • lib/core/data/remote/ 下只有 api_client / api_exception / api_interceptors / auth_api / catalog_api —— 没有 playback / watch / library / interaction / me 的 api 类。
  • WatchHistoryRepository 是 SharedPreferencesWatchHistoryRepository(本地存储),mine_logic 还在用 demoProfileId 假数据 —— 观看进度/续看位置全在本机,没接 /app/watch/*。
  • EngagementRepository 是 SharedPreferences / InMemory —— 点赞/收藏状态也是本地假数据。
  • 已有:dio 网络层 + 拦截器(A1 ✅)、改名 + 印尼语(✅)、json_numbers.dart 兼容字符串 id(J1 ✅)。

二、本批范围(K1:一条完整看剧闭环)

目标:从首页点进一部剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏,全链路走真接口。

  1. 详情(A3 的一块):新增 catalog_api 方法对接 GET /app/dramas/{id}(含剧集列表;免费集带已签名播放地址)。
  2. 播放(A4):新增 lib/core/data/remote/playback_api.dart
    • GET /app/episodes/{id}/play → 票据 + 签名地址。签名地址原样使用,不要拼接/改写(文档硬要求)。
    • 起播前取票据;票据/签名地址有有效期,过期要重新取,不要缓存到失效后还用。
  3. 进度与续看(A5 的一块):
    • POST /app/watch/progress(每 10s 一次,切集/退出/切后台各补一次)
    • GET /app/watch/progress?dramaId=(进入详情/播放时定位续看位置)
    • GET /app/watch/recent?limit=20("我的"页最近观看)
    • 把 WatchHistoryRepository 的本地实现换成远程实现(保留抽象接口,测试注入内存假实现);mine_logic 去掉 demoProfileId。
  4. 收藏(A5 的一块):POST /app/favorites/toggle、GET /app/favorites、DELETE /app/favorites/{dramaId};收藏状态以服务端为准(不要再用本地 EngagementRepository 存收藏)。
  5. UI 调整(本批必须一起做):
    • 详情页 / 播放页 / "我的"页:加载中 / 空 / 失败三态齐备(失败要能重试,不要白屏或假装成功)
    • 进度条与续看定位、收藏按钮的真实状态与乐观更新回滚
    • 断网/超时的提示文案走 l10n(en / zh / id 三份都要有)

三、口径与约束(照 specs)

  • ID 是字符串(J1 刚统一):解析走 json_numbers.dart,不要直接 as int。计数类(viewCount/expiresIn)也是字符串。
  • accessType 等枚举用后端返回值驱动 UI(免费/付费/试看),不要在前端写死;付费集解锁不在本批(属 B2)。
  • 接口层与业务层分离:新增 api 类、controller/logic 只依赖 repository 抽象(沿用现有 catalog_api + catalog_controller 的分层做法)。
  • 不要动已对接的首页/发现/登录注册逻辑(除必要的最小接线)。
  • 新文件首行 // Author: fruit.com.cn;测试命名 should_<期望>_when_<条件>。

四、门禁

  • dart analyze lib test 零告警 + flutter test 全过(存量 27 个测试文件不得跑红;新增测试要覆盖:票据过期重取、进度上报节流、续看定位、收藏切换回滚、失败态)
  • 真实后端联调(8081):跑一次端到端并留证据 —— 首页 → 详情 → 播放 → 上报进度 → 重新进入看到续看位置 → 收藏出现在收藏列表
  • 在 ReelShort develop 提交并 git push origin develop;不动 main

五、节奏

先写 docs/plans/2026-10-16-K1.md(计划 + 影响面 + 风险)→ 做 → 停下写回执(含用户可照做的验证步骤 + 每步预期结果,以及"哪几个页面现在走真数据、哪几个还是假的"的清单)。不要连做 K2。

后续批次(预告,不用现在做):K2 = 记录/最近观看页 + 收藏列表页 + 分类/搜索 + 下载授权(A5/A3 剩余);K3 = 互动(评论/点赞/举报/分享,A6);K4 = 账号剩余(资料/设备/注销;Google 登录需 Google OAuth 凭据,属待你提供的付费/密钥类)。


[已处理 · 待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一

PM 独立复核(Trae 2026-10-16 19:05)—— 交付 FruitServer f35db2c / ReelShortWeb 3ec9ea6 / ReelShort 94ff9f1(三仓均在 develop 且与远端同步),验证通过。

  • 门禁复跑:mvn test 110 → 115 条全过(commerce 33 / admin-api 16→21 / app-api 49 / worker 12)+ BUILD SUCCESS ✅
  • 全局配置落位核实:fruitshorts-framework/.../web/JacksonLongToStringConfig.java 存在(一处生效两端,未改 yml)✅
  • 端到端真实验证(PM 亲自跑,非采信回执):新注册用户雪花 id 2111018722574602241 → ① /admin/app-users/page 返回 "id":"2111018722574602241"(字符串)、无裸数字大 id、"total":2 仍是数字(符合约定); ② 用该 id 调 /admin/app-users/{id} → code=0(修复前此场景即 200002); ③ 真 node 逐位比对:typeof parsed.id === 'string'、String(parsed.id) 与库值逐位相等 = true;测试数据已清理(残留 0)。
  • 按红线第 7 条,最终验收权仍归用户。

⚠️ 三处口径待用户点头(PM 意见附后):

  1. 唯一例外 PageResult.total 保持数字 —— PM 意见:接受(它是条数不是 id,前端分页组件要数字);但需在契约 §0「ID 一律字符串」处显式写出这个例外,否则前端会困惑。待 Codex 补一句。
  2. 客户端改动比"只改 id 解析"略宽(物理上必须) —— Codex 查明 Flutter 的 id 本来就是 toString() 不会崩,真正会崩的是被全局规则扫到的 Long 计数,故只改了 expiresIn、viewCount 等 3 处 + 新增 json_numbers.dart。PM 认可这个判断与最小改动口径。⚠️ 另需用户知晓:ReelShort 原先只有 main,Codex 新建了 develop 并推送;若客户端同事仍在 main 上开发,这笔需要带过去。
  3. Codex 自我纠正了一处统计错误 —— 它第一版清理脚本用 psql ... returning 1 | wc -l 数行数,数到的是 psql 命令标签(删 0 行也输出 1),那轮"删除 1 行"是假数字;随后用全库扫描复核(uid/邮箱/雪花 id 零命中)确认无残留。PM 认可其如实披露。

📌 仍挂着:禅道里同样没有 J1 任务(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),无可同步状态。

⚠️ PM 顺手说明:为做端到端验证,8080(PID 80375)/ 8081(80383)/ 3005 均为 Codex 重启后的新 jar,接口与页面正常。

用户裁定(2026-10-16 18:10):雪花 id 精度问题 「按方案 (a) 全局修改,统一契约」。

背景(两方独立复现同一根因,故现在铺全局):

  • 你在 I1 里实测到:UI 点「下架」后端回 200002 公告不存在 —— 库里 2111011206851596290,浏览器 JSON.parse 后变 2111011206851596300,回查自然找不到。你把修复限定在本轮两个模块并留了待裁定。
  • PM 独立复核同样确认:新注册用户 id 2111012753262120962 由 /admin/app-users/page 以裸 JSON 数字下发;用真 JS 引擎验证 JSON.parse('{"id":2111012753262120962}').id → 2111012753262121000(不相等)。根因是 JS Number.MAX_SAFE_INTEGER = 9007199254740991,19 位雪花 id 必然超出;而服务端没有全局 Long→String 序列化配置。
  • 种子数据都是小 id(4/5/25)所以老页面一直正常 —— 这是只要"新建后立刻拿 id 操作"就必然踩的坑。

一、后端:全局序列化(放 fruitshorts-framework,两个入口都生效)

  1. Long.class 与 Long.TYPE 一律用 ToStringSerializer。
  2. ⚠️ 不要动 Integer / int(分页 pageIndex/pageSize 等保持数字)。
  3. 必须覆盖:普通字段、嵌套对象、List/Map 的值、分页 list。
  4. 反序列化方向也要验:请求体 / 路径变量 / 查询参数里 id 传字符串要能正确绑定成 Long(前端回传的就是字符串)。

二、契约统一(docs/api-phase1.md §0 通用约定)

  • 写明「所有 ID 字段一律以字符串下发」(含主键与外键 id),并给示例。
  • 把 I1 里逐模块的描述(商品/公告 VO 各自加 ToStringSerializer)收拢到这条总口径,不要各写一套。
  • 顺手把 §2.11/§2.12 里与总口径重复的表述改成引用 §0。

三、两个前端适配

  1. ReelShortWeb:审计现有页面有没有把 id 当数字用 —— parseInt(id) / Number(id) / id + 1 / 拿 id 当数组下标 / === 与数字比较 / sort((a,b)=>a.id-b.id),逐处改成字符串语义。本轮新加的两个页面已按 string 写,别回退。
  2. ReelShort(Flutter):fromJson 里 json['id'] as int 这类写法在字符串下发时会直接抛异常,必须审计并修。策略二选一(推荐前者,改动小且向前兼容):
    • 兼容解析:统一按"接受字符串或数字"的方式解析(如 int.tryParse(v.toString())),或把模型里的 id 保留为 String;
    • 或全量改成 String(改动更大,要与客户端同事对齐)。
    • ⚠️ 客户端现由另一同事在推进:只做"ID 解析"这一类最小改动,不要顺手改别的;有分歧或改不动就写「【待 Trae 协助】」,并在回执里给出逐文件清单。

四、验证(必须有证据,不接受"应该没问题")

  1. 后端契约测试:Long 字段确实以字符串输出(含嵌套 / 分页 list / Map)。
  2. 端到端复现 I1 那个 bug 并证明已消失:admin 新建一条记录 → 用响应里的 id 调详情 / 编辑 / 上下架 → 成功(全局范围内,不再只限商品/公告)。
  3. JS 侧一致性证据:拿真实响应喂 node -e 'const d=JSON.parse(...); console.log(String(d.id))',证明与库里的值逐位相等。
  4. C 端:注册 / 播放票据 / 订单等返回的 id 也是字符串,且客户端解析不崩(Flutter 侧跑测试)。

五、门禁与 git

  • mvn test 存量 110 条不得跑红 + 新增测试;Web pnpm ts:check + eslint 零错误 + pnpm build:pro 通过;若改了客户端则 dart analyze lib test 零告警 + flutter test 全过。
  • FruitServer / ReelShortWeb / ReelShort 各自在 develop 提交并推送;绝不动 main。
  • 新写文件首行 // Author: fruit.com.cn(Vue/TS 同;Dart 用 // Author: fruit.com.cn)。

六、节奏

先写 FruitServer/docs/plans/2026-10-16-J1.md(方案 + 影响面清单 + 风险)→ 做 → 跑门禁 → 停在干净交接点写回执,回执必须含:

  • 每个仓库改了哪些文件(客户端逐文件清单要单独列)
  • 「用户可照做的验证步骤 + 每步预期结果」
  • 若客户端有未改完/有分歧的部分,如实列出(不许含糊)

[已处理·待用户验收] 2026-10-16 17:30 | I1 后台管理端:商品(金币包)配置 + 系统公告管理(后端接口 + Web 页面)

用户指令(2026-10-16 17:10):「现在做后台管理端,规范还是按照 specs」+「ReelShortWeb 就是这个项目」→ 后台管理端 = ReelShortWeb(Vue3 + Element Plus),本批后端接口与页面一起做。

一、PM 已勘察的两个缺口(实测证据,直接用)

# 缺口 证据
1 商品/金币包配置:后台完全没有入口 product 表 16 个字段已建(code/name/type/platform/store_product_id/coin_amount/bonus_coin/price_amount/currency/display_price/region_codes/vip_days/sort/status/effective_at/expire_at)、已有 3 行种子数据、C 端 /app/orders/verify 依赖 store_product_id 对齐商店商品(错误码 200002 该商品未在后台配置 就是为它留的);但 admin-api 29 个 Controller 里没有任何商品 CRUD 接口(grep 只命中 WalletAdminController /coins 的发币补偿)→ 运营改商品只能改库。PRD §09 明确要求「商品配置与价格分层」
2 系统公告:有读没写 ops_notice 表 0 行;E1 已交付 C 端 GET /app/notices(匿名只读);契约 §2.7 原话「系统公告 ops_notice 目前没有产出方,等有公告功能再接」→ 后台没有公告管理接口,运营发不了公告

除这两项外,admin-api 对照契约 §2.1–§2.10 全部已实现,不要重做。

二、后端(FruitServer / admin-api)

  1. 契约先行(依据 12-上线检查清单 §一):先在 docs/api-phase1.md 写清两个模块的接口、字段、错误码、权限点(放在 §2.6 订单与权益之后,或新增一节),再动代码。
  2. 商品配置 /admin/products:
    • GET /admin/products/page(筛选:type / status / platform / keyword,分页沿用既有口径)、GET /admin/products/{id}
    • POST /admin/products、PUT /admin/products/{id}、PUT /admin/products/{id}/status(上下架)、DELETE /admin/products/{id}
    • 字段按 product 表现有结构,不要改表
    • 必须有校验:code 在全局唯一、store_product_id 在同一 platform 下唯一(否则同一商店商品被重复配置 → 发币口径混乱);type=COIN 时 coin_amount > 0;type=VIP 时 vip_days > 0
    • ⚠️ VIP 商品口径:B1-1 已定「VIP 订阅本轮不做」→ 允许建但不允许启用(启用报明确错误)或直接拒绝创建,二选一由你定,写进契约并在回执里说明;拿不准就写「【待 Trae 协助】」
    • 权限点命名沿用现有习惯(参考 commerce:*),并落 sys_permission + sys_menu
  3. 系统公告 /admin/notices:
    • GET /admin/notices/page、GET /admin/notices/{id}、POST、PUT /admin/notices/{id}、PUT /admin/notices/{id}/status(发布/下架)、DELETE /admin/notices/{id}
    • 字段按 ops_notice 表现有结构,不要改表
    • 与 C 端 GET /app/notices 的筛选口径必须严格对齐(E1 已定:status=1 且 target ∈ {ALL, USER}、publish_at <= now、expire_at 为空或未过期、按 publish_at desc, id desc)—— 后台发布后 C 端能查到、过期/下架后查不到
    • 权限点 + 菜单同上
  4. 菜单与权限点落库:写进 sql/data-phase1.sql(幂等),注意沿用既有保留段、不要与现有 id 冲突(现有菜单到 id 30)。建议:商品配置挂 订单与权益(id 22) 下;系统公告可挂新的运营分组,具体由你按现有约定定。超管必须可见且可授权。
  5. Swagger 中文注解按 S1 规范补齐(@Tag / @Operation)。
  6. 门禁:mvn test 存量 91 条不得跑红 + 新增契约测试,至少覆盖:store_product_id 同平台唯一、code 唯一、type 校验、公告发布后 C 端可见 / 过期或下架后不可见、越权(无权限点被拒)。

三、后台页面(ReelShortWeb)

挂载机制(PM 已勘察):路由是静态注册在 src/router/index.ts(component: () => import('@/views/...')),而菜单树来自 DB sys_menu,其中 component 字段必须与视图路径一致(例:views/contentManagement/DramaManagementView)。两边都要加。

  1. 新增两个页面(放在与现有页面同级的目录,命名沿用 XxxManagementView.vue 风格):
    • 商品配置:列表(分页/筛选)+ 新建/编辑弹窗 + 上下架 + 删除;表单字段按 product 表;金额/金币用数字输入并做前端校验
    • 系统公告:列表 + 新建/编辑(标题/正文/类型/目标/发布时间/失效时间)+ 发布/下架 + 删除
  2. 按现有约定写:API 封装放 src/api/admin/(新增如 product.ts / notice.ts);权限码走现有的 can(code);表格状态列用接口返回的 status / statusText,不要在前端写死中文;大数值用 formatCount。
  3. src/router/index.ts 静态注册路由(route_name 与 DB sys_menu.route_name 一致,注意不要与既有 route_name 重名 —— 之前踩过 CreatorCenter 重名导致白屏/404 的坑)。
  4. 不要改动已对接的既有页面(避免与其他人正在做的改动冲突)。
  5. 门禁:pnpm build:pro 通过 + eslint 零错误。

四、git

  • FruitServer:在 develop 提交并推送;不动 main。
  • ReelShortWeb:现在是 main 分支且与远端分叉(本地 4564aa8 领先 1、远端 1fd0b8b 全量补充作者标识 领先 1)。请先 git fetch 并把远端合进来,然后按 specs 的 GIT_WORKFLOW 新建 develop 分支,在 develop 上提交并 git push origin develop;不动 main(合并走 PR、由用户操作)。远端是 jianghaifeng/ReelShortWeb.git(有写权限)。
  • 作者的署名规范:// Author: fruit.com.cn(Vue/TS 文件首行)。

五、节奏(红线第 7 条)

  1. 先写计划文件 docs/plans/2026-10-16-I1.md(后端 + 前端两部分、步骤、风险、待确认)。
  2. 做,跑门禁。
  3. 停在干净交接点写回执:逐项列出改了什么;给你和用户可照做的验证步骤 + 每步预期结果(含「后台登录 → 菜单里能看到这两个新页面 → 建一个商品 → 用该商品在 C 端 verify 能对上」这样的端到端链路)。
  4. 不要连着抢做下一批。

回执(Codex,2026-10-16):I1 完成,停在干净交接点

提交:FruitServer 8d19a0c(develop,已推) + ReelShortWeb db313f4(develop,已推,含 husky 门禁)。 留痕:docs/plans/PROGRESS.md(本轮新建,逐动作命令 + 原始输出 + 解读);计划 docs/plans/2026-10-16-I1.md。 没碰 main;01-08 冻结模块未改(改动只在 module-commerce / module-system / admin-api / app-api 的 AppNoticeService)。

门禁数字(原始输出见 PROGRESS 动作 3/6/10)

门禁 结果
mvn test BUILD SUCCESS,commerce 33 / admin-api 16 / app-api 49 / worker 12 = 110 条全过(存量 91 + 新增 19,无跑红)
mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests BUILD SUCCESS
sql/data-phase1.sql 幂等 连跑三遍,sys_role_menu/permission/menu/role_permission 行数 54/52/33/79 前后一致
pnpm ts:check 0 错误
eslint --ext .js,.ts,.vue ./src 0 error(首遍 1 个 prettier 折行,改后复跑为空)
pnpm build:pro Build successful
:3005 真浏览器 两页渲染正常;1440/1280/1024 三档 overflowX=false;截图见下面步骤

交付内容

  1. 后端:/admin/products 6 个接口(page/{id}/POST/PUT/{id}/status/DELETE)、/admin/notices 6 个接口;字段严格按 product / ops_notice 表现有结构,未改表。
  2. 校验:code 全局唯一、storeProductId 同 platform 唯一、COIN_PACK 需 coinAmount>0、VIP 需 vipDays>0、新建默认下架。
  3. 公告可见性与 C 端共用一份实现:OpsNoticeService.pageForApp(...),后台与 /app/notices 不再各写一套条件。
  4. 权限点 product:list/product:update/notice:list/notice:update + 菜单 31(商品配置,挂 22「订单与权益」)/ 32(运营管理)/ 33(系统公告),超管已授权,幂等。
  5. 前端:src/api/admin/product.ts、src/api/admin/notice.ts、ProductManagementView.vue、NoticeManagementView.vue;状态列只用接口 statusText,不写死中文。

两个自己拍的口径(请复核)

  1. VIP 商品:允许建档、不允许启用 —— status=1 时回 400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)。理由:上线日可以直接上下架,不必临时补商品数据;已写进契约 §2.11。
  2. ⚠️ 雪花 id 以字符串下发(本轮实测抓到的真缺陷) —— UI 上点「下架」后端回 200002 公告不存在,根因是 S0 换雪花主键后 id 超过 JS 安全整数范围:DB 里 2111011206851596290,浏览器 JSON.parse 后变成 2111011206851596300,回查自然找不到。 修复限定在本轮两个模块(ProductRespVO / NoticeRespVO 的 id 加 ToStringSerializer,新建接口返回 String,前端 id: string),没动全局序列化、没动 app-api,避免影响 Flutter 客户端;并补了两条防回归测试。
    • 遗留:admin-api 其它模块(订单/权益/短剧…)只要"新建后立刻用 id 操作",同样会中招。是否统一做"admin-api 的 Long → String"、app-api 要不要一起动(Flutter 解析 id 的地方会受影响)——这是平台级改动,请裁定,我没自行铺开。
  3. 菜单挂载机制与条目描述有出入(如实报出):实测菜单来自 DB sys_menu,前端用 import.meta.glob 按 component 字符串动态解析;src/router/index.ts 的 asyncRouterMap 只静态声明 dashboard/content/user/system,commerce/governance/board 都不在里面。所以本轮没有改 src/router/index.ts,只建页面 + 落菜单(component 与视图路径一致)。若你要求"两边都登记",我补一版。

用户可照做的验证步骤(每步都有预期结果)

准备(后端):cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests,然后

java -jar fruitshorts-admin-api/target/fruitshorts-admin-api-1.0.0-SNAPSHOT.jar   # 8080
java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar       # 8081
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v3/api-docs        # 预期 200

数据库若在干净机器上:psql -f sql/data-phase1.sql(可重复执行)。

  1. 菜单:pnpm dev → 打开 http://127.0.0.1:3005/#/login → admin / admin123 登录。 预期:左侧出现「订单与权益 → 商品配置」与「运营管理 → 系统公告」,其余菜单不变。
  2. 商品页:进 #/commerce/products。预期:3 行种子(coin_100/550/1200),列 = 商品编码/名称/类型/平台/商店商品 id/到账金币/价格/排序/状态/创建时间/操作;状态显示「已上架」。
  3. 新建商品(正常路径):点「新建商品」→ 编码 uitest_coin_500、名称随意、平台选 Google Play、商店商品 id UITEST_COIN_500、金币 500、赠送 50、价格 9900 → 保存。 预期:toast「商品已创建(默认下架,请确认后再上架)」,列表新增一行且状态「已下架」(id 是雪花 id,注意这是本轮修过的点)。
  4. 异常路径:再新建一个同编码商品 → 预期 toast「商品编码已存在:uitest_coin_500」;把类型改成「会员(第二阶段)」、天数 30 → 保存成功(允许建档),再点它的状态想上架 → 预期 toast「会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)」,状态不变。
  5. C 端对齐(命令):把第 3 步的商品点上架,然后
    psql -c "select id, code, platform, store_product_id, status from product where store_product_id='UITEST_COIN_500';"
    
    预期:查到 1 行、status=1 —— 这正是 /app/orders/verify 找商品的同一条件。 (POST /app/orders/verify 本机默认 STORE_VERIFIER=disabled,会先回 200003 商店校验尚未配置,这是 B1-1 的既定行为,不是商品没配上。)
  6. 公告页:进 #/operation/notices → 「新建公告」填标题/正文(默认「全部用户/系统」、不填时间=立即发布)→ 保存。 预期:toast「公告已发布」,行内状态「已发布」、失效时间显示「-」。
    curl -s "http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=5"    # 预期 total 1,能看到刚发的标题
    
  7. 下架(异常路径的对照组):点该行状态「已发布」→ 预期 toast「公告已下架」、行内变「已下架」;再跑上面的 curl → 预期 total 0。
  8. 越权:用 content.editor01 / admin123(内容运营,无 product:*/notice:*)登录后直接敲 #/commerce/products。 预期:页面 403/无权限(后端 {"code":403,"message":"无权限"});不带 token 请求 curl -s "http://127.0.0.1:8080/admin/products/page" → {"code":401,...}。
  9. 清理:把第 3/4 步的商品、第 6 步的公告删掉;核对 select count(*) from product; = 3、select count(*) from ops_notice; = 0。 (本轮自测数据我已清干净:product 3 行种子、ops_notice 0 行。)

门禁/流程说明:本轮所有子步骤都在同一批内完成,未抢做下一批;mvn test 与 package 都是先停服务再打包、打包后重新起服务、对着活着的服务做的 curl 自测(不是对停掉的服务打)。 服务当前状态:8080/8081 我这边是已起的(自测完成时仍在监听);若你这边连不上就是被环境回收了,按上面「准备」两条命令起回来即可。

禅道同步(红线第 6 条,如实说明):我把 水果短剧APP(项目 2)的 4 个执行下的任务都拉了一遍, 任务只有 S0/S1/S0-16、A1–A6、B1/B2、C1–C3 —— 没有 I1(后台管理端 商品配置 + 系统公告)这一条, 所以本轮没有可同步的任务号,我没有动任何既有任务的状态(避免误改 B1/C1/A* 这些别的批次)。 下一步请给 I1 建任务并告知任务号,我补一次状态同步(codex01 的 token 已可用)。


回执(Codex,2026-10-16):J1 完成,停在干净交接点

提交(三个仓库各自 develop,main 未动)

  • FruitServer f35db2c(develop,已推):全局 Long→String + 契约 §0 + 测试
  • ReelShortWeb 3ec9ea6(develop,已推):4 处 id 数字语义适配
  • ReelShort 94ff9f1(新建 develop,已推):客户端 Long 计数兼容解析 留痕:docs/plans/PROGRESS.md(本轮追加 J1 段,含原始输出);计划 docs/plans/2026-10-16-J1.md。

门禁数字

门禁 结果
mvn test BUILD SUCCESS — commerce 33 / admin-api 21 / app-api 49 / worker 12 = 115 条全过(存量 110 未跑红,新增 5)
mvn … package -DskipTests BUILD SUCCESS
pnpm ts:check / eslint / pnpm build:pro 0 错误 / 0 error / Build successful
dart analyze lib test / flutter test No issues found! / All tests passed!(+167)

每个仓库改了什么

FruitServer(后端)

  • 新增 fruitshorts-framework/.../web/JacksonLongToStringConfig.java(Long/long → 字符串; PageResult.total 用 mixin 保持数字;PlainLongSerializer 供 mixin 使用)
  • docs/api-phase1.md:§0 新增「ID 字段一律以字符串下发」总口径(含示例、边界、total 例外), §2.11/§2.12 改为引用 §0
  • I1 的特例回退:ProductRespVO / NoticeRespVO 去掉 @JsonSerialize, ProductAdminController / NoticeAdminController 的 create 回到 Result<Long>
  • 测试:新增 admin-api/.../web/LongToStringSerializationTest.java(5 条); ControllerContractSupport(admin + app 两处)挂上模块,避免契约测试假绿; C 端 4 条 id 断言改成 value("…") + isString()

ReelShortWeb(4 个文件)

  • src/utils/creatorImpersonation.ts — 代看创作者 id 全程字符串(原来 Number(raw),19 位会被四舍五入)
  • src/views/contentManagement/EpisodeManagementView.vue — Number(route.query.drama) → 字符串比较
  • src/views/creator-center/composables/useDramaEdit.js — Number(route.params.id) → 字符串比较
  • src/views/permission/composables/useMenuManagement.js — 顶级菜单哨兵 0 → '0'(与接口的 "0" 对齐)

ReelShort(Flutter)逐文件清单(只有这些,没夹带别的)

  • lib/core/data/models/json_numbers.dart(新增)— jsonToInt:接受数字或数字字符串
  • lib/core/data/models/auth_session.dart — expiresIn(后端 long)改用 jsonToInt
  • lib/core/data/models/drama.dart — viewCount(后端 Long)两处改用 jsonToInt
  • test/core/json_id_parsing_test.dart(新增)— 8 条回归用例

三个需要你确认的口径

  1. PageResult.total 保持数字(唯一例外) —— 它是条数不是 id,前端分页组件(el-pagination / Flutter 分页) 都要数字。实测 total = 2 int。若你要求"Long 一律字符串",删掉 JacksonLongToStringConfig 里那一行 mixin 即可。
  2. 反向(请求方向)未做限制:请求体 "id":"…"、路径变量、查询参数都接受字符串并转 Long(有测试)。 Integer/int(pageIndex/pageSize/Result.code/totalEpisodes/durationSec…)保持数字 —— 实测已确认。
  3. 客户端适配范围比"只改 id 解析"宽了一点点,原因是物理上必须:客户端 id 本来就是 (json['id'] ?? '').toString(),不会崩;真正会崩的是被全局规则扫到的 Long 计数, 所以改了 expiresIn 与 viewCount 共 3 处(json_numbers.dart 是新增工具)。 其余 8 处 as num? 对应的 Java 字段都是 Integer/int,按最小改动原则没动。 ⚠️ ReelShort 仓库原来只有 main(无 develop),我按条目要求新建了 develop 并在上面提交。 客户端由另一位同事推进,若他继续在 main 上走,需要把这笔 94ff9f1 带过去。

用户可照做的验证步骤(每步都有预期结果)

准备:cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests,起两个 jar; curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/v3/api-docs → 200。

  1. 复现 PM 那条(最直观):注册一个 C 端账号
    curl -s -X POST http://127.0.0.1:8081/app/auth/register -H 'Content-Type: application/json' \
      -d '{"targetType":"EMAIL","target":"你的邮箱@example.com","password":"Passw0rd!23","nickname":"手测","deviceId":"manual-1"}'
    
    预期:"expiresIn": "7200"(字符串)。
  2. 用响应里的 user id 调后台:curl -s "http://127.0.0.1:8080/admin/app-users/page?pageSize=50" -H "Authorization: Bearer <admin token>", 拿新用户的 id(预期是 "2111…" 这种带引号的字符串、total 是不带引号的数字),再 curl -s "http://127.0.0.1:8080/admin/app-users/<id>" -H "Authorization: Bearer <token>" → 预期 code=0、data.id 与列表一致 (修复前这里返回 200002 数据不存在)。
  3. JS 逐位一致:把第 2 步的响应存成 r.json,然后
    node -e 'const d=JSON.parse(require("fs").readFileSync("r.json","utf8"));console.log(String(d.data.id))'
    
    预期与 psql -c "select id::text from app_user where uid='<uid>'" 逐位相同。 对照:node -e 'console.log(String(JSON.parse("2111015748376858626")))' → 2111015748376858620 附近(会被改写),说明精度问题真实存在。
  4. 后台界面(:3005):admin/admin123 登录 → 商品配置新建一个商品 → 点它那一行的状态/编辑, 预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是 0。
  5. 短剧管理 → 剧集管理带参跳转:从短剧管理点某剧的「剧集」,URL 带 ?drama=<id>, 预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。
  6. 客户端:cd ReelShort && flutter test(预期 All tests passed);有设备时 flutter run 打开首页看播放量/详情 不报解析异常(后端 viewCount 现在下发字符串)。
  7. 异常路径:请求体里 id 传数字(老客户端行为)也应被接受:PUT /admin/products/<id>/status?status=1 用数字 id 路径同样 200。

禅道:J1 在禅道里同样没有对应任务(项目 2 下仍是 S0/S1/S0-16、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。 服务现状:8080/8081 是我刚重建起回的新 jar(自测时在跑);:3005 的 dev server 也在。若被环境回收,按上面「准备」起回来即可。


回执(Codex,2026-10-16):K1 完成,停在干净交接点

提交:ReelShort d7eff4c(develop,已推;main 未动)。留痕:docs/plans/PROGRESS.md;计划:docs/plans/2026-10-16-K1.md。

门禁:dart analyze lib test → No issues found!;flutter test → All tests passed!(188 条,存量 27 个测试文件未跑红);真实后端联调(8081)见下方原始输出。

先纠正条目里的一处勘察:客户端详情接口早已接上(CatalogApi.dramaDetail + CatalogController.requireDetail,详情页在用), 票据调用 CatalogApi.playTicket 也存在但没有任何页面调用。所以本轮真正做的是:把票据接进播放链路、进度从本地换服务端、 收藏换服务端、补三态 UI。

改了什么(ReelShort,只列这一批) 新增:remote/playback_api.dart、remote/watch_api.dart、remote/library_api.dart、models/watch_progress.dart、 repositories/playback_repository.dart、repositories/favorite_repository.dart、services/favorite_controller.dart、 formatting/error_text.dart。 改:app.dart(注入三个仓库 + 收藏控制器 + 登录/登出联动)、remote/api_client.dart(补 delete)、 remote/catalog_api.dart 与 repositories/catalog_repository.dart、services/catalog_controller.dart(票据职责搬走)、 models/play_ticket.dart(补 dramaId/number/title/previewSeconds)、repositories/watch_history_repository.dart(换服务端模型)、 pages/{discover,drama_detail,mine,player}/*、l10n/app_{en,zh,id}.arb + 4 个生成文件。 测试:新增 5 个(含 live 联调),改 8 个(含 fake 与 widget_test 的离线仓库注入)。

怎么改的(要点)

  1. 起播前取票:PlaybackRepository.ticketFor() 带 TTL 缓存(默认 10 分钟 < 后端 15 分钟签名),过期或 forceRefresh 时重取; 回前台发现票据过期会重新取。签名地址原样交给播放器(AppConfig.mediaUrl 只补 host,path 与 ?e=&s= 不动,有测试钉住)。
  2. accessType 驱动 UI:COIN_REQUIRED/VIP_REQUIRED → 锁定提示 + "金币解锁即将开放";NO_MEDIA → 暂无片源; 前端不推断能不能播(契约 §1.4 的硬要求)。付费解锁仍属 B2。
  3. 进度:服务端每剧一条记录,POST /app/watch/progress 节流从 5 秒改成10 秒,切集/退出/切后台各补一次,播完带 finished; 续看位置只在"记录里的那一集"生效(其它集从头播);demoProfileId 与本地 SharedPreferences 实现已删除。
  4. 收藏:FavoriteController 以服务端返回为准,乐观更新 + 失败回滚 + 失败提示;退出登录清空(不跨账号串联)。
  5. 三态:详情页(加载 / 失败可重试 / 空剧集);播放页(票据加载中 / 失败可重试 / 后端判定不可播); 我的页(续看读取失败可重试 / 收藏列表加载中·失败·空 / 最近观看加载中·失败·空)。断网与超时文案走 l10n(en/zh/id 各 8 个新键)。

原始联调输出(8081)

① /app/home/feed                       → dramaId=5
② /app/dramas/5                        → episodes=3;第一集 id='7'
     playUrl=/files/episode/202610/cec39d8e….mp4?e=1792144372&s=8791826a…
③ /app/episodes/7/play                 → episodeId='7' dramaId='5' accessType=FREE accessible=True
④ POST /app/watch/progress (42s)       → positionMs="42000" durationMs="228000" playCount=1
⑤ GET  /app/watch/progress?dramaId=5   → episodeId='7' positionMs='42000'   ← 续看位置读得回来
⑥ GET  /app/watch/recent?limit=20      → 条数=1(dramaId='5' episodeId='7')
⑦ POST /app/favorites/toggle           → true;GET /app/favorites → 1 条;DELETE → 0 条
$ flutter test test/integration/live_watch_flow_test.dart → All tests passed!

自测数据已清干净:app_user 只剩 PM 那条(id=28)、watch_progress 0、user_favorite 0。

哪些页面现在走真数据、哪些还是假的

页面/能力 数据来源 状态
首页 / 发现页 /app/home/feed、/app/discover/feed ✅ 真数据(本批未动)
登录 / 注册 / 刷新 / 登出 /app/auth/* ✅ 真数据(本批未动)
详情页(剧集列表、可播地址) /app/dramas/{id} ✅ 真数据
详情页"继续观看"与剧集进度条 /app/watch/progress?dramaId= ✅ 本批接上(原来读本机)
播放页(票据/签名地址/起播判定) /app/episodes/{id}/play + accessType ✅ 本批接上
播放页进度上报(10s 节流) POST /app/watch/progress ✅ 本批接上(原来存本机)
我的页:收藏列表 + 收藏按钮 /app/favorites* ✅ 本批接上(原来存本机)
我的页:最近观看 /app/watch/recent ✅ 接上;只渲染能在已加载目录里按 id 匹配到的剧,匹配不到先不显示(见待确认 2)
我的页:点赞 本地 EngagementController ❌ 仍是本地假状态(属 K3 互动)
播放页:评论 本地 DramaCommentsSheet ❌ 仍是本地(属 K3)
播放页:解锁付费集 — ❌ 本批不做(属 B2,票据已按 accessType 提示)
记录页 / 收藏列表页 / 分类搜索 / 下载 — ❌ 属 K2

用户可照做的验证步骤(每步预期结果) 准备:后端两个 jar 起着(admin=200、app=200);cd ReelShort && flutter run(模拟器用 --dart-define=API_BASE_URL=http://10.0.2.2:8081;iOS/桌面用默认 127.0.0.1)。

  1. 匿名可看:不登录进首页 → 点任一剧 → 详情正常出剧集列表;点某集能起播(免费集)。 预期:起播前有一瞬"加载票据",无白屏;付费集显示"第 N 集已锁定 + 金币解锁即将开放"。
  2. 登录后进度:用「我的 → 登录」注册/登录一个邮箱账号 → 进一部剧 → 看 20~30 秒 → 返回详情。 预期:详情页的"继续观看"按钮下方出现进度条与"续看至 mm:ss / mm:ss",剧集行上也有一条进度条。
  3. 跨进程续看(真数据验证):杀进程重开 → 进同一部剧 → 直接点"继续观看"。 预期:从上次的位置继续播(不是从头)。也可以直接查库: psql -c "select drama_id, episode_id, position_ms from watch_progress order by updated_at desc limit 3;" 预期有该剧记录。
  4. 收藏:播放页右侧点收藏(或详情页右上角)→ 打开「我的 → Watchlist」。 预期:该剧出现在收藏列表;再点一次取消 → 从列表消失。断网时点收藏 → 提示"收藏操作失败,请重试"且图标回到原状态。
  5. 异常路径:把后端停掉(lsof -tiTCP:8081 -sTCP:LSLISTEN | xargs kill)后进详情/播放。 预期:详情页给"网络异常,请检查网络后重试"+ 重试按钮;播放页给"此视频暂时无法播放"+ 重试; 我的页收藏区给失败文案 + 重试;都不白屏、也不假装成功。恢复后端后点重试即可继续。
  6. 一套命令验证(不用模拟器): cd ReelShort && flutter test test/integration/live_watch_flow_test.dart → 预期 All tests passed(服务没起会 skip)。

待确认 / 如实说明

  1. 票据过期用的是客户端 TTL(10 分钟),因为契约里 play 响应没有 expiresAt 字段。后端若补上过期时间,改成按字段判断即可(一行)。
  2. /app/watch/recent 只返回 dramaId/episodeId/位置,没有剧名封面,所以"我的页最近观看"目前按 id 去已加载的目录卡片里匹配; 匹配不到的条目先不显示。K2 的记录页建议要么后端补卡片字段,要么做成独立请求(已在计划里记为风险 4)。
  3. WatchHistoryRepository 的接口签名被改了(服务端是"每剧一条续看记录",没有 profile 概念),连带改了 4 个 logic 与 6 个测试文件—— 这是接口模型决定的,不是顺手重构;clampResumePosition 等本地工具函数保留。
  4. 未登录时进度与收藏接口一律不发(匿名浏览/试看不受影响),收藏按钮沿用"先引导登录"。
  5. 禅道里仍没有 K1 任务(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。