# PROGRESS — 工作留痕(FruitServer) > 红线第 2 条:谁干活谁写这里(命令 + 原始输出 + 解读,逐动作追加)。 > 指令通道:`docs/plans/INBOX.md`(specs 仓库只拉取、由项目负责人更新)。 --- ## 2026-10-16 I1:后台管理端 —— 商品(金币包)配置 + 系统公告管理 计划文件:`docs/plans/2026-10-16-I1.md`(先写计划再动手)。改了 `docs/api-phase1.md` §2.11/§2.12(契约先行)。 ### 动作 1:开工前核对仓库状态 ``` $ cd FruitServer && git status --short && git log --oneline -1 M docs/api-phase1.md M fruitshorts-admin-api/.../FruitShortsApplication.java M fruitshorts-app-api/.../AppNoticeService.java M sql/data-phase1.sql ?? docs/plans/2026-10-16-I1.md ?? fruitshorts-admin-api/.../ProductAdminController.java(+ NoticeAdminController) ?? fruitshorts-admin-api/src/test/.../ProductAdminControllerContractTest.java(+ Notice / AdminPermissionAnnotation) ?? fruitshorts-module-commerce/.../ProductService.java(+ 3 个 VO) ?? fruitshorts-module-system/.../OpsNoticeService.java(+ 3 个 VO) 7513ac0 docs(plans): 指令通道迁入本仓库(specs 改为只拉取,由项目负责人更新) ``` 解读:分支 `develop`,起点就是指令通道那笔 `7513ac0`;工作区只有本轮 I1 的改动。 ### 动作 2:契约先行(先改文档再写代码) `docs/api-phase1.md` 新增 **§2.11 商品(金币包)配置(已实现)**、**§2.12 系统公告管理(已实现)**: 接口表 + 字段表 + 校验错误码表 + 权限点 + 与 C 端可见性口径对齐说明。VIP 口径在契约里定死: **允许建档、不允许启用**。 ### 动作 3:后端门禁 —— `mvn test` 首轮(雪花 id 修复前): ``` $ mvn test [INFO] Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-module-commerce) [INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-admin-api) [INFO] Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-app-api) [INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-transcode-worker) [INFO] BUILD SUCCESS ``` 解读:存量 91 条(app-api 41 + commerce 27 + admin-api 3 + worker 12 + …)无一跑红,新增 17 条全过,共 108。 ### 动作 4:SQL 幂等(连跑三遍) ``` $ psql -f sql/data-phase1.sql # 第一次 INSERT 0 5 / UPDATE 5 / INSERT 0 4 / INSERT 0 4 / INSERT 0 3 / INSERT 0 3 $ psql -f sql/data-phase1.sql # 第二次 INSERT 0 5 / UPDATE 5 / INSERT 0 0 / INSERT 0 0 / INSERT 0 3 / INSERT 0 0 $ psql -t -c "select 'role_menu='||count(*) from sys_role_menu union all ..." # 跑前 role_menu=54 / permission=52 / menu=33 / role_perm=79 $ psql -f sql/data-phase1.sql # 第三次 RUN3 EXIT=0 role_menu=54 / permission=52 / menu=33 / role_perm=79 # 跑后:完全一致 ``` 解读:脚本可重复执行,**行数不增长、不重复插入**。新增权限点 `1160-1163`、菜单 `31/32/33`(商品挂 22「订单与权益」下、公告新建 32「运营管理」分组 + 33 子菜单),并给超管授了角色-菜单。 ### 动作 5:打包 + 起回服务 + curl 自测(商品) ``` $ mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests → BUILD SUCCESS (kill 8080/8081 → 重新起 jar → 健康检查) admin=200 / app=200 ``` 商品自测(完整脚本 `/tmp/i1-curl.sh`,原始输出 `/tmp/i1-curl.log`): ``` 3) 新建商品 i1_test_coin_100(800+80,IDR 9900,platform=GOOGLE) {"code":0,"message":"ok","data":2111010131922784257} 4) 新建后状态 → "status": 0, "statusText": "已下架" # 新建默认下架 5) code 重复 → {"code":200001,"message":"商品编码已存在:i1_test_coin_100"} 6) 同平台 store id 重复 → {"code":200001,"message":"该平台下商店商品 id 已被占用:I1_TEST_COIN_100"} 7) 金币包 coin_amount=0 → {"code":400,"message":"金币包的金币数量必须大于 0"} 8) VIP vip_days=0 → {"code":400,"message":"会员商品的会员天数必须大于 0"} 9) VIP vip_days=30 → {"code":0} # 允许建档 10) VIP 启用 → {"code":400,"message":"会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)"} 11) 上架 → {"code":0},详情 status=1/已上架 12) C 端等价查询(store_product_id + platform in (GOOGLE,ALL) + status=1) 2111010131922784257 | i1_test_coin_100 | GOOGLE | I1_TEST_COIN_100 | 800 | 80 | 1 (1 row) ``` 公告自测(同一脚本,13–18 步): ``` 13) 公告分页(新建前)→ total 0 14) 新建 target=ALL → {"code":0,"data":2111010133357236226} 15) 新建 target=CREATOR → {"code":0,"data":2111010133537591298} 16) 新建 expire_at=2020-01-01(已过期) → {"code":0} 17) C 端 GET /app/notices(匿名)→ total 1,只返回第 14 步那条 18) 后台下架第 14 步那条 → C 端 total 0 ``` 越权自测(`content.editor01` / role=content_operator,无 `product:*` / `notice:*`): ``` GET /admin/products/page → {"code":403,"message":"无权限"} GET /admin/notices/page → {"code":403,"message":"无权限"} POST /admin/products → {"code":403,"message":"无权限"} PUT /admin/notices/1/status → {"code":403,"message":"无权限"} (无 token)GET /admin/products/page → {"code":401,"message":"未登录或登录已过期"} ``` 解读:商品三类校验、VIP 只建不启、公告"C 端可见/不可见"三个分支、越权 401/403 全部实测通过。 `logs` 里 `200002 该商品未在后台配置` 的触发点在商店校验**之后**(`/app/orders/verify` 当前默认 `DisabledStoreVerifier` → 先返回 `200003 商店校验尚未配置`),所以 C 端链路验证到"进入商店校验环节"为止, 真正的发币链路等 Google Play 凭据到位(见回执「待确认」)。 ### 动作 6:清理第一轮自测数据 ``` DELETE 3 # 3 个自测商品 DELETE 4 # 4 条自测公告 products | soft_deleted notices 3 | 0 0 ``` ### 动作 7:前端页面(ReelShortWeb,develop 分支) 新增 4 个文件(首行署名 `// Author: fruit.com.cn` / ``): ``` src/api/admin/product.ts src/api/admin/notice.ts src/views/commerce/ProductManagementView.vue src/views/operation/NoticeManagementView.vue ``` **挂载机制实测**(与 INBOX 描述有出入,以实测为准):菜单树来自 DB `sys_menu`, `src/utils/routerHelper.ts` 用 `import.meta.glob('../views/**/*.{vue,tsx}')` 按 `component` 字符串**动态解析**; `src/router/index.ts` 的 `asyncRouterMap` 只静态声明 dashboard/content/user/system 这几个早期分组, commerce/governance/board/creator-center 都不在里面 —— 所以新增页面**不需要**改静态路由表, 只要 ① 建 `.vue` ② 菜单 `component` 与视图路径一致。本轮据此实现,未动 `src/router/index.ts`。 前端门禁: ``` $ pnpm ts:check → 0 错误(无输出) $ pnpm exec eslint --ext .js,.ts,.vue ./src ✖ 1 problem (1 error, 0 warnings) # 第一遍:NoticeManagementView.vue prettier 折行 (改完复跑)→ 无输出,0 error $ pnpm build:pro → Build successful. Please see dist-pro directory ``` ### 动作 8:`:3005` 真浏览器实测(Codex in-app browser) ``` 登录 admin → 侧边栏出现「订单与权益 → 商品配置」「运营管理 → 系统公告」(其余菜单不变) #/commerce/products → 标题「商品配置」,11 列与后端字段一一对应,状态列用 statusText(已上架/已下架) #/operation/notices → 标题「系统公告」,空态「还没有发布公告」 新建商品(UI)→ toast「商品已创建(默认下架,请确认后再上架)」,行内 totalCoin 550、状态「已下架」 新建公告(UI)→ toast「公告已发布」,行内 发布时间 16:32:15、失效时间「-」、状态「已发布」 C 端 GET /app/notices(匿名)→ 立刻能看到这条(total 1) 下架 → toast「公告已下架」→ C 端 total 0 删除 → toast「商品已删除」「公告已删除」→ 列表回到种子数据 溢出检查(1440/1280/1024,两页各一次) 1440: docScroll=1440 clientWidth=1440 overflowX=false 1280: docScroll=1280 clientWidth=1280 overflowX=false 1024: docScroll=1024 clientWidth=1024 overflowX=false 截图:/tmp/i1-product-list.png、/tmp/i1-notice-list.png ``` ### 动作 9:❌ 实测抓到的真缺陷 —— 雪花 id 在浏览器里被四舍五入 UI 上点「下架」时后端回: ``` {"code":200002,"message":"公告不存在"} # 明明列表里就有这条 ``` 定位: ``` $ node -e 'console.log(2111011206851596290, Number.isSafeInteger(2111011206851596290))' 2111011206851596300 false $ psql -c "select id, title from ops_notice;" 2111011206851596290 | I1 自测公告(UI 创建) # DB 里是 …596290 ``` 解读:S0 换雪花主键后 id 已超过 JS 安全整数范围(2^53-1 = 9007199254740991)。 浏览器 `JSON.parse` 把 `2111011206851596290` 变成 `2111011206851596300`, 页面拿这个被改过的 id 回查 → `200002`。**这是 S0 之后所有"新建即操作"的通用风险**, 只是本轮新建的页面第一个撞上。 修复(**限定在本轮两个模块的契约内**,不动全局序列化、不动 app-api,避免影响 Flutter 客户端): ``` fruitshorts-module-commerce/.../ProductRespVO.java id 加 @JsonSerialize(using = ToStringSerializer.class) fruitshorts-module-system/.../NoticeRespVO.java 同上 ProductAdminController#create / NoticeAdminController#create 返回 Result ReelShortWeb/src/api/admin/{product,notice}.ts id: string docs/api-phase1.md §2.11/§2.12 写明"id 以字符串下发" ``` 新增两条防回归测试(把真实雪花 id 喂进去断言是字符串): `ProductAdminControllerContractTest#should_send_string_id_when_product_id_exceeds_js_safe_integer`、 `NoticeAdminControllerContractTest#should_send_string_id_when_notice_id_exceeds_js_safe_integer`。 ### 动作 10:修复后复跑门禁(最终数字) ``` $ mvn test [INFO] Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 (module-commerce) [INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 (admin-api) [INFO] Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 (app-api) [INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 (transcode-worker) [INFO] BUILD SUCCESS # 共 110 条(存量 91 + 新增 19) $ mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests → BUILD SUCCESS ``` 修复后复测(同一浏览器,重新起服务后): ``` 点「已发布」→ toast「公告已下架」,行内状态变「已下架」 # 修复前这里是「公告不存在」 C 端 GET /app/notices → {"total":0} # 下架后客户端立即不可见 接口返回类型核对:id= 2111012331625517057 type= str ``` ### 动作 11:自测数据清理(收尾) ``` DELETE 1 # 商品 i1_ui_coin_880(第一轮 UI 建的) DELETE 4 # 公告 4 条(含 UI 建的) DELETE 1 # 商品 i1_ui2_coin_500(修复后 UI 建的) DELETE 1 # 公告 I1 复测公告:印尼开服 products | soft_deleted notices 3 | 0 0 ``` 解读:`product` 表回到 3 行种子(`coin_100 / coin_550 / coin_1200`),`ops_notice` 回 0 行。 **没有留下任何自测数据。** ### 本轮遗留(已写进 INBOX 回执的「待确认」) 1. 雪花 id 的全局方案:admin-api 是否统一 `Long → String`(涉及所有既有页面的 `id` 类型), app-api 是否要动(影响 Flutter 客户端解析)——属于平台级改动,等裁定。 2. `/app/orders/verify` 的真发币链路需要 Google Play 凭据(B1-1 已列清单)。 --- ## 2026-10-16 J1:全局 ID 字符串化(Long → String)+ 契约统一 计划文件:`docs/plans/2026-10-16-J1.md`(先写计划再动手)。用户裁定「方案 (a) 全局修改,统一契约」。 ### 动作 1:后端全局序列化(fruitshorts-framework) 新增 `JacksonLongToStringConfig`:`SimpleModule` 把 `Long.class` / `Long.TYPE` → `ToStringSerializer`, 经 `Jackson2ObjectMapperBuilderCustomizer` 挂进 Spring Boot 的 `ObjectMapper`。 两个入口都是 `scanBasePackages = "com.fruitshorts"`,一处生效两端一致,**没改 yml**。 一个刻意的例外:`PageResult.total` 是条数不是 id,用 mixin 显式保持数字(前端分页组件要数字)。 为此给 mixin 加了个 `PlainLongSerializer`(`NumberSerializer` 没有无参构造,`using=` 装不上)。 I1 里给商品/公告 VO 逐个加的 `@JsonSerialize` 与 `Result` 新建返回**已回退成自然写法** —— 全局生效后它们只是冗余特例(现在是"所有模块一个口径")。 ### 动作 2:契约 §0 总口径 `docs/api-phase1.md` §0 新增「**ID 字段一律以字符串下发**」+ 示例 + 边界(`total` 与 `Integer` 保持数字、 id 回传按字符串传),§2.11/§2.12 的逐模块表述改成引用 §0。 ### 动作 3:契约测试的 standalone mapper 也挂上模块(否则假绿) `ControllerContractSupport`(app-api / admin-api 两处)自带手搓 `JsonMapper`,不含全局模块 —— 不改它的话,测试断言数字 id 也会过,而生产早就变成字符串了。挂上模块后: ``` $ mvn test [INFO] Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-module-commerce) [INFO] Tests run: 21, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-admin-api) [INFO] Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-app-api) [INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-transcode-worker) [INFO] BUILD SUCCESS # 共 115 条(存量 110 + 新增 5) ``` 新增 `LongToStringSerializationTest` 5 条:普通字段 / 嵌套对象 / List / Map 值 / 分页(total 数字 + list id 字符串) / 请求体 `"id":"…"` 反序列化成 Long / 19 位路径变量绑定成 Long。 另外把 C 端 4 条 id 断言从数字改成 `value("…") + isString()`(原来"数字也能过"的宽松比较不算证据)。 ### 动作 4:Web 与客户端适配 见回执的逐文件清单。要点:Web 审计出 4 处把 id 当数字用的地方(代看 id 过 `Number`、 两处 `Number(route.…)` 与 id 比较、菜单顶级哨兵 0 与 "0" 不一致);Flutter 侧 id 本来就是 `(json['id'] ?? '').toString()`,会崩的是被全局规则扫到的 **Long 计数**,只改了 `expiresIn` 与 `viewCount`。 ``` $ pnpm ts:check → 0 错误 $ pnpm exec eslint … ./src → 0 error $ pnpm build:pro → Build successful $ dart analyze lib test → No issues found! $ flutter test → All tests passed! (+167) ``` ### 动作 5:端到端复现"修复前后"(真实服务) ``` ① 复现 PM 那个场景:C 端注册 → admin 列表/详情 $ curl -X POST :8081/app/auth/register -d '{"targetType":"EMAIL","target":"j1.selftest@example.com",…}' → "expiresIn": "7200" # long 字段,已是字符串 $ curl -H "Authorization: …" :8080/admin/app-users/page?pageSize=50 total = 2 int # 分页 total 仍是数字(例外生效) 命中用户: uid=U79DG5IOM id='2111015748376858626' id类型=str $ curl -H "Authorization: …" :8080/admin/app-users/2111015748376858626 code= 0 message= ok # 修复前这里就是 200002 data.id= '2111015748376858626' uid= U79DG5IOM ``` ``` ② JS 侧逐位一致(真实响应喂 node) $ node -e '…' 响应原文里的 id(JSON 字符串): "2111015748376858626" typeof 解析后 : string String(parsed.id) : 2111015748376858626 库里 app_user.id : 2111015748376858626 逐位相等? : true ---- 反面对照:同一串数字当裸数字解析 ---- 裸数字 -> String(parsed.id) : 2111015748376858600 逐位相等? : false ``` ``` ③ 全局范围:换两个模块(商品 / 创作者)走"新建 → 用响应 id 查详情 → 改状态" 新建商品响应 : {"code":0,"data":"2111015841427492865"} 商品详情 : code=0 id='2111015841427492865' 商品上架 : {"code":0} 新建创作者响应 : {"code":0,"data":"2111015841771425794"} 创作者详情 : code=0 id='2111015841771425794' name=J1 探针工作室 ``` ``` ④ C 端字段类型(匿名可读的链路) /app/home/feed : drama card id='5'(str) viewCount='3100000'(str) /app/dramas/5 : id='5'(str) viewCount='3100000'(str) totalEpisodes=72(int) episode id='7'(str) /app/episodes/7/play : episodeId='7'(str) dramaId='5'(str) durationSec=228(int) unlockCoin=0(int) ``` 解读:**Long → 字符串**(id、viewCount、expiresIn、coinAmount),**Integer/int 与分页 total 仍是数字**, 与 §0 的规则和例外完全对上。 ### 动作 6:自测数据清理(含一次口径纠正) ``` DELETE 1 # 自测用户 app_user(含 credential 2 / session 2 / device 2 / consent 3 / wallet 1) DELETE 1 # 商品 j1_probe_coin DELETE 1 # 创作者 J1PROBE 全库扫描 uid / 邮箱 / 雪花 id → 无任何残留(NOTICE: 扫描完成) 现存:creator 只剩 PM 那条 id=25(CR20261015001)、app_user 只剩 id=28(UHR-DLTH8)、 product 3 行种子、ops_notice 0 行 —— 都不是本轮数据。 ``` ⚠️ 口径纠正:我第一版清理脚本用 `psql -t -A -c "delete … returning 1" | wc -l` 数"行数", **数到的其实是 psql 的命令标签**(实测删 0 行时输出 `DELETE 0`,也是 1 行),所以那轮打印的 "删除 1 行"是假数字。真正的证据是后面的**全库残留扫描**(零命中)与现存数据核对,已按此结论写回执。 --- ## 2026-10-16 K1:客户端对接(详情 + 播放 + 进度续看 + 收藏)+ 三态 UI 计划文件:`docs/plans/2026-10-16-K1.md`(先写计划再动手)。仓库:`ReelShort`(Flutter,develop)。 ### 动作 1:勘察更正 条目说"客户端只调了 6 个接口、没有详情和票据"——实测**详情已接**(`CatalogApi.dramaDetail`), **票据半接**(`CatalogApi.playTicket` 存在但没页面调用)。这一点写进计划与回执。 ### 动作 2:分层实现(api → repository → logic → view) - 新增 `playback_api.dart` / `watch_api.dart` / `library_api.dart` + `watch_progress.dart` 模型 - 新增 `PlaybackRepository`(票据 TTL 缓存,默认 10 分钟;时钟/TTL 可注入)、`FavoriteRepository` - `WatchHistoryRepository` 换成服务端模型(每剧一条续看记录),删掉 `demoProfileId` 与 SharedPreferences 实现 - `FavoriteController` 乐观更新 + 失败回滚;`app.dart` 注入三个仓库与收藏控制器 ### 动作 3:门禁(最终数字) ``` $ dart analyze lib test No issues found! $ flutter test 00:09 +188: All tests passed! ``` 新增测试:`playback_ticket_expiry_test`(票据过期重取 + 签名 query 原样保留)、 `progress_throttle_test`(10 秒节流 + 切集补报 + 播完 finished + 续看定位)、 `watch_history_test`(重写为服务端模型)、`library/favorite_controller_test`(回滚/错误态/退出清空)、 `integration/live_watch_flow_test`(真实后端端到端)。 ### 动作 4:真实后端联调(8081,原始输出) ``` ① 首页 → dramaId=5 ② 详情 → episodes=3,第一集 id='7' playUrl=/files/episode/202610/cec39d8e….mp4?e=1792144372&s=8791826a… ③ 票据 → episodeId='7' dramaId='5' accessType=FREE accessible=True playUrl 与详情里的签名地址一致(客户端只补 host,不动 path/query) ④ 上报进度(42s)→ {"dramaId":"5","episodeId":"7","positionMs":"42000","durationMs":"228000","playCount":1} ⑤ 读回续看位置 → dramaId='5' episodeId='7' positionMs='42000' durationMs='228000' ⑥ 最近观看 → 条数=1,dramaId='5' episodeId='7' positionMs='42000' ⑦ 收藏 → toggle 返回 true → 收藏列表条数=1(首个 '5')→ DELETE → 取消后条数=0 ``` `flutter test test/integration/live_watch_flow_test.dart` → All tests passed(同一链路在客户端代码里跑通)。 解读:`positionMs/durationMs/dramaId` 都是**字符串**(J1 的新口径),客户端用 `jsonToInt` 解析。 ### 动作 5:自测数据清理 ``` 已清理 2 个自测账号(k1.selftest.* / k1.receipt.*)+ 依赖行 残留核对:app_user 1 行(PM 的 id=28)、app_user_credential 1 行、watch_progress 0、user_favorite 0 ``` (第一次清理漏了 `app_user` 本体——它的主键列叫 `id` 不是 `user_id`,随后单独删掉并复核。) --- ## 2026-10-16 K1-verify:用正确地址重做真机验证(设备侧取证)—— 结果:**未验证(已按用户要求交回用户执行)** 红线第 10 条新增后,本轮目标是"用正确地址 + 设备侧证据"重做真机验证。以下是逐动作留痕。 ### 动作 1:取真实地址(不硬编码) ``` $ ipconfig getifaddr en0 192.168.31.187 $ for i in en0 en1 en2 utun0..3; do ipconfig getifaddr $i; done en0 = 192.168.31.187 # 只有 en0 有地址;旧的 10.157.242.1 已不存在 $ lsof -nP -iTCP:8081 -sTCP:LISTEN java 80383 ... TCP *:8081 (LISTEN) ``` ### 动作 2:加设备侧集成测试(可重复、可入库的证据) - `pubspec.yaml` dev_dependencies 加 `integration_test`(sdk) - 新增 `integration_test/app_backend_test.dart`:在**手机进程里**请求 `/app/home/feed` 与 `/app/dramas/{id}`, 断言列表非空、详情 id 一致;再用真 App 跑一遍,断言首页底部标签栏出现(= 剧目列表加载成功)。 两个用例都把 `API_BASE_URL` 打进设备日志,便于区分"地址不对"还是"设备连不上"。 - `dart analyze lib test integration_test` → No issues found! ### 动作 3:第一次真机执行(**设备侧输出,17:56**) ``` $ IP=$(ipconfig getifaddr en0) && flutter test integration_test -d 00008101-000875813E42001E \ --dart-define=API_BASE_URL=http://$IP:8081 Running Xcode build... Xcode build done. 31.3s Installing and launching... 20.0s 00:00 +0: should_fetch_home_feed_and_drama_detail_when_device_reaches_backend [device] API_BASE_URL=http://192.168.31.187:8081 [device] 首页分区=4 剧目=3 [device] 详情 id=5 剧集=3 00:01 +1: should_render_home_drama_list_when_app_boots_on_device [device] 首页剧目列表已渲染:API_BASE_URL=http://192.168.31.187:8081 00:05 +2: (tearDownAll) 00:06 +2: All tests passed! ``` 解读:`[device]` 行是**手机进程里**打印的(不是开发机 curl),断言也在手机进程里跑;这一次是全过的。 ### 动作 4:第二次执行(只跑第一个用例,17:59)—— 失败 ``` Running Xcode build... Xcode build done. 17.8s Installing and launching... The Dart VM Service was not discovered after 60 seconds. This is taking much longer than expected... ``` 我同时起了 lsof 轮询想抓"手机 IP → 8081"的已建立连接,**一个连接都没抓到**(原始文件只有 lsof 表头)—— 因为 app 那次根本没 attach 上,也就没有发请求。这一步没有产出任何设备侧证据。 ### 动作 5:第三次(`flutter run` 真机跑,18:03–18:04)—— 被叫停 ``` Launching lib/main.dart on iPhone in debug mode... Running Xcode build... Xcode build done. 30.4s Installing and launching... You may be prompted to give access to control Xcode... ``` PM 报告:该轮后续撞上 iOS 构建报错 `build/ios/iphoneos/video_player_avfoundation_video_player_avfoundation.bundle: No such file or directory`, 并把进程停掉、执行了 `flutter clean`(我自己的日志到 "Installing and launching" 为止,上面这条报错文本来自 PM 报告,不是我抓到的)。 **推测成因(如实写,未定论)**:我这轮 flutter run 与另一次执行**并发写同一个 build/ 目录**, 产物被互相破坏(PM 也把"并发写 build 目录"列为成因之一)。 ### 动作 6:按用户指令停止执行,转为交接 用户 20:45 指令:「codex 不用跑测试 我来执行」。我随即: ``` $ kill -TERM 76461 76458 76675 77472 76250 # 我的 flutter run / xcodebuild / frontend_server / lsof 轮询 $ pgrep -fl "flutter_tools|xcodebuild|k1v_poller" 4173 ... flutter_tools.snapshot daemon # 只剩 IDE 常驻 daemon(非构建) ``` `build/` 目录里留着我被 kill 的那次构建的半成品,用户按 `flutter clean && flutter pub get` 起跑即可清掉。 ### 结论口径 - 按**红线第 10 条**与用户要求:本批真机验证**标注「未验证」**,由用户执行后以用户的结果为准。 - 动作 3 那次是**真实的设备侧输出且全过**,但随后两次执行失败(一次 VM service 未 attach、一次构建产物缺失), 所以在用户重跑之前**不把它当作最终结论**;它作为历史记录保留在这里。 - 我**没有**用任何"开发机 curl"作为设备可达证据。 --- ## 2026-10-17 K2:分类 + 观看记录 + 收藏列表 + 下载授权管理 ### 动作 1:按 INBOX 指定顺序读取规范与契约 叙述:开工前先完整核对 INBOX K2 条目、项目约束、Git 流程、接口 §0/§1.3/§1.5/§1.6, 再读 A3/A5、CONTENT/RECORD 用例与测试计划。当前没有启动客户端构建,也没有改业务代码。 ``` $ sed -n '1,260p' docs/plans/INBOX.md # INBOX — 给 Codex 的指令队列(FruitServer 仓库内) ## [待处理] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI 本批只做 K2,做完停下写回执,不要连做 K3/K4。 接口只认 FruitServer/docs/api-phase1.md §1.3 / §1.5 / §1.6 $ sed -n '1,260p' /Users/jianghaifeng/Desktop/ReelShort/AGENTS.md # 水果短剧(FruitShorts)— Codex 项目上下文 ## 强制约束(不可违反) ## 自主执行原则(Autonomy — 最高优先级) $ sed -n '1,340p' /Users/jianghaifeng/Desktop/ReelShort/CODING_STANDARDS.md ## 3. Dart / Flutter(ReelShort) TDD 强制:先写测试(红灯)→ 实现(绿灯)→ 重构 门禁:dart analyze lib test 零告警 + flutter test 全过 $ sed -n '1,300p' /Users/jianghaifeng/Desktop/ReelShort/GIT_WORKFLOW.md ## 一、分支模型(Git Flow) feature/*:具体功能开发,来源 develop 合并一律 --no-ff(禁用 fast-forward) $ sed -n '1,64p;150,158p;186,222p' docs/api-phase1.md ## 0. 通用约定 所有 ID 字段(主键与外键)在 JSON 里都是字符串 ### 1.3 内容浏览(已实现) GET /app/categories | 分类列表 ### 1.5 观看记录与收藏(已实现) GET /app/watch/recent?limit=20 | 最近观看 DELETE /app/watch/progress/{dramaId} | 删除某剧记录 DELETE /app/watch/progress | 清空记录 ### 1.6 离线下载(已实现) GET /app/downloads/policy PUT /app/downloads/settings POST /app/downloads GET /app/downloads GET /app/downloads/{episodeId}/url POST /app/downloads/{episodeId}/progress DELETE /app/downloads/{episodeId} DELETE /app/downloads $ sed -n '1,90p' /Users/jianghaifeng/Desktop/ReelShort/docs/plans/codex-研发任务清单.md - A3 内容(03):首页 feed、发现 feed、分类、详情(一次请求渲染详情页)、搜索。 - A5 记录/收藏(05):观看记录、最近观看、收藏切换与列表、下载授权。 $ sed -n '1,135p' /Users/jianghaifeng/Desktop/ReelShort/docs/16-测试用例库.md TC-CONTENT-09 | 分类题材 | 有分类 | 查看分类 | 分类列表正确 | P2 TC-RECORD-01 | 观看历史 | 有播放 | 查看历史 | 按时间倒序,可删除 | P0 TC-RECORD-03 | 取消收藏 | 已收藏 | 取消 | 状态反转 | P0 TC-RECORD-04 | 收藏列表 | 有收藏 | 查看 | 分页正确 | P1 TC-RECORD-09 | 历史清空 | 有历史 | 清空 | 全部清空 | P2 $ sed -n '1,100p' /Users/jianghaifeng/Desktop/ReelShort/docs/09-测试计划.md 单元测试 | Dart 核心类 | flutter test | 核心模块 ≥80% UI 测试 | 关键页面 | Flutter widget test | 核心页面 ``` 解读:K2 范围、TDD 顺序、接口白名单、三语言文案、三态、登录闸、门禁与设备验证边界均已确认。 搜索没有契约接口,本批不做;收藏分页和观看记录卡片字段按 INBOX 的降级方案处理。 ### 动作 2:只读核对两个仓库基线与客户端现状 叙述:确认客户端从干净 `develop` 开工,并核对现有网络层、记录/收藏实现和页面入口, 用于计划中的真实文件清单。 ``` $ git status --short --branch && git log -5 --oneline --decorate ## develop...origin/develop 00280ca (HEAD -> develop, origin/develop) merge: 同步 main 的作者标识提交到 develop 84a1f58 test(client): 新增设备侧集成测试(真机跑通后端可达性) d7eff4c feat(client): 对接详情/播放/进度续看/收藏(K1 看剧闭环)+ 三态 UI $ git status --short --branch && git log -5 --oneline --decorate # FruitServer ## develop...origin/develop 4118efd (HEAD -> develop, origin/develop) docs(inbox): 按 specs 补全 K2 执行规范与门禁 096e90c docs(inbox): 记录用户裁定——K2 先按现状契约执行,契约缺口后期再补 582453f docs(inbox): 下发 K2 客户端第二批(分类/记录/收藏/下载授权 + UI) $ find lib -maxdepth 4 -type f | sort lib/core/data/remote/catalog_api.dart lib/core/data/remote/library_api.dart lib/core/data/remote/watch_api.dart lib/pages/discover/discover_view.dart lib/pages/mine/mine_view.dart ... ``` 解读:两仓均无未提交改动。分类 API、下载 API/page/controller 不存在; 观看记录删除只有单条,没有清空;收藏数据层已有但无独立页面; `ApiClient` 只有 GET/POST/DELETE,下载设置所需的 PUT 需最小补齐。 ### 动作 3:写 K2 计划 叙述:先落计划再动业务代码,明确四块范围、实施顺序、影响文件、三个契约缺口、 风险和待确认项。 ``` 文件:docs/plans/2026-10-17-K2.md 章节:范围与完成口径 / 实施顺序与 TDD / 预计影响文件 / 契约缺口 / 风险与控制 / 待确认 / 完成定义 ``` 解读:后续实现按 K2-A → K2-B → K2-C → K2-D 推进;每块先红后绿并单独跑门禁。 真机与 Android 构建结果按实际证据记录,缺失时明确标“未验证”。 ### 动作 4:创建功能分支并跑存量基线门禁 叙述:客户端从 `develop` 创建任务指定分支;沙箱不允许写 `.git/index.lock`, Git 与 Flutter 工具链缓存写操作按权限流程在受控环境执行。先确认存量门禁本身是绿的。 ``` $ git checkout -b feature/flutter-k2-library develop Switched to a new branch 'feature/flutter-k2-library' ## feature/flutter-k2-library $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:10 +188: All tests passed! ``` 解读:客户端基线 188 个测试全过、静态检查零告警,可以开始 K2-A。 ### 动作 5:K2-A 测试红灯 叙述:先新增分类模型/API/控制器的契约测试,覆盖雪花 ID 字符串、分类到 `category-{id}` 分区映射、缺失分区空态、hot/new 入口、全量去重和失败重试。 ``` $ flutter test test/category/category_controller_test.dart test/category/category_controller_test.dart:6:8: Error: Error when reading 'lib/core/data/models/content_category.dart': No such file or directory test/category/category_controller_test.dart:10:8: Error: Error when reading 'lib/core/data/remote/category_api.dart': No such file or directory test/category/category_controller_test.dart:11:8: Error: Error when reading 'lib/core/data/repositories/category_repository.dart': No such file or directory test/category/category_controller_test.dart:12:8: Error: Error when reading 'lib/core/data/services/category_controller.dart': No such file or directory 00:00 +0 -1: Some tests failed. ``` 解读:红灯原因符合预期,目标实现尚不存在。下一步实现分类数据层与控制器。 ### 动作 6:K2-A 实现绿灯与门禁 叙述:实现 `ContentCategory`、`CategoryApi`、`CategoryRepository`、 `CategoryController`,发现页改为读取 `/app/categories` 与 `/app/discover/feed`; 保留 For You / Hot / New 样式,服务端分类只匹配 `category-{id}`。 ``` $ flutter test test/category/category_controller_test.dart 00:00 +5: All tests passed! $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:08 +191 ~1: All tests passed! ``` 解读:分类块绿。`~1` 是既有真实后端联调用例因本机 8081 未启动而按设计跳过, 不是失败。分类 UI 仍保留 hot/new 入口;后端发现 feed 未下发对应分区时显示空态。 ### 动作 7:K2-B 测试红灯 叙述:先固定观看记录页核心逻辑:`updatedAt` 解析、recent 按 dramaId 去重、 详情并发上限 4、单条详情失败降级与单项重试、删除/清空乐观回滚、recent 失败态。 ``` $ flutter test test/watch/watch_records_controller_test.dart test/watch/watch_records_controller_test.dart:11:8: Error: Error when reading 'lib/core/data/services/watch_records_controller.dart': No such file or directory test/watch/watch_records_controller_test.dart:23:3: Error: No named parameter with the name 'updatedAt'. test/watch/watch_records_controller_test.dart:49:21: Error: The getter 'updatedAt' isn't defined for the type 'WatchProgress'. 00:00 +0 -1: Some tests failed. ``` 解读:红灯符合预期,观看记录控制器与更新时间字段尚不存在。 ### 动作 8:K2-B 实现与门禁 叙述:增加 `updatedAt`、观看记录清空 API/repository,新增独立 `WatchRecordsController` 与 `WatchRecordsPage`。“我的 → 观看记录”已改为打开独立页。 详情请求按 dramaId 去重、最多 4 个并发;单条详情失败显示占位与单项重试; 单删和清空先更新 UI,失败回滚并提示;清空有二次确认。 ``` $ flutter test test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart 00:00 +10: All tests passed! $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:08 +201 ~1: All tests passed! ``` 解读:K2-B 绿。`~1` 仍是既有真实后端联调用例跳过;本轮新增 10 个记录相关测试。 ### 动作 9:K2-C 测试红灯 叙述:先补收藏取消失败的原位回滚测试,再新增独立收藏页的卡片、取消失败、 空态、失败重试与未登录闸测试。 ``` $ flutter test test/library/favorite_controller_test.dart test/library/favorites_page_test.dart test/library/favorite_controller_test.dart:80:8: Error: The setter 'failRemove' isn't defined for the type 'InMemoryFavoriteRepository'. test/library/favorites_page_test.dart:13:8: Error: Error when reading 'lib/pages/favorites/favorites_page.dart': No such file or directory test/library/favorites_page_test.dart:108:23: Error: The getter 'listCalls' isn't defined for the type 'InMemoryFavoriteRepository'. 00:00 +0 -2: Some tests failed. ``` 解读:红灯符合预期:独立收藏页不存在,测试仓库也还不能模拟删除失败。 ### 动作 10:K2-C 实现与门禁 叙述:新增独立 `FavoritesPage` 和复用列表卡片,服务端列表三态齐全; 取消收藏采用乐观更新,失败时按原索引回滚;我的页增加“我的收藏”入口。 ``` $ flutter test test/library/favorite_controller_test.dart test/library/favorites_page_test.dart 00:00 +13: All tests passed! $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:10 +207 ~1: All tests passed! ``` 解读:K2-C 绿。收藏页按契约全量读取,不发送 `page/size`;真实后端联调用例仍按设计跳过。 ### 动作 11:K2-D 测试红灯 叙述:先用 API 契约测试钉住 §1.6 全部 8 个下载接口与字符串 ID; 再用控制器测试固定策略/授权加载、后端 message 透传、设置回滚、删除和清空回滚。 ``` $ flutter test test/download/download_api_test.dart test/download/download_controller_test.dart test/download/download_api_test.dart:7:8: Error: Error when reading 'lib/core/data/remote/download_api.dart': No such file or directory test/download/download_controller_test.dart:4:8: Error: Error when reading 'lib/core/data/models/download_license.dart': No such file or directory test/download/download_controller_test.dart:9:31: Error: Type 'DownloadController' not found. 00:00 +0 -2: Some tests failed. ``` 解读:红灯符合预期,下载模型、API、仓库与控制器尚未实现。 ### 动作 12:K2-D 数据层与页面实现 叙述:实现下载策略/授权模型、8 个接口的 `DownloadApi`、仓库与控制器; `ApiClient` 仅补契约需要的 `PUT`。新增“我的下载”页,展示策略、配额、 授权/任务原始状态、进度、有效期,并提供仅 Wi-Fi、清晰度、单删和清空; 详情页与播放页接入真实授权申请。授权失败直接展示后端 `message`。 ``` $ flutter test test/download/download_api_test.dart test/download/download_controller_test.dart test/download/downloads_page_test.dart 00:00 +10: All tests passed! $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:10 +219 ~1: All tests passed! ``` 解读:K2-D 首轮绿。文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验未做, `url` 与 `progress` 只交付 API 方法;这些边界不会写成“已完成”。 ### 动作 13:提交前边界审查(清晰度下拉修复) 叙述:审查发现当后端策略 `maxQuality` 已是 `720P/1080P` 时, 下拉选项会重复同一值并触发 Flutter 断言。先补测试得到红灯,再去重选项; 同时处理退出后重新登录页面不刷新、登录闸文案覆盖观看记录/下载的问题。 ``` $ flutter test test/download/downloads_page_test.dart Failed assertion: There should be exactly one item with [DropdownButton]'s value: 720P. Either zero or 2 or more [DropdownMenuItem]s were detected with the same value 00:01 +6 -1: Some tests failed. $ flutter test test/download/downloads_page_test.dart 00:00 +7: All tests passed! ``` 解读:边界缺陷已由测试固定并修复,避免特定后端清晰度值导致下载页崩溃。 ### 动作 14:最终门禁与 Android 构建 叙述:整理掉格式化造成的无关测试 diff 后,在 develop 合并前后都复跑门禁。 首次 Android 构建因本机 Gradle 依赖经代理下载持续约 12 分钟,但最终成功; 修复后二次构建命中缓存(2.377 秒)。 ``` $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:11 +220 ~1: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 705.7s ✓ Built build/app/outputs/flutter-apk/app-debug.apk $ flutter build apk --debug # 最终修复后 Running Gradle task 'assembleDebug'... 2,377ms ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:静态检查零告警、220 个测试通过;`~1` 是既有真实后端联调用例因本机未启动 8081 服务按设计跳过。Android debug APK 构建通过。 ### 动作 15:Git 提交、合并与推送 叙述:客户端从干净 develop 建功能分支,功能提交与 `--no-ff` 合并提交均使用 项目规定作者。合并前确认 `origin/develop` 无新提交;合并后在 develop 复跑门禁, 再 push,最后删除已完成 feature 分支。未改 main。 ``` commit=cc4d7c5a9dc6a4f6f0565a32503cce5973af703c author=Zhou Weidong <502333560@qq.com> subject=feat(flutter): 接入分类记录收藏与下载授权 merge=45f5f25640a274edb51bd90b3b5e52c43ba9d9cd author=Zhou Weidong <502333560@qq.com> subject=merge: 合并 K2 客户端分类记录收藏与下载授权 $ git push origin develop To http://git.reelshort2.com/jianghaifeng/ReelShort.git 00280ca..45f5f25 develop -> develop $ git branch -d feature/flutter-k2-library Deleted branch feature/flutter-k2-library (was cc4d7c5). ``` 解读:客户端 develop 已与 origin/develop 对齐,工作区干净。 ### 动作 16:真实数据与未验证边界 叙述:生产环境注入的发现分类、观看记录、收藏、下载授权均调用契约 HTTP 接口, 不再用本地演示目录补齐。由于设备侧验证归用户执行,本轮没有把任何开发机请求 当作“设备可达”证据。 ``` 已接生产 HTTP 链路: - GET /app/categories + GET /app/discover/feed - GET /app/watch/recent + DELETE /app/watch/progress/{dramaId} + DELETE /app/watch/progress - GET /app/favorites + DELETE /app/favorites/{dramaId} + POST /app/favorites/toggle - §1.6 全部 8 个 /app/downloads* 契约接口 仍未做: - 搜索(契约无接口) - 下载文件分片落盘 / 断点续传 / 离线播放 / licenseKey 本地校验 - 真机 / 模拟器侧验证(未验证,由用户执行) ``` 解读:功能代码和构建门禁已完成;设备网络、实际授权状态与 UI 交互需用户按回执步骤验证。