# 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 交互需用户按回执步骤验证。 ### 动作 17:用户真机验收清单 叙述:本动作只补齐验收证据与回执缺项,不新做功能、不改 ReelShort 产品代码。 本轮重新核对 ReelShort `develop` 为 `45f5f25`,FruitServer 文档开工前为 `698bb78`; 并复跑 `dart analyze lib test`、`flutter test`、`flutter build apk --debug`。 设备侧取证由用户执行,Codex 未执行。下面所有“预期”都是用户在手机上应看到的业务结果; 开发机 `curl` 只用于检查后端是否启动,不作为手机可达或设备侧验收证据。 #### 17.1 本轮门禁复跑原始输出 叙述:先确认 8081 监听状态,再按 ReelShort `develop` 顺序复跑三个客户端门禁。 Flutter 首次在受限沙箱内运行时被拒绝写 SDK 缓存 (`engine.stamp.tmp` / `engine.realm: Operation not permitted`), 随后在受控环境用同一组命令复跑;下面贴的是复跑成功的原始输出。 ``` $ lsof -nP -iTCP:8081 -sTCP:LISTEN (无输出,说明本轮复跑时 8081 未监听) $ flutter devices Found 3 connected devices: iPhone (mobile) • 00008101-000875813E42001E • ios • iOS 26.6.1 23G83 macOS (desktop) • macos • darwin-arm64 • macOS 15.5 24F74 darwin-arm64 Chrome (web) • chrome • web-javascript • Google Chrome 155.0.8059.80 Checking for wireless devices... No wireless devices were found. Run "flutter emulators" to list and start any available device emulators. ``` ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test test/integration/live_watch_flow_test.dart: should_complete_the_watch_flow_when_backend_is_running 客户端接口服务未启动,已跳过联调测试 test/integration/live_auth_test.dart: should_surface_business_error_when_sign_in_uses_unknown_email 客户端接口服务未启动,已跳过联调测试 test/integration/live_catalog_test.dart: should_parse_banner_and_sections_when_home_feed_is_fetched 客户端接口服务未启动,已跳过联调测试 test/integration/live_catalog_test.dart: should_order_episodes_by_number_and_attach_url_when_detail_is_fetched 客户端接口服务未启动,已跳过联调测试 test/integration/live_catalog_test.dart: should_return_play_url_when_episode_is_playable 客户端接口服务未启动,已跳过联调测试 00:10 +216 ~5: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 2,672ms ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:静态检查零告警;8081 未启动时测试结果是 `+216 ~5`,5 条跳过均来自 live 联调用例的 `markTestSkipped`,不是失败;Android debug APK 构建成功(缓存命中,`2,672ms`)。 本轮没有设备侧业务证据,也没有用开发机请求替代手机取证。 #### 17.2 从零前置条件 以下命令都需要在 Mac 终端执行。第 1 步只在首次搭建环境时执行;已有数据库可跳过。 1. 启动 PostgreSQL;首次环境再建角色、数据库并初始化脚本。 ``` $ brew services start postgresql@18 $ export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH" $ psql -d postgres -c "CREATE ROLE fruitshorts LOGIN PASSWORD 'fruitshorts';" $ psql -d postgres -c "CREATE DATABASE fruitshorts OWNER fruitshorts ENCODING 'UTF8';" $ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer $ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema.sql $ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data.sql $ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema-phase1.sql $ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/partition-maintenance.sql $ psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data-phase1.sql ``` 预期:五个 SQL 脚本按顺序执行完成,没有主键冲突或表不存在错误。 重复执行是幂等的;不要颠倒 `partition-maintenance.sql` 与 `data-phase1.sql` 的顺序。 2. 构建并启动客户端接口服务 8081。 ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer $ mvn -pl fruitshorts-app-api -am package -DskipTests $ java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar ``` 预期:启动日志出现 `Started AppApiApplication`;另开终端检查: ``` $ lsof -nP -iTCP:8081 -sTCP:LISTEN java ... TCP *:8081 (LISTEN) $ curl -fsS http://127.0.0.1:8081/healthz {"code":0,...,"data":{"status":"UP","app":"fruitshorts-app-api",...}} ``` `curl` 只证明开发机上的 8081 已启动,不能证明手机可达。 3. 准备手机与局域网。 - 手机和 Mac 连接同一个 Wi-Fi;关闭会隔离客户端的访客网络/VPN 代理。 - iPhone 首次启动 App 时,在系统弹窗中允许「本地网络」权限。 - Mac 防火墙若拦截 Java,到「系统设置 → 网络 → 防火墙 → 选项」允许 `java` 传入连接。 - 同网段地址用 `IP=$(ipconfig getifaddr en0)` 获取。若为空,先确认当前 Wi-Fi 网卡名, 再改用对应接口(如 `en1`)。 预期:终端打印形如 `192.168.x.x` 的局域网地址;手机浏览器能访问 `http://<该地址>:8081/healthz`,页面显示 `"status":"UP"`。 4. 获取 Flutter 运行设备号。 ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter devices ``` 预期:真机以 `iPhone (mobile) • • ios` 形式出现。本轮机器识别到的设备号是 `00008101-000875813E42001E`;换手机或重装后必须重新执行 `flutter devices`, 不能继续照抄旧 UDID。也可以执行 `xcrun xctrace list devices` 查 iPhone UDID。 这里的“运行设备号”用于 `flutter run -d`。下载绑定使用的设备号是另一个概念: 客户端首次运行会在本地生成并持久化,随后由 `ApiClient` 作为 `X-Device-Id` 发送, 用户不需要也不能在界面上手填。测试文件里的 `dio-check-device`、`k1-live-device` 只是测试固定值,不是生产 App 的设备号。 5. 用当前 Mac 的局域网 IP 注入接口地址并运行 App。 ``` $ cd /Users/jianghaifeng/Desktop/ReelShort $ git checkout develop $ git pull origin develop $ git rev-parse HEAD 45f5f25640a274edb51bd90b3b5e52c43ba9d9cd $ cd ReelShort $ IP=$(ipconfig getifaddr en0) $ echo "API_BASE_URL=http://$IP:8081" $ flutter run -d 00008101-000875813E42001E \ --dart-define=API_BASE_URL=http://$IP:8081 ``` `API_BASE_URL` 是 Dart 编译期 `--dart-define`,不是 `export API_BASE_URL=...` 环境变量; 改了地址必须停止 App 后用新参数重新 `flutter run`,热重载不会替换它。 真机不能用 `127.0.0.1` 访问 Mac,Android 模拟器才用 `10.0.2.2:8081`。 预期:App 启动后首页和发现页能加载封面、标题与分区;手机没有出现网络异常空屏。 如果页面一直报网络错误,先在手机浏览器验证第 3 步的 `/healthz`,再检查 Mac 防火墙和 本地网络权限;不要用开发机 `curl` 冒充设备通过。 #### 17.3 K2-A 分类验收 1. 打开「发现」页。 预期:先出现分类/分区加载态,随后分类条显示保留的 `For You`、`Hot`、`New` 入口, 再显示后端 `/app/categories` 返回的分类名称。默认种子数据下包括「都市情感」「重生逆袭」 「甜宠爱情」「悬疑复仇」;数据库改过名称时以实际后端返回名称为准。 2. 依次点「都市情感」「甜宠爱情」。 预期:顶部对应分类 chip 高亮;下方只展示 `/app/discover/feed` 中 `code == "category-{id}"` 的分区卡片。切换分类时内容跟着切换,不会保留上一个分类的卡片。 3. 点默认种子中没有已发布内容的「重生逆袭」或「悬疑复仇」。 预期:分区区域显示「这个分类暂时没有剧集。」。这是无分区空态,不应出现自行拼装的推荐列表、 旧分类残留卡片或网络失败文案。 4. 切回有内容的分类。 预期:卡片恢复显示;封面和标题与后端发现 feed 一致;点卡片可进入对应详情。 5. 失败与重试:停止 8081,冷启动 App 后打开「发现」页。 预期:分类区域显示「分类加载失败。」和「重试」按钮,不白屏。 重新启动 8081 后点「重试」,预期分类和分区恢复显示;必要时下拉刷新。 设备侧只能直接证明服务端分类名称与分类切换结果;`category-{id}` 的代码映射、 全量去重和并发/重试细节由 `test/category/category_controller_test.dart` 自动化覆盖。 解读:K2-A 的用户验收点是“分类条来自服务端、内容只取对应 `category-{id}` 分区、 缺失分区显示空态、失败可重试”;真机没有执行这些步骤,当前结论仍为未验证。 #### 17.4 K2-B 观看记录验收 1. 登录后播放任意一部剧的免费集约 10 秒,返回并进入「我的 → 观看历史」。 预期:页面先显示加载态,随后出现该剧卡片;卡片可见封面、剧名、 「第 N 集」、进度条、`续看至 mm:ss / mm:ss` 和「更新于 ...」。 即使该剧不在首页/发现已加载目录中,也应正常显示,不能因为目录里匹配不到而消失。 2. N+1 合并卡片:对同一部剧先后播放两个不同剧集,再播放另一部剧的一集,重新打开观看历史。 预期:同一部剧只出现一张卡片,不因看过多个剧集而重复;卡片展示该剧最新进度和对应集号。 不同剧各自一张卡片。详情是按 `dramaId` 去重后补取的,单条卡片加载中不应拖住其他卡片。 3. 单条删除:点某张卡片右侧的关闭图标。 预期:卡片立即从页面消失;返回再进入观看历史后仍然不存在。 4. 单删失败回滚:保持页面已加载,停止 8081,再点另一张卡片的关闭图标。 预期:卡片先乐观消失,随后回到原位置,并出现「删除记录失败,请重试。」。 重新启动 8081 后,删除动作可再次执行。 5. 清空:点页面右上方「清空」。 预期:先弹出「清空观看记录?」及「这会从当前账号移除全部观看记录。」; 点「暂不」列表保持不变;点「清空」后列表变空并显示「还没有观看记录。」。 6. 清空失败回滚:有记录且页面已加载时停止 8081,点「清空」并在二次确认中点「清空」。 预期:列表先清空,随后恢复原记录,并出现「清空观看记录失败,请重试。」。 7. 三态和失败重试:后端正常时进入页面观察加载态;清空后观察空态;停止 8081 后冷启动 App 再进入观看历史,观察「续看进度加载失败。」和「重试」。重启 8081 后点「重试」, 预期记录重新加载。 若单条详情请求失败,预期该卡片显示「剧集信息加载失败」与「重试」,其他卡片仍正常显示; 该分支和 4 并发上限由 `test/watch/watch_records_controller_test.dart`、 `test/watch/watch_records_page_test.dart` 自动化覆盖。真机验收重点看合并卡片和整页不被单条失败拖垮。 解读:K2-B 的用户验收点是三态、同一剧 N+1 详情合并为一张卡片、单删与清空的乐观回滚; 自动化测试已覆盖控制器行为,手机上的真实网络与回滚表现仍待用户取证。 #### 17.5 K2-C 收藏验收 1. 登录后在详情页或播放页点书签图标收藏一部剧。 预期:图标立即切换为已收藏状态;失败时图标回到原状态并显示「收藏操作失败,请重试。」。 2. 再收藏 1 至 2 部不同的剧,进入「我的 → 我的收藏」。 预期:页面展示服务端 `GET /app/favorites` 返回的全部收藏卡片,封面、标题与首页卡片一致; 列表不显示分页控件,也不发送自造的 `page/size`。点击卡片可进入详情。 3. 取消收藏:点某张收藏卡片右侧的粉色收藏图标。 预期:卡片立即消失;返回再进入我的收藏后仍然不存在。 4. 取消收藏失败回滚:保持列表已加载,停止 8081,再点另一张卡片的粉色图标。 预期:卡片先乐观消失,随后回到原位置,并出现「收藏操作失败,请重试。」。 重新启动 8081 后可再次取消。 5. 三态和失败重试:后端正常时进入页面观察加载态;取消全部收藏后观察「还没有收藏。」; 停止 8081 后冷启动 App 再进入我的收藏,观察「收藏列表加载失败。」和「重试」。 重启 8081 后点「重试」,预期列表恢复。 6. 服务端状态校准:在另一台设备或另一次登录中取消同一部收藏,再回到当前设备的我的收藏刷新。 预期:当前列表以服务端结果为准,不继续显示已被取消的收藏。 解读:K2-C 的用户验收点是服务端全量收藏列表、取消收藏后的乐观回滚和三态; 未登录引导另见 17.7,设备侧列表与回滚表现仍待用户取证。 #### 17.6 K2-D 下载授权验收 1. 登录后进入「我的 → 下载」。 预期:先出现加载态,随后显示策略卡: 「剩余 X / Y 个授权」、「授权有效期 N 天」以及后端返回的 `expirePolicyText` 原文; 下面显示「仅 Wi-Fi 下载」开关和「最高清晰度」下拉框。 2. 验证 §1.6 的 8 个接口。真机可观察结果如下: | # | 接口 | 真机操作与预期可见结果 | |---|---|---| | 1 | `GET /app/downloads/policy` | 打开下载页后策略卡出现,配额、授权天数、`expirePolicyText` 来自后端 | | 2 | `PUT /app/downloads/settings` | 切换仅 Wi-Fi 或清晰度后,页面立即显示新值;离开再进入仍保持 | | 3 | `POST /app/downloads` | 详情页/播放页点下载图标,出现授权成功或后端拒绝提示 | | 4 | `GET /app/downloads` | 打开下载页后出现当前设备授权列表;无授权时显示空态 | | 5 | `GET /app/downloads/{episodeId}/url` | 本批只提供 API 方法,UI 不调用;真机无可观察入口,见自动化测试 | | 6 | `POST /app/downloads/{episodeId}/progress` | 本批只提供方法,不做文件落盘/进度上报;真机无可观察入口,见自动化测试 | | 7 | `DELETE /app/downloads/{episodeId}` | 点单条右侧关闭图标后该授权消失,重新进入仍不存在 | | 8 | `DELETE /app/downloads` | 点「清空」并二次确认后授权列表变空,重新进入仍为空 | 3. 申请授权成功:进入任意可下载剧集的详情页或播放页,点下载图标(详情页右上角下载按钮, 或播放页右侧下载入口)。 预期:服务端接受时出现「下载授权已就绪。」;进入我的下载后能看到对应剧名、集号、 授权状态、任务状态、进度百分比、清晰度(后端有值时)和到期时间。 4. 授予失败透传后端 `message`:选择付费/未解锁剧集,或把当前设备配额用满后再次申请。 预期:SnackBar 原样显示后端返回的 `message`,不能替换成前端自造的“付费/未解锁/配额满”判断。 只有后端没有可用 message 时才回退到「申请下载失败。」。验收时记录手机屏幕上看到的完整原文; 当前环境没有执行这一步,因此具体拒绝文案仍属未验证。 5. 仅 Wi-Fi/清晰度成功:切换「仅 Wi-Fi 下载」,再选择 `AUTO`、`720P` 或 `1080P`。 预期:开关/下拉值立即变化,没有崩溃或重复选项;退出下载页再进入,值从 `GET /app/downloads/policy` 重新读取且保持。 6. 设置失败回滚:保持下载页已加载,停止 8081,再切换仅 Wi-Fi 或清晰度。 预期:值先乐观变化,随后恢复原值,并出现「下载设置更新失败。」。 7. 单删和清空。 - 点单条右侧关闭图标:预期该授权立即消失,重新进入后仍不存在。 - 停止 8081 后点另一条关闭图标:预期先消失再恢复,并提示「删除下载授权失败。」。 - 点「清空」:预期先弹出「清空下载授权?」及「这会吊销当前设备的全部下载授权。」; 点「暂不」不变,点「清空」后列表为空并显示「还没有下载授权。」。 - 停止 8081 后清空:预期列表先清空再恢复,并提示「清空下载授权失败。」。 8. 状态枚举后端驱动:查看授权卡上「授权:...」「任务:...」后的文本。 预期:显示后端原值,如 `ACTIVE/EXPIRED/REVOKED`、 `QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED`,前端不做中文映射或本地推算。 现有数据只可能展示实际出现的状态;其余枚举由下载 API/控制器测试覆盖, 设备侧未逐一构造全部后端状态,回执不能写成“所有状态已真机验证”。 9. 三态和失败重试:后端正常时进入观察加载态;清空授权后观察空态; 停止 8081 后冷启动 App 再进入下载页,观察「下载设置加载失败。」和「重试」。 重启 8081 后点「重试」,预期策略卡和授权列表恢复。 本批不落盘、不断点续传、不提供离线播放;即使授权状态为 `ACTIVE`,也不能据此验收文件已下载。 解读:K2-D 的用户验收点是策略卡、8 个接口的可见结果、后端 `message` 透传、 仅 Wi-Fi/清晰度、单删/清空以及状态枚举原样展示。URL/进度两个接口没有本批 UI 入口, 只能由自动化测试证明方法契约;真机拒绝文案和全部状态仍未验证。 #### 17.7 未登录 AuthGate 与失败重试 1. 在「我的」页退出登录。 预期:账号回到未登录状态。 2. 依次进入「我的 → 观看历史」「我的 → 我的收藏」「我的 → 下载」。 预期:三个页面都显示登录引导: 「登录后可继续」、「观看记录、收藏、评论和下载需要登录。」和「去登录」按钮; 不显示网络错误、白屏或空列表冒充无数据。 3. 在未登录的详情页点收藏或在详情页/播放页点下载。 预期:出现底部登录提示;点「去登录」进入登录页,点「暂不」关闭提示且不发业务请求。 4. 完成登录后返回观看历史、我的收藏或下载页。 预期:页面自动从登录引导切换到加载态,再展示对应数据;不需要杀 App 才能刷新。 5. 失败重试统一检查:先让页面加载成功,再停止 8081 并冷启动 App 进入页面。 预期:记录页显示「续看进度加载失败。」、收藏页显示「收藏列表加载失败。」、 下载页显示「下载设置加载失败。」,均有「重试」按钮;重启 8081 后点「重试」能恢复, 不出现白屏或无提示的假成功。 解读:未登录时三个独立页面都应由页面级 AuthGate 接住;登录或后端恢复后应重新加载。 这些手机交互本轮未执行,不能用开发机请求代替。 #### 17.8 测试跳过口径差异与稳定复现命令 PM 复核事实与本轮复跑结果一致: - 8081 未启动:`flutter test` 呈 `+216 ~5`,5 个 live 用例全部由 `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` 跳过。 - 8081 已启动:PM 复核呈 `+220 ~1`,4 个 live 用例通过; 当前演示数据下 `live_watch_flow_test.dart` 仍由播放断言前的 `markTestSkipped('这部演示剧没有可播剧集,已跳过播放相关断言')` 保留 1 个跳过。 如果换了一套有可播剧集的种子数据,这个用例可能通过并呈 `+221 ~0`;以实际输出为准。 5 个 live 用例及跳过机制: | 文件 | 用例 | 跳过机制 | |---|---|---| | `test/integration/live_auth_test.dart` | `should_surface_business_error_when_sign_in_uses_unknown_email` | 捕获连接失败后 `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` | | `test/integration/live_catalog_test.dart` | `should_parse_banner_and_sections_when_home_feed_is_fetched` | `serverUp()` 失败后 `markTestSkipped('客户端接口服务未启动,已跳过联调测试')` | | `test/integration/live_catalog_test.dart` | `should_order_episodes_by_number_and_attach_url_when_detail_is_fetched` | 同上 | | `test/integration/live_catalog_test.dart` | `should_return_play_url_when_episode_is_playable` | 同上 | | `test/integration/live_watch_flow_test.dart` | `should_complete_the_watch_flow_when_backend_is_running` | 注册失败后跳过“服务未启动”;注册成功但无可播剧集时跳过“没有可播剧集” | 稳定复现 `+216 ~5`: ``` $ lsof -nP -iTCP:8081 -sTCP:LISTEN (确认无输出) $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test 00:10 +216 ~5: All tests passed! ``` 稳定复现 `+220 ~1`: ``` # 终端 A:启动 8081 后保持运行 $ cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer $ mvn -pl fruitshorts-app-api -am package -DskipTests $ java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar # 终端 B:确认 8081 已监听后跑测试 $ curl -fsS http://127.0.0.1:8081/healthz $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test 00:xx +220 ~1: All tests passed! ``` 这 3 个 live 测试文件把地址硬编码为 `http://127.0.0.1:8081`,不读取 `--dart-define=API_BASE_URL`。因此复现测试口径必须在同一台 Mac 上启动 8081, 手机同一局域网只用于真机 App 验收,不能替代这里的环回联调。 本轮没有改这 3 个测试文件或产品代码。 解读:`~5` 与 `~1` 的差异来自 8081 是否启动,不是测试代码漂移; `~1` 还是当前演示数据下播放断言前的跳过,不能把 `~5` 误报成 5 个失败。 #### 17.9 提交作者口径差异登记 叙述:已发生的历史提交不改写,不回滚,不 force push。下面用原始 Git 输出登记事实。 ``` $ git show -s --format='%H%n author: %an <%ae>%n committer: %cn <%ce>%n subject: %s' cc4d7c5 cc4d7c5a9dc6a4f6f0565a32503cce5973af703c author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: feat(flutter): 接入分类记录收藏与下载授权 $ git show -s --format='%H%n author: %an <%ae>%n committer: %cn <%ce>%n subject: %s' 45f5f25 45f5f25640a274edb51bd90b3b5e52c43ba9d9cd author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: merge: 合并 K2 客户端分类记录收藏与下载授权 ``` 差异口径与最终裁定: - specs 规范要求提交作者统一为 `Zhou Weidong <502333560@qq.com>`,上述两个历史提交按此口径产生。 - 用户曾口头要求使用 `jianghaifeng `;该口径已作废,不再作为后续提交依据。 - 两个历史提交均已 push 到 `develop`;保留原始历史,不通过回滚或 force push 改写,不动 `main`。 - 已裁定:后续提交统一使用 `Zhou Weidong <502333560@qq.com>`,author 与 committer 均按此口径。 - 本回执提交 `cfce77a` 曾误用 `jianghaifeng ` / `trae-solo-agent `,现已通过 amend 修正为 `Zhou Weidong <502333560@qq.com>`;amend 后最终提交号以本轮 `git log -2` 原始输出为准。 解读:提交作者差异是已发生事实,已推送的历史提交不通过回滚或 force push 改写; jianghaifeng 口径作废,后续统一使用 Zhou Weidong,本回执提交已通过 amend 校正。 #### 17.10 仍未验证的边界 - 设备侧业务取证由用户执行,Codex 未执行;本轮没有手机屏幕截图、设备日志或手机到 8081 的连接证据。 - 具体后端拒绝 `message` 原文尚未在真机触发并记录。 - 8 个下载接口中,`GET /app/downloads/{episodeId}/url` 与 `POST /app/downloads/{episodeId}/progress` 本批没有 UI 调用入口,只有自动化 API 契约测试。 - `ACTIVE/EXPIRED/REVOKED` 与 `QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED` 只验证“前端展示后端原值”的机制;没有逐一构造全部状态做真机验收。 - 文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验不在 K2 范围,仍未实现,也不能验收。 - 收藏接口没有分页参数,本批按全量列表验收,不验收服务端分页。 - 搜索契约没有接口,本批不验收搜索。 --- ## 2026-10-17 K3-A 评论实施(【Codex】) 叙述:按 K3 任务书先补计划文档基线,再按评论 API/controller/UI 顺序做 TDD。 首轮测试在实现前运行,用于确认红灯;实现后运行同一组定向测试转绿。 ### 18.1 计划文档 命令:使用补丁工具增量更新 `docs/plans/2026-10-17-K3.md`。 原始结果: ``` Success. Updated the following files: M /Users/jianghaifeng/Desktop/ReelShort/FruitServer/docs/plans/2026-10-17-K3.md ``` 解读:保留既有 K3 计划,仅补记 ReelShort `45f5f25`、FruitServer `a0156ee` 和计划文件未跟踪的实测基线,没有覆盖已有内容。 ### 18.2 K3-A 首次红灯 命令: ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test test/comment/comment_api_test.dart test/comment/comment_controller_test.dart ``` 原始输出(节选): ``` Error when reading 'lib/core/data/remote/comment_api.dart': No such file or directory Error when reading 'lib/core/data/models/comment.dart': No such file or directory Error when reading 'lib/core/data/services/comment_controller.dart': No such file or directory 00:00 +0 -2: Some tests failed. ``` 解读:生产 API、模型和 controller 尚不存在,红灯原因符合预期。 ### 18.3 K3-A 数据层绿灯 命令:实现评论模型、API、仓库和 controller 后,重跑同一组定向测试。 原始输出: ``` 00:00 +9: All tests passed! ``` 解读:评论游标分页、String ID、发布回复、替换内容、敏感词 BLOCK message、 删除成功与失败回滚均通过。 ### 18.4 K3-A UI 红灯与绿灯 首次运行: ``` $ flutter test test/comment/drama_comments_sheet_test.dart ProviderNotFoundException: Could not find the correct Provider 00:00 +0 -5: Some tests failed. ``` 解读:旧弹窗仍直接依赖本地 `EngagementController`,符合 UI 红灯预期。 替换为服务端 controller、三态和回复输入后: ``` 00:01 +5: All tests passed! ``` 随后运行整个评论测试目录: ``` $ flutter test test/comment 00:01 +14: All tests passed! ``` 解读:API、controller 和弹窗测试共 14 项全部通过。 ### 18.5 K3-A 静态分析 首次分析: ``` info - lib/core/data/remote/comment_api.dart:23:9 - use_null_aware_elements info - lib/core/data/remote/comment_api.dart:24:9 - use_null_aware_elements 2 issues found. ``` 按 lint 改为 null-aware map value 后: ``` $ dart analyze lib test Analyzing lib, test... No issues found! ``` 解读:K3-A 达到零告警口径。真机交互未执行,设备侧取证仍由用户完成。 ### 18.6 K3-B 点赞红灯 命令: ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test test/interaction test/comment/drama_comments_sheet_test.dart ``` 原始输出(节选): ``` Error when reading 'lib/core/data/services/interaction_controller.dart': No such file or directory Method not found: 'InteractionController'. 00:01 +8 -1: Some tests failed. ``` 解读:互动 API 已先通过,缺失的剧集点赞 controller 构成 K3-B 红灯。 ### 18.7 K3-B 绿灯 实现 `InteractionController`、替换播放页右栏、去掉本地互动演示层后: ``` $ flutter test test/interaction test/comment/drama_comments_sheet_test.dart 00:01 +11: All tests passed! ``` 补齐评论点赞成功/回滚和整机 AuthGate 断言后: ``` $ flutter test test/comment/comment_controller_test.dart test/widget_test.dart 00:04 +23: All tests passed! ``` 静态分析: ``` $ dart analyze lib test Analyzing lib, test... No issues found! ``` 解读:剧集与评论点赞均使用服务端结果校准;失败会回滚;未登录点剧集赞会打开 AuthGate 且仓库调用次数为 0。剧集点赞数字未展示,符合契约缺口降级方案。 ### 18.8 K3-C 举报红灯 命令: ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test test/interaction/report_dialog_test.dart test/comment/drama_comments_sheet_test.dart test/widget_test.dart ``` 原始输出(节选): ``` Error when reading 'lib/pages/interaction/widgets/report_dialog.dart': No such file or directory Found 0 widgets with key [<'comment-menu-10'>] Found 0 widgets with key [<'detail-more'>] 00:05 +21 -5: Some tests failed. ``` 解读:举报弹窗、评论更多菜单和详情页更多菜单均尚未实现,红灯符合 K3-C 范围。 ### 18.9 K3-C 绿灯 实现固定五项 `SPAM/ABUSE/PORN/AD/OTHER` 单选、可选描述、评论与短剧举报入口后: ``` $ flutter test test/interaction/report_dialog_test.dart test/comment/drama_comments_sheet_test.dart test/widget_test.dart 00:05 +26: All tests passed! ``` 解读:举报原因与描述提交、评论举报成功、重复举报 message 透传、未登录 AuthGate 和详情页举报短剧均通过。 ### 18.10 K3-C 静态分析 首次分析: ``` info - lib/pages/drama_detail/drama_detail_view.dart:319:42 - use_build_context_synchronously info - lib/pages/drama_detail/drama_detail_view.dart:321:26 - use_build_context_synchronously info - lib/pages/drama_detail/drama_detail_view.dart:327:26 - use_build_context_synchronously 3 issues found. ``` 改为 State 自身 `context` + `mounted` 守卫后: ``` $ dart analyze lib test Analyzing lib, test... No issues found! ``` 解读:举报实现达到零告警口径。 ### 18.11 K3-D 分享回归 命令: ``` $ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort $ flutter test test/core/platform_share_service_test.dart test/formatting/localized_content_test.dart test/widget_test.dart ``` 原始输出: ``` 00:05 +25: All tests passed! ``` 解读:系统分享文本保持“标题 → 简介 → footer”顺序;en/zh/id 三语均生成 非空标题、简介和 `FruitShorts` footer;播放页分享入口回归通过。 ### 18.12 代码规模红线修正 提交前检查命令: ``` $ wc -l lib/pages/player/widgets/drama_comments_sheet.dart 535 lib/pages/player/widgets/drama_comments_sheet.dart ``` 解读:主弹窗超过单文件 500 行红线。将评论条目与交互回调拆到 `comment_tile.dart` 后: ``` $ wc -l lib/pages/player/widgets/drama_comments_sheet.dart lib/pages/player/widgets/comment_tile.dart 396 lib/pages/player/widgets/drama_comments_sheet.dart 149 lib/pages/player/widgets/comment_tile.dart ``` 拆分后复跑互动定向测试: ``` 00:06 +51: All tests passed! ``` 解读:行为未回归,两个文件均低于 500 行。 ### 18.13 最终门禁 ``` $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:11 +241 ~1: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 6.8s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:唯一跳过项是既有 live watch 用例,因客户端接口服务未启动而按测试机制跳过。 真机/设备侧集成、截图、设备日志仍未执行。 ### 18.14 Git 合并与推送 ReelShort 功能提交: ``` d361034226ec2bc431548d45068dc60046e5e473 author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: feat(flutter): 接入互动评论点赞举报 ``` `--no-ff` 合并提交: ``` 9db6935308e65342be46c81419c2992ed2df1df0 author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: merge: 合并 K3 客户端互动能力 ``` 推送: ``` To http://git.reelshort2.com/jianghaifeng/ReelShort.git 45f5f25..9db6935 develop -> develop ``` 解读:K3 已合并到 ReelShort `develop` 并推送,未操作 `main`。 ### 18.15 本人评论举报错误透传补充修复 逐条复核任务书时发现:本人评论的更多菜单原先只提供删除,无法触发 “不能举报自己发布的内容”这一后端 message。先补红灯用例,再让菜单始终提供 “举报评论”,本人评论额外提供“删除评论”。 红灯: ``` $ flutter test test/comment/drama_comments_sheet_test.dart --plain-name should_show_backend_message_when_reporting_own_comment Found 0 widgets with text "Report comment" 00:00 +0 -1: Some tests failed. ``` 修复后: ``` 00:00 +1: All tests passed! ``` 最终重跑: ``` $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:12 +242 ~1: All tests passed! $ flutter build apk --debug ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 修复提交: ``` 1488ee8f85671de96c70347c1b17fb9c4556bd88 fix(flutter): 透传举报本人评论错误 ``` `--no-ff` 合并提交: ``` e1147cfcbabcdf457e427bbd118c32d37f8d9b15 merge: 合并 K3 举报错误透传修复 ``` 推送: ``` 9db6935..e1147cf develop -> develop ``` 解读:报告本人内容现在会把后端错误原样展示;author/committer 均保持统一。 --- ## 2026-10-17 K5 克莱因蓝主题与印尼语适配(【Codex】) ### 19.1 诊断分支收口 叙述:上一项 iOS 首页诊断插桩已提交在独立分支,不并入 K5,也不阻塞干净基线。 ``` $ git status --short --branch ## feature/flutter-ios-home-diagnostics $ git log -1 --oneline 7efbed9 chore(flutter): 增加首页加载失败诊断日志 ``` 解读:诊断提交保留为未合并 feature 分支;K5 从 `develop e1147cf` 新建, 后续所有 K5 改动均在该基线上。 ### 19.2 K5 计划 叙述:先写 `docs/plans/2026-10-17-K5.md`,记录方案 A 色值、影响文件、TDD、 风险和待确认,再开始代码。 ``` Success. Updated the following files: A docs/plans/2026-10-17-K5.md ``` ### 19.3 K5 首次红灯 命令: ``` $ flutter test test/theme test/config/locale_controller_test.dart test/mine/mine_logic_test.dart test/widget_test.dart ``` 原始输出(节选): ``` Error when reading 'lib/core/theme/app_theme.dart': No such file or directory Member not found: 'bluePressed' Member not found: 'onPrimary' Method not found: 'resolveAppLocale' Found 0 widgets with text "Bahasa Indonesia" Expected: empty Actual: [ 'lib/core/theme/app_colors.dart:5: static const blue = Color(0xFF2F7BFF);', ... ] 00:07 +22 -5: Some tests failed. ``` 解读:主题 token/ThemeData、locale 解析、语言选项和旧蓝扫描全部按预期红灯。 ### 19.4 K5 实现与定向绿灯 实现内容: - `AppColors` 新增 `bluePressed/onPrimary`,更新 `blue/blueSoft`。 - 新增 `AppTheme.light/dark` 显式 ColorScheme 和控件主题。 - 页面旧蓝清理,深色高亮改 `blueSoft`,大面积选中态改 `bluePressed`。 - `resolveAppLocale` 支持 en/zh/id,未知系统语言回退 id。 - 两个语言入口增加 Bahasa Indonesia;快捷抽屉改 Wrap。 - 日期/时间改用 `MaterialLocalizations`。 定向测试: ``` $ flutter test test/theme test/config test/formatting test/mine/mine_logic_test.dart test/widget_test.dart test/auth test/library test/watch test/download test/drama_detail 00:08 +97: All tests passed! ``` 旧蓝扫描补跑: ``` $ rg -n '0xFF2F7BFF|0xFF8FB4FF|Colors\.blue|_orange|seedColor' lib -g '*.dart' ``` 结果:无匹配(仅保留 `AppColors.blue/blueSoft/bluePressed` 等 token 引用)。 ### 19.5 K5 最终门禁 ``` $ 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 ``` 解读:全量测试除既有 live watch 跳过外全部通过,APK 构建成功。 ### 19.6 Git ``` 10d448c8ba93aa2762e14babdb528805897b3992 author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: feat(flutter): 应用克莱因蓝主题与印尼语适配 e52d81881c19202c7adc8d13a6f78f78e4f6182f author: Zhou Weidong <502333560@qq.com> committer: Zhou Weidong <502333560@qq.com> subject: merge: 合并 K5 克莱因蓝主题与印尼语适配 To http://git.reelshort2.com/jianghaifeng/ReelShort.git e1147cf..e52d818 develop -> develop ``` 解读:K5 已以 `--no-ff` 合并到 ReelShort develop 并推送,未操作 main。 --- ## 2026-10-17 K6 登录页、底部导航与震动反馈(【Codex】) ### 20.1 K6 计划与 UI 红灯 叙述:先写 `docs/plans/2026-10-17-K6.md`,再基于 K5 `develop e52d818` 创建 `feature/flutter-k6-auth-nav-ui`。首轮登录/底栏测试在实现前运行: ``` $ flutter test test/auth/auth_page_test.dart test/widget_test.dart Found 0 widgets with text "FruitShorts" Found 0 widgets with key [<'auth-email'>] Expected: Color(... #101114) 00:07 +22 -5: Some tests failed. ``` 解读:品牌区、表单 Key/显隐/焦点、底栏主题样式尚未实现,红灯符合任务范围。 ### 20.2 K6 UI 绿灯 实现登录品牌区、autofill、密码显隐、next/done、busy 禁用、主题错误色、 Small screen 滚动和底部导航主题胶囊后: ``` $ flutter test test/auth test/home/home_nav_ui_test.dart test/widget_test.dart test/config/l10n_id_test.dart test/theme 00:06 +42: All tests passed! ``` 提交:`e609e3a feat(flutter): 优化登录页与底部导航`。 ### 20.3 Haptics 补充需求红灯 FruitServer `93af19e` 新增 K6-C 点击震动反馈。补 fake Haptics 和触发测试后: ``` Error when reading 'lib/core/platform/haptics.dart': No such file or directory No named parameter with the name 'haptics' 00:02 +1 -4: Some tests failed. ``` 解读:Haptics 抽象、Provider 注入与页面触发尚未实现。 ### 20.4 Haptics 实现与定向绿灯 新增 `Haptics` + `FlutterHaptics`,通过 Provider 注入;tab selection、 关键操作 light、危险确认 medium;默认实现吞掉平台异常,页面调用全部 `unawaited`。新增 fake/静态扫描/异常静默测试。 ``` $ flutter test test/core/haptics_test.dart test/core/haptics_scan_test.dart test/auth test/home/home_nav_ui_test.dart test/comment test/watch test/download test/library test/drama_detail test/player 00:05 +98: All tests passed! ``` 提交:`8a481d5 feat(flutter): 增加关键操作震动反馈`。 ### 20.5 K6 最终门禁 ``` $ 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 跳过。 ### 20.6 Git ``` e609e3a feat(flutter): 优化登录页与底部导航 8a481d5 feat(flutter): 增加关键操作震动反馈 0cedaab merge: 合并 K6 登录导航与震动反馈 To http://git.reelshort2.com/jianghaifeng/ReelShort.git e52d818..0cedaab develop -> develop ``` 解读:K6 已合并并推送,author/committer 均为 Zhou Weidong,未操作 main。 ### 20.7 iOS Xcode SPM 构建修复(K6 收尾) 叙述:只修改 `Runner.xcscheme` PreActions。新增 Python 无关的 shell 兜底链解析 Flutter,先 `flutter pub get`;原 prepare action 保留,并改为 同一路径解析后执行 `xcode_backend.sh prepare`。 XML 校验: ``` $ xmllint --noout ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (无输出,退出码 0) ``` 清理 ephemeral 后直接 xcodebuild: ``` $ rm -rf ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage $ xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build Resolve Package Graph the package at '.../FlutterGeneratedPluginSwiftPackage' cannot be accessed xcodebuild: error: Could not resolve package dependencies ``` 解读:Xcode 在运行 scheme PreActions 之前先解析 Package Graph,因此 “包目录完全不存在”的首次冷启动无法仅靠 scheme PreAction 自动生成。 这是 Xcode 执行顺序限制,不是 XML 语法错误。 使用 `flutter pub get` 恢复包后: ``` $ flutter pub get Got dependencies! $ xcodebuild -list -project ios/Runner.xcodeproj Resolved source packages: FlutterGeneratedPluginSwiftPackage: .../ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage ... ``` 完整重建精简日志: ``` $ xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build Resolve Package Graph Resolved source packages: Run pre-actions export SCHEME_ACTION_NAME="Bootstrap Flutter SPM Package" Resolving dependencies... Got dependencies! ** BUILD SUCCEEDED ** ``` 解读:SPM 包存在时,PreAction 能成功刷新依赖,prepare 和完整 iPhoneOS Debug 构建通过并完成签名。冷启动自动生成未通过验证,用户仍需在 新克隆/`flutter clean`/手动删除 ephemeral 后先运行 `flutter pub get`。 Git: ``` cc037a2956848128c4899df1f5092fea44d3620b fix(ios): 刷新 Flutter SPM 包后再准备框架 0cedaab..ac706a4 develop -> develop ``` 解读:scheme 修复已合并推送;author/committer 均为 Zhou Weidong。 ### 20.8 K6 导航白线与观看历史缺陷修复 ### 导航白线 首次红灯: ``` Expected: a value less than <0.2> Actual: <1.0> ``` 解读:当前深色 ColorScheme 默认 `outlineVariant` 实为白色,确认了白线根因。 修复:新增 light/dark outline token,两个 ColorScheme 显式设置 `outline/outlineVariant`;导航边框继续使用主题 token。 ### 观看历史真实诊断 后端日志: ``` 2026-10-17T16:35:22.816+08:00 POST /app/auth/register 耗时 242ms 2026-10-17T16:35:37.123+08:00 POST /app/watch/progress 耗时 51ms ``` 数据库只读查询: ``` uid | drama_id | episode_id | position_ms | duration_ms UKZBA9IBE | 1 | 1 | 1522 | 91326 UKZBA9IBE | 5 | 7 | 0 | 227880 ``` 真实 recent 响应: ``` {"code":0,"message":"ok","data":[ {"dramaId":"5","episodeId":"7",...}, {"dramaId":"1","episodeId":"1",...} ]} ``` 临时直连 controller 诊断: ``` watch-records-diagnostic error=null records=2 record drama=5 episode=7 detail=闪婚后,傅先生每天都在撒糖 loading=false failed=false record drama=1 episode=1 detail=替嫁新娘:总裁的掌心宠 loading=false failed=false ``` 解读:真实用户同类数据在 API、JSON 解析、controller 和详情补取链路均正常, 未复现设备侧 401/100000、解析异常或详情失败。后端日志中的 429 仍属于 其他联调设备,用户 watch 请求没有该错误。 确认的客户端缺陷是 controller 吞掉真实异常: ``` Expected: '账号不存在,请先注册' Actual: 'progressError' ``` 修复:debug 打印异常类型、原文与堆栈;ApiException message 透传页面错误态。 ### 定向与全量门禁 ``` $ 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): 修复导航白线与观看记录错误透传 ac706a4..b7b1c50 develop -> develop ``` 解读:修复已合并推送;用户设备特定 error 仍需设备日志确认。 --- ## 2026-10-17 K7 首页轮播与站内通知(【Codex】) ### 21.1 K7 计划与首次红灯 叙述:先写 `docs/plans/2026-10-17-K7.md`,从干净 `develop b7b1c50` 创建 `feature/flutter-k7-carousel-notices`。 首次轮播/通知测试: ``` No named parameter with the name 'active' Error when reading 'lib/core/data/models/notice.dart': No such file or directory Error when reading 'lib/core/data/remote/notice_api.dart': No such file or directory 00:00 +0 -3: Some tests failed. ``` 解读:轮播增强、通知数据层和页面尚不存在,红灯符合预期。 ### 21.2 轮播绿灯 实现 Timer、交互暂停、active/TickerMode、圆点、失败占位与语义后: ``` $ flutter test test/discover/cover_carousel_test.dart 00:00 +7: All tests passed! ``` ### 21.3 通知实现与定向绿灯 实现 Notice model/API/repository/controller/Page、我的页入口和三语文案后, 真实匿名接口返回: ``` $ curl -sS 'http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=20' {"code":0,"message":"ok","data":{"total":0,"list":[]}} ``` 通知与轮播定向测试: ``` $ flutter test test/discover/cover_carousel_test.dart test/notice All tests passed! ``` 期间修正 320px 下类型/时间同一行 3.5px 溢出,改为双 Flexible。 ### 21.4 K7 最终门禁 ``` $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:14 +284 ~1: All tests passed! $ flutter build apk --debug ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 21.5 Git ``` b870956665f5268070c691533cc2a334f2aa7f59 feat(flutter): 增加轮播自动播放与站内通知 b7b1c50..3e8703b develop -> develop ``` 解读:K7 已合并推送;author/committer 均为 Zhou Weidong。 --- ## 2026-10-17 K7 真机缺陷修复:观看历史 Provider 与首页轮播数据源 ### 22.1 范围与分支 叙述:用户真机验收后报告观看历史仍报错、首页轮播仍不显示。本轮只修客户端 Provider 注册与首页轮播数据源,不改后端、Web、specs 或既有发现页分区逻辑。 ReelShort 从 `develop 3e8703b` 拉 `fix/flutter-home-watch-provider-banners`;FruitServer 从 `develop 9c6aaed` 拉 `docs/flutter-home-watch-device-fix`,本仓库只改 `docs/plans/PROGRESS.md`。 ### 22.2 先红:复现设备报告 真实 `LumaDramaApp` Provider 树打开「我的 -> 观看历史」: ```text ProviderNotFoundException: Could not find the correct Provider above this _WatchRecordsBody Widget ``` 首页 feed 返回 2 条 banner、discover feed 返回 0 条 banner: ```text Expected: exactly one matching candidate Actual: Found 0 widgets with type "CoverCarousel" ``` 解读:两处红灯分别复现了用户报告的 Provider 缺失和轮播为空。 ### 22.3 修复 - `lib/app.dart` 在现有 Repository Provider 附近注册 `Provider.value(value: _catalogRepository)`,与 `CatalogController` 共用同一实例。 - `lib/pages/discover/discover_view.dart` 改为监听 `CatalogController.feed.banners`,修复后首页 tab 使用 `/app/home/feed` 的 banners;发现页分区仍使用 `CategoryController`,没有新增接口。 - 新增 `test/watch/watch_history_provider_test.dart`:在真实 Provider 树打开 观看历史,断言不抛错并展示空态。 - 新增 `test/discover/home_banner_source_test.dart`:home banners 为 2、 discover banners 为空时,断言渲染两个圆点、首图显示且 4.5 秒后自动切到次图。 ### 22.4 门禁原始输出 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test test/watch/watch_history_provider_test.dart test/discover/home_banner_source_test.dart 00:01 +2: All tests passed! ``` ```text $ flutter test test/discover/cover_carousel_test.dart test/watch/watch_records_page_test.dart 00:00 +12: All tests passed! ``` ```text $ flutter test 00:16 +286 ~1: All tests passed! ``` 解读:完整套件 286 项通过,1 项联调测试因客户端接口服务未启动按设计跳过; 输出中的 `[WatchRecordsController] ...` 异常堆栈来自故意注入失败的既有测试。 ### 22.5 Git ```text 5058853 fix(flutter): 注册目录仓库并修正首页轮播数据源 6cc5bfc merge: 合并首页轮播与观看历史真机修复 3e8703b..6cc5bfc develop -> develop ``` 修复提交与合并提交的 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 22.6 用户真机验证步骤与预期 以下设备侧取证由用户执行,开发机测试不能替代真机证据。 1. 使用最新 `develop` 重新构建并安装,登录用户设备 `d-mvc33zh5-pemv3e`。 预期:`flutter run` 终端不再出现 `ProviderNotFoundException`。 2. 打开「我的 -> 观看历史」。 预期:页面正常打开;有历史时显示剧目与进度,无历史时显示空态,不再进入 Provider 错误页。 3. 回到发现页并等待首页 feed 加载完成。 预期:顶部轮播显示后端 `/app/home/feed` 返回的 2 张 banner,底部有两个圆点。 4. 等待 4.5 秒。 预期:轮播自动切到第 2 张;手动拖动后仍可从当前页继续自动轮播。 设备侧截图、日志与最终验收结论由用户执行。 --- ## 37. K21 登录页 UI 优化 ### 37.1 基线与布局 ```text ReelShort: e44fbdf style(ui): 合并 K20 客户端视觉统一 FruitServer: af28161 docs(plans): 下发 K21 登录页 UI 优化任务 ``` 计划先提交为 `0e3ddfc docs(plans): 增加 K21 登录页实施计划`。 布局由顶部对齐 + ListView 改为: - `LayoutBuilder + SingleChildScrollView + ConstrainedBox + Center`; - 正常高度品牌与卡片垂直居中; - 高度小于 720 时使用紧凑横向品牌,保证 600 高测试视口与提交按钮可达; - 返回按钮改为 `Positioned + SafeArea` 浮层,不占内容流; - 小屏键盘仍保留 `28 + viewInsets.bottom` 底部 padding,可滚动到 `auth-submit`。 ### 37.2 品牌与文案 - `auth-brand-mark` 改用 `assets/icon/splash_mark.png`,尺寸仍为 48×48。 - 品牌名 26/w800,tagline 14/blueSoft;正常视口居中竖排。 - 登录使用现有 `authSubtitle`;注册新增 `authSignUpSubtitle`: ```text en: Create an account to keep your list, history, and progress. zh: 创建账号,保存你的片单、观看记录和进度。 id: Buat akun untuk menyimpan daftar, riwayat, dan progresmu. ``` - 注册态密码框下显示 `authPasswordHint`,Key `auth-password-rule`;登录态隐藏。 ### 37.3 字段错误与交互 `AuthLogic` 新增 `fieldError`,`validate()` 记录具体字段错误,输入变化或切换模式时清除。 邮箱/密码字段增加 semantic error 红框: ```text errorBorder / focusedErrorBorder color = colorScheme.error width = 1.5 ``` 同时: - `_submit()` busy 首行短路; - 异步 catch 的 `setState` 前判断 mounted; - 密码框 IME done 提交同样经过 busy 短路; - 提交按钮增加 alpha 0.22 的辣椒红轻阴影,背景色与按下色不变。 ### 37.4 保留项 ```text auth-brand-mark 48x48 auth-card radius 20 / border outlineVariantDark / width 1 input height 52 / radius 14 / fill bar / focus blue 1.5 submit height 52 / radius 26 / blue / bluePressed auth-enter-animation 220ms / tween 0 -> 1 mode keys, title copy, hidden compatibility copy error capsule radius 12 / alpha 0.1 / semantic color 320x568 + keyboard 260 scroll reachability ``` 主 glow 颜色 alpha 保持 ≤0.18;新增 warm glow Key `auth-background-warm-glow`,alpha 上限 0.06。 ### 37.5 TDD 与门禁 新增 `test/auth/auth_k21_test.dart`,先红灯后转绿: ```text $ flutter test test/auth/auth_k21_test.dart 00:00 +2: All tests passed! ``` 全部 Auth 测试: ```text $ flutter test test/auth/auth_visual_test.dart \ test/auth/auth_page_test.dart test/auth/auth_k21_test.dart 00:01 +17: All tests passed! ``` 全量门禁: ```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 ``` ### 37.6 Git ```text 8a96f34 feat(auth): 优化登录页布局与字段反馈 b1c69e2 feat(auth): 合并 K21 登录页优化 e44fbdf..b1c69e2 develop -> develop ``` author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 37.7 用户真机验收 设备侧取证由用户执行: 1. 普通手机竖屏打开登录页:品牌 hero 与卡片垂直居中,返回键为顶部浮层。 2. 切换注册:副标题变为注册文案,密码框下出现规则提示。 3. 提交空邮箱:邮箱出现 1.5px 红色边框,错误胶囊仍保留。 4. 输入非法邮箱后修改:字段红框清除。 5. 输入弱密码:密码框变红,规则提示仍在。 6. 连续点提交或按键盘 Done:只发出一次请求,busy 时按钮与模式切换禁用。 7. 320×568 小屏弹出键盘:滚动后提交按钮可点击。 8. 检查主辣椒红 glow、暖色副 glow 均淡且不影响文字对比度。 9. 检查品牌图为 `splash_mark.png`,显示 48×48,无拉伸。 --- ## 36. K20 客户端视觉统一 ### 36.1 基线与计划 ```text ReelShort: 42ce1d1 fix(account): 合并网络异常文案修复 FruitServer: 810d9fc docs(plans): 下发 K20 客户端视觉统一任务 ``` 计划先提交为 `3435784 docs(plans): 增加 K20 视觉统一实施计划`。 客户端从干净 develop 创建 `feature/flutter-k20-visual`。 ### 36.2 统一 Token 新增 `lib/core/theme/app_dimens.dart`: ```text 页面边距 16 卡片圆角/内边距 16/16 内嵌块圆角/内边距 12/12 胶囊圆角 20 caption/body/list/section/page = 12/14/15/16/20 详情标题 24 导航标签 11 主按钮/整页主操作 = 48/52 点击区 44 导航高度/图标 = 64/22 进度条高度/圆角 = 4/4 账号卡高度/图标/标签 = 64/18/11 观看占位高度 114 ``` `AppColors.favorite = #D74A7D`;主色继续使用辣椒红 `#C4322A`。 ### 36.3 十四项实现 1. 新增 `AppBottomNav` / `AppBottomNavItem`,替换 home、discover、mine、 player fallback 四处导航;保留 `tab-home` 等既有 Key。 2. 账号入口卡改为 64 高、18 图标、11 标签和 44 以上点击区。 3. 清理全部 9.5/10 业务字号,任务书列出的 11 字号提升至 12。 4. 页面边距、卡片圆角、内边距和列表间距按 token 收敛。 5. 任务书列出的硬编码颜色替换为 `AppColors`;收藏红心使用 `favorite`。 6. 详情、记录、下载进度条统一 4 高、4 圆角、blueSoft/white24。 7. Filled/OutlinedButton 最小高度 48;empty 重试色改 blueSoft。 8. 页面标题 20/w800,详情标题 24,区块与列表标题按 token。 9. `themeMode = ThemeMode.dark`;根节点统一状态栏和系统导航栏颜色。 10. 8 个独立列表页补底部安全区。 11. 详情加载/失败分支补 AppBar,封面顶部加黑色渐变,边距 24→16。 12. 点赞/收藏选中态改实心图标;分类与 my tabs 补 selected 语义。 13. HeaderPill、LoginButton、分类 Chip 点击区补到 44。 14. 观看记录加载占位改为 114,并复用 12 内边距。 ### 36.4 既有断言兼容 首轮回归发现两项既有断言与通用 token 冲突: ```text Auth card radius expected 20, actual 16 EmptyState subtitle expected 12.5, actual 12 EmptyState action expected 13, actual 14 ``` 按“不改测试断言”要求保留为显式 token 例外: ```text authCardRadius = 20 emptyStateSubtitle = 12.5 emptyStateAction = 13 ``` 其余通用卡片仍为 16,正文 caption 仍为 12。 ### 36.5 门禁 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:24 +341: All tests passed! ``` ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 7.8s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 36.6 Git ```text df5fbe5 feat(ui): 增加视觉 token 与统一导航 c5bfd00 style(ui): 统一页面字号间距与状态样式 5f1df40 fix(ui): 保留空态既有字号口径 e44fbdf style(ui): 合并 K20 客户端视觉统一 42ce1d1..e44fbdf develop -> develop ``` 提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 36.7 用户真机验收 设备侧取证由用户执行: 1. 首页底部导航:检查 64 高、22 图标、11 标签、主色胶囊选中态和四个原 Key。 2. 发现页:检查三处导航一致,分类 Chip 点击区 44、选中语义和 header 按钮高度。 3. 我的页:检查账号卡 64 高、四入口可点,菜单/语言入口仍在首屏可用。 4. 详情页:检查返回按钮可读、封面顶部渐变、24 标题、16 边距、进度条 4 高。 5. 观看记录:检查加载占位 114、真实卡片 12 padding、进度条和 12 以上辅助文本。 6. 下载页:检查策略卡、授权卡、12 圆角、进度条和安全区。 7. 公告/收藏/账号中心:检查卡片 12/16 圆角、正文 ≥12、底部安全区。 8. 播放器抽屉与右侧栏:检查点赞/收藏选中实心图标,fallback 导航与其他页一致。 9. 浅色封面详情页:确认返回/更多图标因顶部黑色渐变保持可读。 10. Android 系统栏:确认状态栏浅色图标、导航栏为 `AppColors.bar`。 --- ## 35. K19 非 ApiException 网络文案修复 ### 35.1 缺陷与修复 Trae 复核发现,账号中心三个逻辑类在断网、超时等非 `ApiException` 失败时,把内部哨兵键直接交给 `errorText`,会原样上屏: ```text accountProfileLoadFailed accountProfileSaveFailed accountDevicesLoadFailed accountExportLoadFailed ``` 修复严格沿用既有 `errorText` 约定: - 非 `ApiException` 统一置 `networkError`; - `ApiException.message` 继续原样透传; - 未修改 `errorText` 映射表; - 未修改既有测试断言。 修改位置: ```text ReelShort/lib/pages/account/account_profile_logic.dart ReelShort/lib/pages/account/account_devices_logic.dart ReelShort/lib/pages/account/account_export_logic.dart ``` ### 35.2 TDD 与定向测试 先给 `FakeAccountRepository` 增加通用异常开关,并新增四个页面测试。 修复前红灯: ```text 00:01 +4 -4: Some tests failed. should_show_localized_network_copy_when_profile_load_fails should_show_localized_network_copy_when_profile_save_fails should_show_localized_network_copy_when_devices_fail should_show_localized_network_copy_when_export_fails ``` 修复后定向测试: ```text $ flutter test test/account/account_pages_test.dart 00:01 +8: All tests passed! ``` 四个用例分别使用中文、英文、印尼语当前语言环境,断言上屏 `l10n.networkError`,且不出现内部哨兵键。 ### 35.3 最终门禁 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:23 +339: All tests passed! ``` ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 7.2s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 35.4 Git ```text 27ab7ce fix(account): 本地化网络异常文案 42ce1d1 fix(account): 合并网络异常文案修复 9b3ab9d..42ce1d1 develop -> develop ``` 提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 35.5 用户验证建议 设备侧取证由用户执行: 1. 登录后断开手机网络,打开「个人资料」。 预期:显示当前语言的网络异常文案,不出现 `accountProfileLoadFailed`。 2. 恢复网络后修改昵称,再断网保存。 预期:显示当前语言的网络异常文案,不出现 `accountProfileSaveFailed`。 3. 断网打开「登录设备」。 预期:显示当前语言的网络异常文案,不出现 `accountDevicesLoadFailed`。 4. 断网打开「数据导出」。 预期:显示当前语言的网络异常文案,不出现 `accountExportLoadFailed`。 --- ## 34. K19 客户端账号中心 ### 34.1 基线与实施范围 基线确认: ```text ReelShort: 7d68699 fix(config): 合并开发默认服务器地址 FruitServer: 5c93ba9 docs(plans): 下发 K19 账号中心接口对接任务 ``` 计划文档先提交为 `6b5e70e docs(plans): 增加 K19 账号中心实施计划`。 客户端从干净 develop 创建 `feature/flutter-k19-account`,只对接: ```text GET /app/me PUT /app/me GET /app/me/devices GET /app/me/export POST /app/me/deactivate POST /app/me/deactivate/cancel ``` 接口口径以后端实际代码为准:设备直接返回实体数组;导出返回 `profile / credentials / consents / devices`;注销请求只发送 `reason`。 ### 34.2 TDD 与实现 先用不存在的 `AccountApi/AccountProfile` 跑红灯,确认测试在实现前失败: ```text Error: Error when reading 'lib/core/data/remote/account_api.dart': No such file or directory Some tests failed. ``` 第一批实现后: ```text $ flutter test test/account/account_api_test.dart 00:00 +7: All tests passed! ``` 登录态同步先红灯: ```text Error: The method 'applyProfile' isn't defined for the type 'AuthController'. ``` 实现 `applyProfile(AuthUser)` 后: ```text 00:00 +1: All tests passed! ``` 逻辑层覆盖资料保存回调、设备读取、导出复制、注销清会话和撤销失败文案: ```text $ flutter test test/account/account_logic_test.dart 00:00 +5: All tests passed! ``` 页面与入口先红灯,随后用稳定 Key 实现: ```text $ flutter test test/account/account_pages_test.dart \ test/account/mine_account_entry_test.dart 00:01 +6: All tests passed! ``` 实现文件: ```text lib/core/data/remote/account_api.dart lib/core/data/models/account_profile.dart lib/core/data/models/account_device.dart lib/core/data/models/account_export.dart lib/core/data/repositories/account_repository.dart lib/pages/account/* lib/pages/mine/mine_view.dart lib/core/data/services/auth_controller.dart lib/app.dart lib/l10n/app_{en,zh,id}.arb ``` ### 34.3 首屏布局回归 首次全量测试时,新增账号卡把既有偏好设置和观看历史入口挤出可视区: ```text Found 0 widgets with text "App Language" Found 0 widgets with text "Watch History" Some tests failed. ``` 处理:不改既有测试断言,把账号卡收敛为 48px 紧凑横向入口, 保留四项图标、分隔线和账号标题;原有菜单项改用紧凑密度。 三个受影响测试回归: ```text $ flutter test test/home/home_nav_ui_test.dart \ test/watch/watch_history_provider_test.dart test/widget_test.dart 00:06 +21: All tests passed! ``` ### 34.4 最终门禁 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:26 +335: All tests passed! ``` ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 10.0s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 34.5 Git ```text 44f6feb feat(account): 接入个人中心接口模型 ef7f4ce feat(account): 同步资料保存后的登录态 c68f024 feat(account): 增加账号中心页面逻辑 da09511 feat(account): 增加账号中心页面与入口 6fce24d refactor(account): 收敛账号页组件结构 7151bc9 style(account): 保持账号入口首屏可用 9b3ab9d feat(account): 合并 K19 账号中心 7d68699..9b3ab9d develop -> develop ``` 所有提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 34.6 已知缺口 - 无注销状态查询:不能预先判断是否在冷静期,撤销失败直接展示后端文案。 - 无设备下线接口:设备页只读。 - 无头像上传接口:头像只展示。 - 资料响应不含生日:本批生日只能本次设置,不能回填或清空。 ### 34.7 用户真机验证 以下步骤必须在设备侧执行并取证,开发机不代跑: 1. 登录后打开「我的」。 预期:钱包卡后、菜单卡前出现「账号」卡,四项入口完整可点击。 2. 打开「个人资料」。 预期:服务端字段回填;修改昵称/性别并保存后,我的页昵称同步更新。 3. 打开「登录设备」。 预期:设备号首尾可见、中间脱敏;平台、机型、时间、状态正确;空列表显示空态。 4. 打开「数据导出」。 预期:显示资料、凭据、授权记录数、设备数;复制 JSON 成功。 5. 打开「注销账号」。 预期:显示 15 天冷静期和立即登出提示;提交必须经过二次确认。 6. 确认注销。 预期:显示 `scheduledAt`,本地 token 清空并回到未登录状态。 7. 重新登录后撤销注销。 预期:有申请时成功;无申请时显示后端“没有待处理的注销申请”。 设备侧截图、日志和最终验收结论由用户执行。 --- ## 2026-10-17 K10 统一空列表占位 ### 26.1 范围、分支与计划 叙述:新增统一 `EmptyState`,替换任务书表格中的 10 处纯文字空态。只改空态 视觉、三语 l10n 与测试,不改列表数据、接口、加载/失败逻辑。 - 客户端分支:`feature/flutter-k10-empty-states`,基线 `19babb8`。 - 文档分支:`docs/k10-empty-states`,基线 K10 任务书 `3f84bc6`。 开工前先写并提交计划: ```text f1d56b1 docs(plans): 增加 K10 统一空态实施计划 ``` ### 26.2 先红:组件与页面 组件文件尚不存在: ```text $ flutter test test/empty_state/empty_state_test.dart Error when reading 'lib/core/widgets/empty_state.dart': No such file or directory Failed to load ".../empty_state_test.dart" ``` 页面测试已能运行,但五处空态仍是旧实现: ```text $ flutter test test/library/favorites_page_test.dart test/download/downloads_page_test.dart test/watch/watch_records_page_test.dart test/notice/notices_page_test.dart test/comment/drama_comments_sheet_test.dart Found 0 widgets with type "EmptyState": [] 00:03 +28 -6: Some tests failed. ``` 解读:组件、收藏、下载、记录、通知、评论空态均未接入,红灯符合 K10 范围。 ### 26.3 组件实现 新增: ```text 128 lib/core/widgets/empty_state.dart ``` 规格: - 默认 96x96、圆角 28、`AppColors.blue` 11% 背景,图标 42。 - compact 64x64、圆角 18、图标 28。 - 标题 16/w600,副标题 13 muted,最多 2 行。 - 可选 TextButton 最小高度 44,主题主色。 - 深色主题图标 `blueSoft`,浅色主题图标 `blue`。 - 外层 Semantics 只读标题;图形和文本排除重复语义,操作按钮仍可访问。 新增三语副标题: ```text en: New content will appear here. zh: 有新内容时会显示在这里。 id: Konten baru akan muncul di sini. ``` ### 26.4 10 处替换 ```text lib/pages/home/home_view.dart movie_outlined lib/pages/discover/widgets/home_drama_grid.dart grid_view_rounded / search_off_rounded lib/pages/favorites/favorites_page.dart favorite_border_rounded lib/pages/notices/notices_page.dart notifications_none_rounded lib/pages/drama_detail/drama_detail_view.dart video_library_outlined lib/pages/mine/mine_view.dart bookmark_border_rounded / history_rounded lib/pages/downloads/downloads_page.dart download_outlined lib/pages/watch_records/watch_records_page.dart history_rounded lib/pages/player/widgets/drama_comments_sheet.dart chat_bubble_outline_rounded(compact) ``` 首页将原合并的 `error || empty` 分支拆开: - error 继续使用 `error_outline_rounded`、`catalogError`、重试与 debug API 地址。 - 空列表才使用 `EmptyState(noStories)`。 收藏、通知、记录、评论等失败态仍保留原错误与重试,不用 `EmptyState`。 ### 26.5 定向与全量绿灯 组件测试: ```text $ flutter test test/empty_state/empty_state_test.dart 00:00 +4: All tests passed! ``` 组件与五处页面: ```text $ flutter test test/empty_state/empty_state_test.dart test/library/favorites_page_test.dart test/download/downloads_page_test.dart test/watch/watch_records_page_test.dart test/notice/notices_page_test.dart test/comment/drama_comments_sheet_test.dart 00:03 +38: All tests passed! ``` ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:18 +302 ~1: All tests passed! ``` 解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;既有页面 用例保留,新增断言只增加 `EmptyState` 校验,不删除原用例。 ### 26.6 Android debug 构建 用户 iOS `flutter run` 仍在运行,未终止该会话。为避免争用原项目 `build/`, 将同一工作区(排除 build/.git/.dart_tool/ephemeral)同步到独立临时副本后构建: ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 6.9s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:K10 最终代码通过 Android debug 构建,且原 iOS 运行会话未被中断。 ### 26.7 Git ```text 6d08330 feat(flutter): 统一空列表占位组件 0466b4b merge: 合并 K10 统一空态组件 19babb8..0466b4b develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 - 2026-10-19 K22.1 首屏 loading:`.app-loading` 文字色改为 `#f8f1ea`,Web 提交 `a97b8cc`,合并 `3e8312e`,`pnpm build:pro` 通过。 ### 26.8 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 使用最新客户端 `develop`,分别切换深色和浅色主题进入一个空列表页面。 预期:显示 96x96 柔和蓝色底、图标、标题与可选说明,不白屏、不溢出。 2. 打开收藏、下载、观看记录、通知页面并清空对应数据。 预期:均显示统一 `EmptyState`,原有文案与清空/刷新逻辑正常。 3. 打开无评论的剧目评论弹窗。 预期:显示 compact 64x64 图标占位,弹窗仍可滚动与输入。 4. 打开无剧集详情或首页无剧目场景。 预期:显示对应空态;重试按钮行为保持不变。 5. 模拟接口失败。 预期:仍显示原错误图标、错误文案和重试,不显示空态占位。 6. 在 320px 宽小屏检查长标题、副标题和按钮。 预期:最多两行、可换行且无横向溢出,按钮点击区域不小于 44pt。 设备侧截图、语音朗读与最终验收结论由用户执行。 --- ## 2026-10-17 K11 首页 3 列与全部分类 ### 27.1 范围、分支与计划 叙述:在 K9 首页「全部剧目」网格上做最小修订,每行 3 个,分类首项从 `For You` 改为「全部」。只改网格参数、首个分类 label 和对应测试。 - 客户端分支:`feature/flutter-k11-home-grid-3col`,基线 `0466b4b`。 - 文档分支:`docs/k11-home-grid-3col`,基线任务书 `2a06190`。 开工前先写并提交计划: ```text f97574b docs(plans): 增加 K11 首页三列网格实施计划 ``` ### 27.2 先红:3 列与全部文案 先更新 K9 网格测试的期望值并执行: ```text $ flutter test test/discover/home_all_dramas_grid_test.dart Expected: exactly one matching candidate Actual: _TextWidgetFinder: 00:01 +0 -1: Some tests failed. ``` 解读:分类首项仍显示旧文案,3 列断言在首个失败处之后尚未执行;红灯与 K11 目标一致。 ### 27.3 实现 `home_drama_grid.dart`: ```text crossAxisCount: 3 mainAxisSpacing: 14 crossAxisSpacing: 10 childAspectRatio: 2 / 3 ``` `discover_view.dart`: ```text (code: CategoryController.allCode, label: l10n.all) ``` 已核对三份 ARB 原本已有 `all`: ```text lib/l10n/app_en.arb:49: "all": "All", lib/l10n/app_zh.arb:49: "all": "全部", lib/l10n/app_id.arb:49: "all": "Semua", ``` 解读:直接复用现有三语 getter,未添加重复 JSON key,也未改动 l10n 生成文件。 ### 27.4 定向绿灯 ```text $ flutter test test/discover/home_all_dramas_grid_test.dart 00:01 +5: All tests passed! ``` 测试同时断言: - `crossAxisCount == 3` - `crossAxisSpacing == 10` - `mainAxisSpacing == 14` - `childAspectRatio == 2/3` - 首个 chip 为 `All`,且位于 `Hot` 左侧 - 320px 小屏 3 列布局无溢出 两个既有首页壳层用例仍保留,只更新旧分类断言: ```text $ flutter test test/widget_test.dart 00:05 +17: All tests passed! ``` ### 27.5 全量门禁与构建 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:18 +302 ~1: All tests passed! ``` 解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 用户 Android `flutter run` 仍在运行,未终止该会话。为避免争用原项目 `build/`,将当前工作区同步到独立临时副本后执行: ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 21.8s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 27.6 Git ```text f0db631 feat(flutter): 首页网格改为三列并首位显示全部 b5a8656 merge: 合并 K11 首页三列网格 0466b4b..b5a8656 develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 27.7 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 使用最新客户端 `develop` 进入首页。 预期:分类 chips 第一项显示「全部 / All / Semua」,其后仍是热门、最新和 服务端分类。 2. 查看「全部剧目」列表。 预期:每行 3 个剧目,列间距 10、行间距 14,左右边距保持 16。 3. 检查长标题和类型。 预期:标题最多 2 行、类型最多 1 行,不横向或纵向溢出。 4. 点击「全部」及其他分类。 预期:网格内容按现有逻辑更新,点击卡片仍进入对应详情。 5. 在 320px 宽设备或小屏模拟器滚动列表。 预期:3 列布局稳定,无卡片重叠或整页溢出。 设备侧截图、滚动和分类切换结果由用户执行。 --- ## 2026-10-17 K12 首页去标题与空态重做 ### 28.1 范围、分支与计划 叙述:删除 K11 首页「全部剧目」标题,使分类 chips 后直接进入网格;同时将 K10 空态从实心圆角块改为双层同心圆。只改首页间距、`EmptyState` 和对应测试。 - 客户端分支:`feature/flutter-k12-home-empty-polish`,基线 `b5a8656`。 - 文档分支:`docs/k12-home-empty-polish`,基线任务书 `87a37e9`。 开工前先写并提交计划: ```text e49664c docs(plans): 增加 K12 首页与空态视觉修订计划 ``` 构建前已确认用户真机会话停止,进程检查无并发 Flutter run/build。 ### 28.2 先红:标题与旧空态 ```text $ flutter test test/empty_state/empty_state_test.dart test/discover/home_all_dramas_grid_test.dart Found 0 widgets with key [<'empty-state-art-inner'>]: [] Expected: Size(60, 60) Actual: Size(64, 64) Expected: <260> Actual: <280.0> Found 1 widget with key [<'home-all-dramas-title'>] 00:02 +2 -7: Some tests failed. ``` 解读:旧实心圆角空态仍为 96/64、最大宽 280,首页标题仍存在,红灯与 K12 目标一致。 ### 28.3 首页去标题 删除 `home-all-dramas-title` Text 及其间隔,分类 Sliver 的底部 padding 改为 14。 实际 chips 控件增加稳定 Key: ```text home-category-chips ``` 测试读取 chips 底边与首张卡片的 RenderBox 顶边,断言间距: ```text closeTo(14, 0.1) ``` ### 28.4 空态双层同心圆 最终规格: ```text default outer: 84, blue alpha 0.055, border alpha 0.11 default inner: 56, blue alpha 0.11 compact outer: 60; inner: 40 icon: 27 / compact 21 title: 15 w600 subtitle: 12.5, height 1.45 button: 13, min height 44 padding: 24x28 / compact 16x16 max width: 260 ``` 新增 Key: ```text empty-state-art-inner empty-state-padding ``` `empty-state-art`、`empty-state-icon`、`empty-state-title`、 `empty-state-action` 保持,外层 Semantics 仍只读标题。 ### 28.5 定向与全量绿灯 组件、首页和既有壳层: ```text $ flutter test test/empty_state/empty_state_test.dart test/discover/home_all_dramas_grid_test.dart test/widget_test.dart 00:06 +26: All tests passed! ``` 既有壳层测试中的 `All Dramas` 文本锚点改为稳定网格 Key `home-all-dramas-grid`,原导航流程与用例全部保留。 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:17 +302 ~1: All tests passed! ``` 解读:302 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 ### 28.6 APK 构建 ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 16.6s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 28.7 Git ```text 2dd4627 feat(flutter): 精简首页标题并重做空态视觉 b762289 merge: 合并 K12 首页与空态视觉修订 b5a8656..b762289 develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 28.8 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 打开首页并查看分类 chips 下方。 预期:不再显示「全部剧目」标题,chips 下方 14px 后直接出现 3 列网格。 2. 打开任一空列表页面。 预期:显示双层同心圆,外层比内层更淡,图标居中,不再是大块实心圆角矩形。 3. 分别切换深色和浅色主题。 预期:深色图标为 `blueSoft`,浅色图标为 `blue`,文字与背景对比清晰。 4. 检查带副标题和操作按钮的空态。 预期:标题 15、副标题 12.5、按钮 13,间距舒展,按钮点击区域不少于 44pt。 5. 打开无评论弹窗。 预期:使用 60/40 compact 双层圆,图标 21,上下留白 16,不挤压输入区。 6. 在 320px 宽小屏检查空态和首页。 预期:最大宽 260 生效,无横向溢出,标题最多两行。 设备侧截图、深浅主题与小屏结果由用户执行。 --- ## 2026-10-17 K13 运行时服务器地址设置 ### 29.1 范围、分支与计划 叙述:解决 debug APK 不带 `--dart-define` 时固定回退到 `.local` 的问题。 新增 debug-only 运行时地址,优先级为本地设置 > dart-define > 现有默认。 - 客户端分支:`feature/flutter-k13-dev-server-setting`,基线 `b762289`。 - 文档分支:`docs/k13-dev-server-setting`,基线任务书 `1a5951c`。 开工前先写并提交计划: ```text 6405dff docs(plans): 增加 K13 开发服务器地址实施计划 ``` 当前有真机 `flutter run` 会话,未主动终止;构建在独立副本中完成。 ### 29.2 先红:运行时配置与入口缺失 ```text $ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart Error when reading 'lib/core/config/server_endpoint.dart': No such file or directory Member not found: 'AppConfig.clearRuntimeBaseUrl' No named parameter with the name 'showServerSetting' Some tests failed. ``` 解读:运行时解析器、AppConfig 覆盖入口和 debug 设置参数均不存在,红灯符合 K13 范围。 ### 29.3 运行时解析与启动顺序 新增: ```text 86 lib/core/config/server_endpoint.dart ``` 关键规则: ```text preference key: dev.api_base_url runtime > dart-define(API_BASE_URL) > default normalize: trim, http(s), non-empty host, trailing slash removed release: ServerEndpoint.load(debug: false) returns before preferences are read ``` `main()` 在 `runApp` 前执行: ```dart await ServerEndpoint.load(); runApp(const LumaDramaApp()); ``` 因此 `LumaDramaApp` 创建共用 ApiClient 前,运行时地址已经加载完成。 ### 29.4 Debug 设置入口 我的页设置弹窗增加服务器地址行: - 显示当前 `AppConfig.apiBaseUrl`; - 编辑弹窗预填当前地址; - 保存合法值后提示重启生效; - 非法值显示三语错误且不写 SharedPreferences; - 恢复默认清除 `dev.api_base_url`; - 设置面板改为 85% 屏高内可滚动,避免小屏溢出。 可见性通过 `MyPage.debugShowSetting` 注入,默认 `kDebugMode`。release 编译时默认 false,不显示该行,也不读取本地覆盖。 新增三语 key: ```text serverAddress serverAddressHint saveAddress restartToApply restoreDefault invalidServerAddress ``` ### 29.5 定向与全量绿灯 ```text $ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart 00:00 +7: All tests passed! ``` 覆盖: - 本地设置 > dart-define > 默认; - trim、末尾斜杠、http/https; - `ftp://`、缺 scheme、空 host 拒绝; - debug=false 不应用本地覆盖; - save/clear SharedPreferences; - debug 入口可见、注入 false 不可见; - 非法地址不保存并显示错误。 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:19 +309 ~1: All tests passed! ``` 解读:309 项通过,1 项联调测试因客户端接口服务未启动按设计跳过。 ### 29.6 无 dart-define 的 Debug APK 用户真机 run 未终止。将当前工作区同步到独立临时副本,并明确不带构建参数: ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 23.4s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:即使 APK 没有 `--dart-define`,安装后也可在 debug 设置中保存真实服务器 地址,重启后覆盖现有 `.local` 默认值。 ### 29.7 Git ```text 907562a feat(flutter): 增加 debug 运行时服务器地址设置 f95325d merge: 合并 K13 开发服务器地址设置 b762289..f95325d develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 29.8 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 安装本次不带 `--dart-define` 构建的 debug APK。 预期:App 可以启动,进入「我的 → 设置」能看到「服务器地址」。 2. 点击「服务器地址」。 预期:编辑框预填当前地址,并显示「重启 App 生效」。 3. 输入真实局域网地址,例如 `http://192.168.31.187:8081`,保存。 预期:返回设置页并提示重启;输入框不会保留非法格式。 4. 完全结束 App 后重新打开,确认首页/发现请求成功,并在后端日志看到来自手机 的 `/app/home/feed` 等请求。 预期:请求使用新地址,不再访问 `.local` 默认地址。 5. 再次进入设置,点击「恢复默认」并重启。 预期:本地覆盖被清除,回到编译配置地址。 6. 用 release 构建检查。 预期:设置中完全没有服务器地址入口,且即使本地存有 key 也不会读取。 设备侧日志、截图与最终连通性结论由用户执行。 --- ## 2026-10-17 K14 客户端文档 ### 30.1 范围与依据 叙述:重写客户端 README,新增任务清单和文件说明。只改 ReelShort 的三份文档, 不改代码、specs、后端或 Web。 核对依据: - `pubspec.yaml`、`pubspec.lock` - `lib/` 与 `test/` 实际目录 - `lib/core/data/remote/**` 实际调用 - `FruitServer/docs/api-phase1.md` 章节 - `FruitServer/docs/plans/PROGRESS.md` 已完成批次 客户端分支:`docs/k14-client-docs`,基线 `f95325d`。 ### 30.2 接口与目录核对 Flutter 版本: ```text $ flutter --version Flutter 3.47.7 • channel stable Tools • Dart 3.13.5 • DevTools 2.60.0 ``` 接口路径: ```text $ rg -o "'/app/[^']+'" lib/core/data/remote -g '*.dart' | sort -u | wc -l 26 ``` 按 HTTP 方法和重复 URL 逐项统计为 31 个方法级调用。README 已按账号、内容、 观看记录、收藏、下载、互动和公告分组列出。 ### 30.3 文档内容 `ReelShort/README.md`: - 印尼市场定位、技术栈、目录概览; - 实际接口清单; - Android USB、Android 模拟器、iOS 和 debug 运行时地址; - iOS foundation 2.5.6、SPM 恢复、Android libdartjni 处理; - 门禁命令、测试命名和当前边界。 `ReelShort/docs/TASKS.md`: - K1-K13 批次表; - K4、支付/金币、搜索、下载落盘待办; - 未确认范围标「需核对」,未编造提交号。 `ReelShort/docs/FILES.md`: - 目录树、关键文件职责、test 结构; - `app_localizations*.dart`、`*.g.dart`、build、ephemeral 等生成物标注为勿手改。 仓库 `.gitignore` 包含 `/docs/`。本轮只对新增的两个 docs 文件使用 `git add -f`,没有修改 `.gitignore`。 ### 30.4 门禁 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:20 +309 ~1: All tests passed! ``` 解读:文档改动未误改代码;309 项通过,1 项联调测试因服务未启动跳过。 ### 30.5 Git ```text 5a28845 docs(client): 重写 README 并补充任务与文件说明 213c86e docs(client): 修正已接接口数量说明 9fe62d1 merge: 合并 K14 文档接口统计修正 f95325d..9fe62d1 develop -> develop ``` 主提交、修正提交和最终合并的 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 30.6 文档验收 1. 打开 `ReelShort/README.md`,按表中命令可完成 Android USB、iOS 和 debug 运行时地址配置。 2. 打开 `ReelShort/docs/TASKS.md`,能区分已合入功能与待办/需核对事项。 3. 打开 `ReelShort/docs/FILES.md`,能定位核心配置、数据层、页面、测试和生成物。 4. 文档中的接口路径与 `lib/core/data/remote/` 一致。 --- ## 2026-10-17 K15 首页服务器设置与真机默认地址 ### 31.1 最终范围 用户最终确认手机与 Mac 在同一网络,不使用 USB 转发。修复范围确定为: 1. 真机默认地址改为 `http://10.157.242.1:8081`,只替换 `devHostBaseUrl`; 2. 平台默认选择与优先级保持不变:运行时设置 > dart-define > 平台默认; 3. 首页加载失败页 debug 下增加「服务器设置」,复用 K13 地址弹窗; 4. release 不显示入口。 客户端修复分支:`fix/home-server-default`,基线 `9fe62d1`。 ### 31.2 地址与失败页实现 `AppConfig`: ```text devHostBaseUrl = "http://10.157.242.1:8081" ``` 注释已说明该 IP 跟随开发机网络变化,换网络时只改这一行。模拟器、桌面端和 iOS 模拟器仍使用 `127.0.0.1`;iOS/Android 真机使用该局域网地址。 K13 的服务器地址弹窗抽到: ```text lib/core/widgets/server_endpoint_dialog.dart ``` 我的页和首页错误页共用同一弹窗。首页错误页新增 `home-server-settings` 按钮,仅调试工具开启时显示;可编辑、保存、恢复默认, 保存后提示重启生效。 ### 31.3 测试与门禁 新增/更新测试: - `AppConfig.devHostBaseUrl` 为最终局域网地址; - 原有运行时 > dart-define > 默认优先级继续通过; - 首页错误页 debug 下显示按钮并可打开 `server-endpoint-input`; - 注入 release 条件时按钮不显示; - 我的页设置入口与非法地址拒绝继续通过。 ```text $ flutter test test/config/server_endpoint_test.dart test/mine/server_endpoint_setting_test.dart test/home/home_server_setting_test.dart 00:02 +10: All tests passed! ``` ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:21 +312 ~1: All tests passed! ``` ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 13.2s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 解读:312 项通过,1 项联调测试因服务未启动跳过;debug APK 构建成功。 ### 31.4 Git ```text c4694c6 fix(flutter): 增加首页服务器设置并更新真机默认地址 4778ce2 merge: 合并 K15 首页服务器设置修复 9fe62d1..4778ce2 develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 31.5 用户真机验证步骤 1. Android Studio 直接 Run,不额外传 `--dart-define`。 预期:默认请求 `http://10.157.242.1:8081`。 2. 若首页仍加载失败,在错误页查看 debug 地址和「服务器设置」按钮。 预期:点击后打开与「我的 → 设置」相同的地址弹窗。 3. 修改地址并保存,完全结束 App 后重开。 预期:使用新地址请求,后端日志出现手机侧请求。 4. release 构建中检查首页错误页。 预期:不显示服务器设置入口。 设备侧日志、截图和最终网络连通性结论由用户执行。 --- ## 2026-10-17 K16 注册限流后台配置化 ### 32.1 最终口径 叙述:K16 开工后 Trae 于 `461fb8e` 更新任务书,要求从 `sys_config` 读取配置, 不使用 `application.yml` 固化。初版 `@Value` 实现已按最新口径修正。 最终配置键: ```text app.register.max-per-ip-per-day app.register.max-per-device-per-day ``` - 值 <= 0:对应维度不限制; - 键缺失或非整数:回退 IP 5 / 设备 3; - 错误码和错误文案不变。 ### 32.2 后端实现与测试 `AppAuthService` 注入 `ConfigService`,module-user 增加 module-system 依赖。 首次红灯: ```text Could not find field 'maxRegisterPerIpPerDay' Tests run: 16, Failures: 0, Errors: 4 ``` 最终定向测试: ```text $ mvn -pl fruitshorts-app-api -am -Dtest=AppAuthServiceTest -Dsurefire.failIfNoSpecifiedTests=false test Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` 覆盖:IP 超限、设备超限、自定义阈值、IP=0、设备=0、缺省回退、非数字回退。 全量测试: ```text $ mvn test [INFO] BUILD SUCCESS ``` ### 32.3 种子配置与后台页面 `sql/data-phase1.sql` 增加幂等种子: ```sql INSERT INTO sys_config (id, config_key, config_value, remark) VALUES (3, 'app.register.max-per-ip-per-day', '0', '0=不限制;生产建议 5'), (4, 'app.register.max-per-device-per-day', '0', '0=不限制;生产建议 3') ON CONFLICT (config_key) DO NOTHING; ``` 当前开发库执行结果: ```text INSERT 0 2 id | config_key | config_value | remark ----+-------------------------------------+--------------+-------------------- 3 | app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5 4 | app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3 ``` 后台 `SettingsView.vue` 原有页面只显示两个旧配置字段,因此补充了两个数值项, 沿用现有 `/admin/configs` 接口,不新建页面。 ```text $ pnpm build:pro Build successful. Please see dist-pro directory ``` Web 提交 `54db082`,合并 `5c087f4`,已推送。 ### 32.4 打包、重启与接口自检 ```text $ mvn -pl fruitshorts-app-api -am package -DskipTests [INFO] BUILD SUCCESS ``` 旧 8081 服务停止后,通过 `launchctl` 启动新 jar。当前服务: ```text 80642 0 com.fruitshorts.app-api java 80642 ... TCP *:8081 (LISTEN) ``` 健康检查: ```text HTTP/1.1 200 {"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api",...}} ``` 同一新设备号连续注册 4 次(令牌字段已省略): ```text attempt 1 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"U-4JA-DDD"} attempt 2 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UTIOUCLLV"} attempt 3 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UR6ZJRO74"} attempt 4 HTTP 200 {"code":0,"message":"ok","newUser":true,"uid":"UXJF6MECJ"} ``` 解读:开发库两键均为 0,IP 与设备 guard 均跳过,连续注册不再 429。 ### 32.5 Git ```text FruitServer: 3403a52 docs(plans): 增加 K16 注册限流配置化计划 758eb69 feat(backend): 注册限流阈值改为可配置 f1aaaee fix(backend): 改为后台配置注册限流阈值 f4003ed test(backend): 覆盖注册限流缺省回退 5b7be2e merge: 合并注册限流缺省回退测试 78fb995..5b7be2e develop -> develop ReelShortWeb: 54db082 feat(admin): 增加注册限流配置项 5c087f4 chore(admin): 合并注册限流配置项 3ec9ea6..5c087f4 develop -> develop ``` 最终后端修正与 Web 提交的 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 32.6 用户验收步骤 1. 登录后台「系统设置」。 预期:看到注册频控两个数值项,当前均为 0。 2. 将单设备上限改为 3,保存后使用同一设备注册第 4 次。 预期:返回 429;恢复 0 后第 4 次可成功。 3. 将单 IP 上限改为 1,保存后从同一 IP 注册第 2 次。 预期:返回 429;恢复 0 后恢复不限。 4. 验证完成后开发库恢复 0/0;生产配置建议恢复 IP 5 / 设备 3。 设备侧接口结果与后台截图由用户最终验收。 ### 32.7 API 文档键名补正 复核 `docs/api-phase1.md` §1.1 时发现文档仍沿用旧的 `fruitshorts.auth.register-max-*` 键名,与 `AppAuthService` 实际读取的 `sys_config` 键不一致。已补正为: ```text app.register.max-per-ip-per-day app.register.max-per-device-per-day ``` 同时将入口表述统一为后台「系统配置」。本次只补正文档,不改运行逻辑。 补正后复核: ```text $ mvn test AppAuthServiceTest: Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` 开发库两键均为 `0`。同一新设备号连续注册 4 次均为 `code=0`: ```text uid=UFCIW1LWU uid=UP2BH-DN_ uid=ULOFAAU2Z uid=UDAYJR-_E ``` --- ## 2026-10-17 K16 返工:sys_config 优先 + @Value 兜底 ### 33.1 返工范围 K16 首版合并后,用户要求明确区分后台运行配置与 yml fallback: - `AppAuthService` 运行时优先读取 `sys_config`: `app.register.max-per-ip-per-day`、`app.register.max-per-device-per-day`; - 键缺失或非整数时,回退 `@Value` 注入的 `fruitshorts.auth.register-max-*`; - yml 中的值只作 fallback,默认 5 / 3;≤0 仍表示不限制。 返工分支:`fix/k16-register-config-fallback`。 ### 33.2 TDD 与测试 先把非数字配置用例改为验证 @Value fallback=2,旧实现红灯: ```text Could not find field 'maxRegisterPerIpPerDay' Tests run: 18, Failures: 0, Errors: 1, Skipped: 0 BUILD FAILURE ``` 增加注入字段并接入 registerLimit 后,定向测试转绿: ```text RUNNING AppAuthServiceTest Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` 全量测试: ```text $ mvn test [INFO] fruitshorts-module-user ............................ SUCCESS [INFO] fruitshorts-app-api ............................... SUCCESS [INFO] BUILD SUCCESS ``` 代码提交: ```text 35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底 ``` ### 33.3 数据库与后台接口 重新执行 `sql/data-phase1.sql` 中的幂等插入: ```text INSERT 0 0 ``` 查询结果: ```text config_key | config_value | remark app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3 app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5 (2 rows) ``` 使用本机开发管理员登录后,`GET /admin/configs` 返回: ```json { "code": 0, "message": "ok", "configs": [ { "key": "app.register.max-per-device-per-day", "value": "0", "remark": "0=不限制;生产建议 3" }, { "key": "app.register.max-per-ip-per-day", "value": "0", "remark": "0=不限制;生产建议 5" } ] } ``` 现有 `SettingsView.vue` 已使用 `listConfigsApi` / `updateConfigsApi`, 页面上的两个数值输入项可直接读写这两键。 ### 33.4 阈值 2 往返验证 通过后台 `PUT /admin/configs` 把 IP 上限临时设为 2、设备上限设为 0, 使用同一条测试 IP 连续注册: ```text attempt 1 HTTP 200 code=0 message=ok attempt 2 HTTP 200 code=0 message=ok attempt 3 HTTP 200 code=429 message=当前网络注册过于频繁,请稍后再试 ``` 随后通过同一后台接口恢复 IP=0,再注册一次: ```text attempt 4 HTTP 200 code=0 message=ok ``` 说明:当前统一响应包装对外使用 HTTP 200,业务码在 `code` 字段返回 429。 ### 33.5 打包与 8081 ```text $ mvn -pl fruitshorts-app-api -am package -DskipTests [INFO] BUILD SUCCESS ``` 通过 `launchctl kickstart -k gui/501/com.fruitshorts.app-api` 重启: ```text before PID: 80642 after PID: 94657 java 94657 ... TCP *:8081 (LISTEN) ``` 健康检查: ```text {"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api","profiles":"","time":"2026-10-17T23:38:32.363463"}} HTTP 200 ``` --- ## 2026-10-17 iOS 构建阻塞:固定 shared_preferences_foundation 2.5.6 ### 25.1 现象与根因 用户从 Xcode 16.2 构建 iOS 时,Swift 6.0.3 在 `shared_preferences_foundation 2.6.0` 生成文件中报: ```text Unexpected , separator messages.g.swift:33:2 ``` 根因是 2.6.0 的函数参数列表使用 trailing comma,需要 Swift 6.1。此前可构建 时锁文件为 foundation 2.5.7;`a4415c8` 同步依赖锁后升到了 2.6.0。 用户随后修正固定目标:最终使用 2.5.6,不使用 2.5.4;2.5.7 的本地缓存缺 `PrivacyInfo.xcprivacy`,也不使用。 ### 25.2 修复 客户端从 `develop a4415c8` 创建独立分支: ```text fix/ios-shared-preferences-foundation ``` `pubspec.yaml`: ```yaml dependency_overrides: shared_preferences_foundation: 2.5.6 ``` 执行: ```text $ flutter pub get Resolving dependencies... Downloading packages... ! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available) Got dependencies! ``` 锁文件: ```text $ rg -n -A 8 '^ shared_preferences_foundation:' pubspec.lock 407: shared_preferences_foundation: 408- dependency: "direct overridden" 409- description: 410- name: shared_preferences_foundation 411- sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" 412- url: "https://pub.flutter-io.cn" 413- source: hosted 414- version: "2.5.6" ``` 确认缓存包完整: ```text $ find "$HOME/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6" -name PrivacyInfo.xcprivacy -print /Users/jianghaifeng/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/Resources/PrivacyInfo.xcprivacy ``` ### 25.3 依赖树原始输出 ```text $ flutter pub deps --style=compact | grep shared_preferences_foundation - shared_preferences 2.5.6 [flutter shared_preferences_android shared_preferences_foundation shared_preferences_linux shared_preferences_platform_interface shared_preferences_web shared_preferences_windows] - shared_preferences_foundation 2.5.6 [flutter shared_preferences_platform_interface] ``` 解读:`shared_preferences 2.5.6` 与 override 后的 `shared_preferences_foundation 2.5.6` 兼容,无需降级主插件。 ### 25.4 iOS 编译验证 为排除先前版本的构建缓存,先清理再重新解析: ```text $ flutter clean Cleaning Xcode workspace... 7.2s Deleting build... 482ms Deleting .dart_tool... 24ms Deleting ephemeral... 2ms Deleting Generated.xcconfig... 0ms Deleting flutter_export_environment.sh... 0ms Deleting .flutter-plugins-dependencies... 0ms ``` ```text $ flutter pub get ! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available) Got dependencies! ``` 完整无签名设备构建: ```text $ flutter build ios --debug --no-codesign Warning: Building for device with codesigning disabled. You will have to manually codesign before deploying to device. Building com.stormg.lumadramv for device (ios)... Running Xcode build... Xcode build done. 22.1s ✓ Built build/ios/iphoneos/Runner.app ``` 解读:Swift Compiler 编译通过,不再出现 `Unexpected , separator`。 本轮未运行真机,也未执行签名安装。 ### 25.5 Git ```text 2c30f83 chore(ios): 固定 shared_preferences_foundation 2.5.6 19babb8 merge: 合并 iOS shared_preferences 编译修复 a4415c8..19babb8 develop -> develop ``` 该 chore 提交只包含 `pubspec.yaml` 与 `pubspec.lock`,K9 修改未混入; author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 25.6 用户后续验证 1. 拉取最新客户端 `develop` 并执行 `flutter pub get`。 预期:终端显示 `shared_preferences_foundation 2.5.6 (overridden)`。 2. 从 Xcode 16.2 直接 Run 或执行 `flutter run` 到 iPhone。 预期:Swift Compiler 不再报 `Unexpected , separator`,App 可完成构建启动。 3. 打开设置或任何依赖 `shared_preferences` 的页面,修改语言或登录态。 预期:偏好设置读写正常。 设备侧构建日志、启动截图与偏好读写结果由用户执行。 --- ## 2026-10-17 K8 登录页视觉重设计 ### 23.1 范围、分支与计划 叙述:按 INBOX K8 条目重设计登录页视觉,不改 AuthLogic、校验、AuthController、 接口、登录/注册流程或其他页面。客户端从 `develop 6cc5bfc` 创建 `feature/flutter-k8-auth-visual`;FruitServer 从 K8 任务书提交 `c33e6cd` 创建 `docs/k8-auth-visual`,只改 `docs/plans/`。 开工前先写并提交计划: ```text e7c11c3 docs(plans): 增加 K8 登录页视觉实施计划 ``` 实施文件: - `lib/pages/auth/auth_view.dart` - `lib/pages/auth/auth_widgets.dart` - `lib/pages/auth/auth_form_widgets.dart` - `test/auth/auth_visual_test.dart` `auth_view.dart` 初次实现达到 654 行,触发项目单文件 500 行红线。随后按页面 组合、卡片/分段控件、表单控件与品牌组件拆分,最终行数: ```text 259 lib/pages/auth/auth_view.dart 183 lib/pages/auth/auth_widgets.dart 363 lib/pages/auth/auth_form_widgets.dart 311 test/auth/auth_visual_test.dart ``` 解读:所有新增 Dart 文件均低于 500 行,页面状态类低于 300 行。 ### 23.2 先红:视觉测试 叙述:先新增 `test/auth/auth_visual_test.dart`,未改既有 `test/auth/auth_page_test.dart`。 ```text $ flutter test test/auth/auth_visual_test.dart Expected: exactly one matching candidate Actual: Found 0 widgets with key [<'auth-brand'>]: [] Bad state: No element The finder "Found 0 widgets with key [<'auth-mode-sign-up'>]: []" could not find any matching widgets. Expected: exactly one matching candidate Actual: Found 0 widgets with key [<'auth-error-capsule'>]: [] 00:01 +2 -4: Some tests failed. ``` 解读:品牌区、220ms 入场动画、pill 分段控件和错误胶囊尚不存在,红灯与 K8 新增视觉范围一致;既有功能型交互仍可运行。 ### 23.3 绿灯:视觉与交互实现 实现后的新增与既有 auth 用例: ```text $ flutter test test/auth/auth_visual_test.dart test/auth/auth_page_test.dart 00:01 +15: All tests passed! ``` 覆盖内容: - 深色品牌页、`AppColors.ink`、顶部克莱因蓝渐变透明度不超过 0.18。 - 48px 蓝色圆角品牌方块、App 名和印尼市场 tagline。 - `AppColors.card`、20px 圆角、1px `outlineVariant` 边框、20px 内边距。 - 52px 输入框、14px 圆角、`AppColors.bar` 填充、邮箱/锁前缀图标和聚焦 1.5px 主色边框。 - 52px 主按钮、26px 圆角、`AppColors.blue`/`bluePressed`、16/w700 文字与 18px loading。 - pill 登录/注册分段控件、选中主色底、切换调用现有 `toggleMode()`。 - 220ms 淡入上滑、错误浅红胶囊、44pt 以上点击区域。 - 邮箱→密码焦点流转、密码显隐、busy 禁用、Haptics 和 autofill 均保留。 既有 `auth-switch-mode` Key 与旧测试所需文本兼容入口保留,但用户可见切换控件 已改为 pill 分段;旧用例没有删改。 ### 23.4 全量门禁与构建 构建前检查并发进程: ```text $ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew' ``` 输出仅有当前检查命令自身,没有 Flutter run/build 进程后开始门禁。 ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:19 +292 ~1: All tests passed! ``` 解读:292 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的 `[WatchRecordsController]` 堆栈来自既有故意注入失败的测试。 ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 38.2s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 门禁执行期间 Flutter 工具自动改写 `pubspec.lock` 的包镜像和新版 SDK 传递依赖; 该文件不属于 K8 范围,已单独还原,未进入提交。 ### 23.5 Git ```text 45c3c64 feat(flutter): 重设计登录页视觉 99f64ab merge: 合并 K8 登录页视觉重设计 6cc5bfc..99f64ab develop -> develop ``` 修复提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 23.6 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 拉取最新客户端 `develop`,冷启动并进入登录页。 预期:全屏深色背景,顶部只有克制的蓝色光晕,品牌方块、App 名和印尼 tagline 清晰可见。 2. 观察表单卡片和输入框。 预期:卡片为深色圆角面板;输入框高 52、圆角 14,邮箱和锁图标存在。 3. 点击邮箱输入框并切换到密码框。 预期:聚焦输入框出现 1.5px 克莱因蓝边框,图标高亮;键盘 Next 会跳到密码, Done 会提交。 4. 点击登录/注册 pill 两段。 预期:选中段蓝底白字,标题、副标题和按钮文案同步切换。 5. 点击密码右侧眼睛按钮。 预期:密码明文/密文切换,按钮点击区域不小于 44pt。 6. 输入错误邮箱或不安全密码并提交。 预期:卡片内出现浅红胶囊错误提示,文案与当前语言一致。 7. 在 iPhone SE 尺寸打开键盘并滚动。 预期:无横向溢出,提交按钮可滚动到键盘上方并点击。 8. 登录/注册提交时观察按钮与触觉。 预期:按钮显示 18px 白色 loading 环,输入与分段切换禁用;设备支持触觉时 有一次 light impact。 设备侧截图、日志与最终视觉验收结论由用户执行。 --- ## 2026-10-17 K9 首页 2 列全部剧目 ### 24.1 范围、分支与计划 叙述:按 INBOX K9 条目移除首页「继续观看」,在分类 chips 下方展示当前分类的 全部剧目。只改发现页、三语 l10n、首页相关测试与 `docs/plans/`;不改接口、 数据模型、轮播、通知或其他页面。 - 客户端:从 `develop 99f64ab` 创建 `feature/flutter-k9-home-grid`。 - 文档:从 K9 任务书 `b434f65` 创建 `docs/k9-home-grid`。 开工前先写并提交计划: ```text 496d273 docs(plans): 增加 K9 首页剧目网格实施计划 ``` ### 24.2 先红:首页网格测试 叙述:先新增 `test/discover/home_all_dramas_grid_test.dart`,未改实现与既有测试。 ```text $ flutter test test/discover/home_all_dramas_grid_test.dart Expected: no matching candidates Actual: Found 1 widget with text "Continue Watching" Found 0 widgets with key [<'home-drama-card-city-lights-demo'>]: [] Found 0 widgets with text "All Dramas": [] 00:02 +0 -5: Some tests failed. ``` 解读:继续观看仍存在,「全部剧目」标题、2 列网格和卡片 Key 尚不存在,红灯与 K9 范围一致。 ### 24.3 实现与定向绿灯 `discover_view.dart` 改为外层 `CustomScrollView`,首页 sliver 顺序为: 轮播、分类与标题、剧目网格、底部留白。网格代码拆到: - `lib/pages/discover/widgets/home_drama_grid.dart` 关键参数: ```text crossAxisCount: 2 mainAxisSpacing: 16 crossAxisSpacing: 12 childAspectRatio: 2 / 3 horizontal padding: 16 ``` 卡片使用 `RemoteImage`、14px 圆角、底部深色渐变;标题 2 行、类型 1 行。 点击直接调用现有 `logic.openCard(...)`。分类数据直接使用 `CategoryController.visibleDramas`。 新增三语 `allDramas` 并执行 `flutter gen-l10n`: ```text $ rg -n "allDramas" lib/l10n lib/l10n/app_localizations_en.dart:161: String get allDramas => 'All Dramas'; lib/l10n/app_zh.arb:58: "allDramas": "全部剧目", lib/l10n/app_en.arb:58: "allDramas": "All Dramas", lib/l10n/app_localizations_zh.dart:158: String get allDramas => '全部剧目'; lib/l10n/app_id.arb:58: "allDramas": "Semua Drama", lib/l10n/app_localizations_id.dart:161: String get allDramas => 'Semua Drama'; ``` 定向绿灯: ```text $ flutter test test/discover/home_all_dramas_grid_test.dart 00:01 +5: All tests passed! ``` 覆盖:无继续观看、全部剧目标题、2 列 delegate、左右/行列间距、2:3 封面、 分类切换、无剧目空态、分类缺失空态、失败重试与 320px 小屏。 文件规模: ```text 487 lib/pages/discover/discover_view.dart 167 lib/pages/discover/widgets/home_drama_grid.dart 245 test/discover/home_all_dramas_grid_test.dart ``` 解读:改造前 `discover_view.dart` 为 649 行;删除继续观看旧实现并拆分网格后 降到 500 行以内。首页源码已无 `continueWatching`、`_continueTile`、 `_ShowcaseCard`、`_cardsFor` 或嵌套 `GridView`。 ### 24.4 既有壳层断言更新 `test/widget_test.dart` 原有 5 处断言首页出现 `Continue Watching`。K9 明确要求 该文案彻底消失,因此该断言与 `find.text('Continue Watching'), findsNothing` 不可能同时成立。 处理:保留全部既有用例、测试名称和导航流程,仅把 5 处已作废的可见文案断言 原位改为 `All Dramas`;没有删除测试用例。 ```text $ flutter test test/widget_test.dart 00:05 +17: All tests passed! ``` ### 24.5 全量门禁与构建 首次静态分析发现新增测试有一个未使用 import: ```text warning - test/discover/home_all_dramas_grid_test.dart:10:8 Unused import: 'package:fruitshorts/core/data/repositories/catalog_repository.dart'. 1 issue found. ``` 移除后: ```text $ dart analyze lib test Analyzing lib, test... No issues found! ``` ```text $ flutter test 00:18 +297 ~1: All tests passed! ``` 解读:297 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的 `[WatchRecordsController]` 堆栈来自既有故意注入失败的测试。 构建前检查: ```text $ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew' ``` 输出仅有当前检查命令自身,没有并发 Flutter run/build 后执行: ```text $ flutter build apk --debug Running Gradle task 'assembleDebug'... 8.4s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` ### 24.6 Git ```text cf03178 feat(flutter): 首页改为两列全部剧目网格 2c381be merge: 合并 K9 首页全部剧目网格 99f64ab..2c381be develop -> develop ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 24.7 用户真机验证步骤与预期 以下设备侧取证由用户执行,Codex 未跑真机。 1. 使用最新客户端 `develop` 进入首页「Home/发现」tab。 预期:页面不再出现「继续观看」标题或横向卡片。 2. 向下查看分类 chips。 预期:chips 下方显示当前语言对应的「全部剧目」/ `All Dramas` / `Semua Drama`。 3. 观察列表布局。 预期:一行 2 个剧目,左右各 16,列间距 12、行间距 16;封面为 2:3 圆角。 4. 向下滚动长列表。 预期:继续按 2 列懒加载,滚动中无整页白屏或卡顿。 5. 依次点击「为你推荐 / 热门 / 最新 / 服务端分类」。 预期:网格内容随分类更新;「为你推荐」显示去重后的全部剧目。 6. 点击任意剧目卡片。 预期:沿用原详情流程打开对应剧目。 7. 在有长标题或空分类数据时检查卡片。 预期:标题最多 2 行、类型最多 1 行,不横向溢出;空态和失败重试文案保留。 设备侧截图、日志与最终验收结论由用户执行。 ## K22 后台管理视觉与交互统一 ### 分支与提交 ```text ReelShortWeb: 5c087f4 chore(admin): 合并注册限流配置项 97fbeda style(web): 统一后台图标与全局视觉 8f3447e chore(web): 合并 K22 后台视觉统一 FruitServer: 5e55171 docs(plans): 增加 K22 后台视觉统一计划 c14b4cb fix(sql): 统一后台菜单图标 be9e381 chore(sql): 合并 K22 菜单图标 ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。未修改 main、客户端业务代码与 Web 接口契约。 ### 图标体系与扫描 新增 `src/constants/icons.ts`、`src/components/AppIcon/`,并将客户端品牌图标 复制为 `src/assets/imgs/brand-mark.png`。 ```text before-old-prefix=22 before-el-icon=15 before-element-plus-icons=16 old-prefix=0 el-icon=0 element-plus-icons=0 AppIcons unique=54 missing=0 ``` 页面与共享组件已不再直接引用旧 Iconify 前缀、`` 或 `@element-plus/icons-vue`。剩余两处 `` 分别是 `AppIcon` 的底层包装与 `Icon` 自身的在线图标实现。 任务书列出的 `language-rounded`、`view-column-rounded`、`lock-rounded`、 `chevron-down-rounded` 在当前 Material Symbols JSON 中不存在,使用同语义且 通过存在性校验的 `translate-rounded`、`combine-columns-rounded`、 `lock-person-rounded`、`keyboard-arrow-down-rounded`。 ### 菜单 SQL 与接口自检 已幂等执行: ```text $ psql -d fruitshorts -v ON_ERROR_STOP=1 -f sql/data.sql INSERT 0 3 INSERT 0 4 INSERT 0 19 INSERT 0 13 ... INSERT 0 4 $ psql -d fruitshorts -v ON_ERROR_STOP=1 -f sql/data-phase1.sql DO INSERT 0 0 ... UPDATE 5 ... INSERT 0 0 ``` 目标菜单回查: ```text id | title | icon ----+------------+--------------------------------------------- 1 | 工作台 | material-symbols:dashboard-rounded 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 (11 rows) ``` 管理员登录后调用 `/admin/auth/menus`: ```text 工作台 material-symbols:dashboard-rounded 内容管理 material-symbols:video-library-rounded 后台管理 material-symbols:settings-rounded 用户管理 material-symbols:group-rounded 创作者管理 material-symbols:movie-edit-rounded 内容治理 material-symbols:policy-rounded 订单与权益 material-symbols:payments-rounded 数据看板 material-symbols:monitoring-rounded 运营管理 material-symbols:campaign-rounded menus=28 icons=9 allMaterial=true ``` 解读:超级管理员可见菜单接口返回的图标全部为 `material-symbols:` 前缀。 `创作者中心`(id 26)按既有规则对超级管理员隐藏,未出现在该菜单树,但数据库 回查已确认其图标正确。 ### 登录页与全局壳 登录页: - 删除 `admin` / `admin123` 占位,改为三语占位文案。 - “记住我”默认关闭,只保存用户名;退出和恢复时清理历史密码字段。 - 支持密码框回车提交;请求与动态路由生成完成前保持 loading,防重复。 - 品牌统一使用客户端 `splash_mark.png`,登录页与侧边栏同一资源。 - ≤960px 压缩品牌区;实测登录卡在 1024×768、800×768、375×812 首屏完整可见。 全局壳: - 深色选中菜单、当前标签、主题开关轨道统一使用主色派生 token; 深色选中白字对比度由 `2.78:1` 提升到 `5.00:1`。 - 面包屑深色对比度 `11.13:1`,顶栏 `16.70:1`。 - 标签关闭图标改用 `var(--el-text-color-secondary)`。 - 删除 `body { min-width: 980px }` 和未使用的旧壳层死选择器。 - 补 1200/768 断点;1024、800、375 下 `documentElement.scrollWidth` 分别等于视口宽度,顶栏无越界元素。 - 用户头像/姓名读取 `userStore`,不再写死 `avatar.jpg` / `Archer`。 - “清除缓存并且重置”弹出二次确认,取消后保持当前状态。 - 字体、圆角、主色变量收敛;`#409eff !important` 已清零。 ### 门禁 ```text $ pnpm lint:eslint > fruitshorts-admin@1.0.0 lint:eslint /Users/jianghaifeng/Desktop/ReelShort/ReelShortWeb > eslint --fix --ext .js,.ts,.vue ./src ``` ```text $ pnpm build:pro > fruitshorts-admin@1.0.0 build:pro /Users/jianghaifeng/Desktop/ReelShort/ReelShortWeb > VITE_CJS_IGNORE_WARNING=1 vite build --mode pro [warn] Circular chunk: element-plus -> vue-chunks -> element-plus. Please adjust the manual chunk logic for these chunks. [warn] node_modules/.pnpm/mockjs@1.1.0/node_modules/mockjs/dist/mock.js (537:22): Use of eval in "node_modules/.pnpm/mockjs@1.1.0/node_modules/mockjs/dist/mock.js" is strongly discouraged as it poses security risks and may cause issues with minification. Build successful. Please see dist-pro directory ``` 解读:ESLint 零错误;构建成功。两条 warning 来自既有 chunk 配置与 mockjs 依赖, 本批未改构建配置。 ### 浏览器自查 Codex 已在本地开发服务完成以下自查,不作为用户验收: - 登录页:三档宽度无横向溢出;800×768 登录卡底边 `754.40`,375×812 底边 `772.77`,均在首屏内。 - 登录交互:占位为“请输入用户名/请输入密码”;“记住我”默认未勾选; 密码框回车可进入工作台。 - 侧边栏:1024 展开 200px、收起 64px;375 抽屉展开 200px、遮罩关闭正常。 - 顶栏、标签页、面包屑、短剧管理工具栏均正常渲染 Material Symbols Rounded。 - 深色/浅色切换正常;深色选中菜单与当前标签均为白字压深主色,对比度 `5.00:1`。 - 清除缓存按钮显示“确认清除本地缓存并重新加载后台吗?”,取消有效。 - 浏览器控制台 0 个 error;动态路由刷新期间出现 2 条既有 `No match found` router warning,随路由生成完成消失,不影响页面。 ### 用户验收步骤与预期 1. 运行 `pnpm dev`,打开 `http://127.0.0.1:3005/#/login`。 预期:暖色品牌登录页;用户名/密码为中文占位;“记住我”默认关闭。 2. 输入管理员账号,在密码框直接回车。 预期:按钮进入 loading,随后只跳转一次工作台。 3. 检查左侧菜单、顶栏、标签页和短剧管理页工具栏。 预期:图标均为 Material Symbols Rounded,无旧图标混用。 4. 展开/收起侧栏,并打开右侧“项目配置”。 预期:品牌 mark 一致;主题开关边界清楚;布局无跳动。 5. 切换到深色再切回浅色。 预期:菜单、面包屑、当前标签、关闭图标与开关均清晰可辨。 6. 分别以 1024、800、375 宽度查看工作台、短剧管理和登录页。 预期:内容区与顶栏不横向裁切;375 可通过菜单按钮打开侧栏抽屉。 7. 点击“清除缓存并且重置”后选择取消。 预期:先出现二次确认,取消后页面状态不变。 最终业务验收由用户在浏览器执行。 ### K22 复核收尾:静态路由图标 将 `src/router/index.ts` 中 5 处静态路由 `meta.icon` 从旧前缀改为 `AppIcons`: ```text 工作台:AppIcons.dashboard 内容管理:AppIcons.content 用户管理:AppIcons.group 后台管理:AppIcons.settings ``` `src/constants/icons.ts` 的历史短名与旧 Iconify 名兜底映射保持不变。 ```text $ rg -n "icon:\s*'(ant-design:|ep:)" src/router/index.ts (无输出) $ pnpm lint:eslint > eslint --fix --ext .js,.ts,.vue ./src $ pnpm build:pro Build successful. Please see dist-pro directory ``` 提交: ```text 29465ab fix(web): 统一静态路由图标 5494190 chore(web): 合并 K22 路由图标收尾 ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`,Web `develop` 已推送。 ## K22.1 后台收尾 ### 登录页与注册清理 登录 hero 保留品牌 mark、kicker 与标题,删除说明段落和三个标签: ```text FruitShorts 内容运营后台 短剧内容管理 登录 使用管理员账号进入工作台 用户名 密码 记住我 忘记密码 登录 ``` 同时删除已无引用的 `login.message/noAccount/pointDrama/pointPublish/pointAccess/ register/checkPassword/otherLogin/hasUser/code/codePlaceholder`、注册表单、注册入口、 `to-register` 事件和注册切换状态。 ### 文件删除证据 删除前在 ReelShortWeb 全仓检索: ```text [src/assets/imgs/logo.png] [src/assets/imgs/avatar.jpg] [public/logo.png] [public/favicon.ico] [CustomMenuView] (以上均无引用) [RegisterForm] src/views/Login/components/index.ts src/views/Login/Login.vue ``` `RegisterForm` 的引用已在同一提交中先移除。ReelShortWeb 全仓及开发库均无 `CustomMenuView` 路由/菜单记录: ```text $ psql -d fruitshorts -c "SELECT id, title, component FROM sys_menu WHERE component ILIKE '%CustomMenu%';" id | title | component ----+-------+----------- (0 rows) ``` 最终删除: - `public/favicon.ico` - `public/logo.png` - `src/assets/imgs/logo.png` - `src/assets/imgs/avatar.jpg` - `src/views/permission/CustomMenuView.vue` - `src/views/Login/components/RegisterForm.vue` - `mock/` ### 品牌与本地图标 - `brand-mark.png` 复制到 `public/brand-mark.png`,favicon 和首屏 loading 均改用它。 - 首屏背景改为 `#100C0A`,标题浅色,loading mark 使用 12px 圆角。 - 删除 `public/favicon.ico`、`public/logo.png`。 - 修正侧边栏品牌图任意值类未生成的问题,展开/收起均为 38px、圆角 12px。 - 在线图标关闭后,UnoCSS 的运行时动态类不会自动收集;已把 `AppIcons` 全量加入 safelist,并显式加载本地 `@iconify/json/json/material-symbols.json`。 品牌文件校验: ```text public/brand-mark.png src/assets/imgs/brand-mark.png 76e01df1fa0ec94ca59842ea9e15f8084ae6532290ee874a88a3c5718159cea7 ``` 两个文件 SHA-256 一致。 ### Mock 与 env ```text .env.base VITE_USE_MOCK=false VITE_USE_ONLINE_ICON=false .env.dev VITE_USE_MOCK=false VITE_USE_ONLINE_ICON=false .env.pro VITE_USE_MOCK=false VITE_USE_ONLINE_ICON=false .env.test VITE_USE_MOCK=false VITE_USE_ONLINE_ICON=false ``` 已删除 `mock/`、`viteMockServe`、`mockPath`、生产 mock 注入和 `getTestRoleApi` 的 `/mock/role/list2` 分支;`tsconfig.json` 不再包含 mock。 `package.json` 移除 `@element-plus/icons-vue`、`@faker-js/faker`、 `vite-plugin-mock` 三个不再直接使用的依赖。 ### 最终扫描 ```text old-prefix=0 el-icon=0 element-plus-icons=0 logo-png=0 mock-build-code=0 mock-directory=absent ``` `src/constants/icons.ts` 的旧名兼容映射按要求保留,未计入 old-prefix。 生产产物: ```text dark-mode-rounded=1 translate-rounded=1 dashboard-rounded=1 video-library-rounded=1 group-rounded=1 settings-rounded=1 dist-pro/brand-mark.png ``` ### 门禁 ```text $ pnpm lint:eslint > fruitshorts-admin@1.0.0 lint:eslint > eslint --fix --ext .js,.ts,.vue ./src ``` ```text $ pnpm build:pro > fruitshorts-admin@1.0.0 build:pro > VITE_CJS_IGNORE_WARNING=1 vite build --mode pro [warn] Circular chunk: element-plus -> vue-chunks -> element-plus. Build successful. Please see dist-pro directory ``` ### 浏览器自查 - 登录 hero 只剩 kicker + 标题,无说明段落和三个标签。 - favicon 为 `/brand-mark.png`;首屏 loading 为 `/brand-mark.png`, 背景 `#100C0A`。 - `/brand-mark.png` 返回 `image/png`;旧 `/logo.png` 不再作为图片存在。 - 登录后菜单、顶栏、短剧管理工具栏全部使用离线 Material Symbols Rounded。 - 短剧管理页可见图标 22 个,空白 0 个;图标 mask 均有效。 - 侧边栏展开/收起品牌图均为 38×38,与登录页一致。 - 浏览器控制台 error 0 条,无 mock 或旧资源错误。 ### 提交 ```text 8aa84a3 docs(plans): 增加 K22.1 后台收尾计划 ada741f style(web): 精简登录品牌与注册入口 8797048 chore(web): 清理 mock 与旧品牌资源 e5c5101 chore(web): 合并 K22.1 后台收尾 ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ## K23 后台视觉深化 ### 主色 ```text --el-color-primary: #c4322a --el-color-primary-light-3: #d3615b --el-color-primary-light-5: #e19995 --el-color-primary-light-7: #f0c2c0 --el-color-primary-light-8: #f5d9d8 --el-color-primary-light-9: #faeeed --el-color-primary-dark-2: #9d2822 ``` 旧用户持久化的 `#409eff` 在 store 恢复时迁移为 `#c4322a`。默认蓝扫描: ```text $ rg '#409eff|#79bbff|#a0cfff|#c6e2ff|#d9ecff|#ecf5ff|#337ecc|rgba?\(64[ ,]+158[ ,]+255' src (无输出) ``` ### 二级菜单图标为空 `data.sql` 与 `data-phase1.sql` 均加入: ```sql UPDATE sys_menu SET icon = '' WHERE type = 'MENU'; ``` ```text type | total | empty_icon | with_icon ---------+-------+------------+----------- CATALOG | 10 | 0 | 10 MENU | 23 | 23 | 0 parents=9 parentMissingIcon=0 children=19 childWithIcon=0 ``` 菜单管理仅在 `type='CATALOG'` 时显示图标选择,保存 MENU 时强制 icon 为空。 渲染层对嵌套项强制不输出 AppIcon,历史脏值也不保留图标空位。 ### 对齐实测 ```text 1024px: parent text left=42px, row=44px; child text left=42px, row=40px, icon=0 375px: parent text left=42px, row=44px; child text left=42px, row=40px, icon=0 popper: child row=40px, icon=0 ``` 浅色选中为浅主色底 + 主色文字;暗色选中为深主色底 + 白字, 暗色实测对比度 8.73:1。 ### 短剧编辑弹窗 - 宽 680px,三区块,内容滚动、底部固定;375px 下宽 345px 且无横向溢出。 - 数字输入固定 180px,控件位于右侧;1024/375 实测均为 180px。 - 封面 96×128 预览、basename、状态 chip、更换/移除、查看原图、进度遮罩和错误提示。 - 删除原始 URL 输入框。 - txt 实测提示“封面仅支持 JPG、PNG 或 WebP 格式”;限制 5MB。 - 空名称保存显示字段错误“请输入短剧名称”;类型必选。 - 保存/上传中按钮禁用,关闭被拦截。 ### 锁屏弹窗 - 宽 440px,高度 360px;375px 下宽 359px。 - 头部底边线 0px,关闭图标距右边缘 22px。 - 增加“输入密码解锁当前会话”;密码框回车可锁定,取消可关闭。 - 取消/锁定按钮均为 96×36px。 - 删除 `console.log('set: ', val)`。 ### 门禁 ```text $ pnpm lint:eslint > eslint --fix --ext .js,.ts,.vue ./src $ pnpm build:pro [warn] Circular chunk: element-plus -> vue-chunks -> element-plus. Build successful. Please see dist-pro directory ``` 浏览器自查:1024/375、明暗主题通过;控制台 error 0;无资源 404。 ### 提交 ```text 5356f24 docs(plans): 增加 K23 后台视觉深化计划 91bbfd5 docs(plans): 更正 K23 子菜单图标口径 ba90bf9 style(web): 统一辣椒红主色与菜单层级 805d7dd feat(web): 重做短剧编辑弹窗 898e439 fix(web): 优化锁屏弹窗布局与交互 ee3bb1c chore(web): 合并 K23 后台视觉深化 b400e36 fix(sql): 清空二级菜单图标 ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### 用户浏览器验收 1. 登录后检查按钮、链接、焦点环、标签和选中菜单:均应为辣椒红,无 Element 蓝。 2. 展开内容管理/后台管理:一级项有图标,二级项只有文字,父子文字左边缘同线。 3. 收起侧栏打开 popper:二级项无图标、行高 40px。 4. 编辑短剧:检查三区块、固定数字框、封面预览与“查看原图”,不再出现原始 URL 输入。 5. 上传非法格式或超过 5MB 图片:弹窗内显示错误且原图不变。 6. 打开锁屏:标题/说明清晰,可取消,回车可锁定。 7. 切换明暗主题并缩至 375px:布局不溢出,暗色选中态白字清晰。 - 2026-10-19 K23 菜单对齐补丁:一级 padding-left=16px、左侧 margin=0,二级 margin-left=42px;1024 实测父子文字 left 均为 42px,Web 提交 `40fbd6e`,合并 `fe1ae95`,`pnpm build:pro` 通过。 ## K23.1 后台修正 ### 品牌文字 `app.ts` 浅色分支改为: ```text logoTitleTextColor = #303133 topHeaderTextColor = #303133 ``` 持久化旧主题在恢复时重新执行菜单/顶栏主题计算。浏览器实测: ```text FruitShorts logo text = rgb(48, 49, 51) Top header text = rgb(48, 49, 51) Breadcrumb text = rgb(96, 98, 102) ``` 浅色侧边栏不再出现蓝色品牌字。 ### 删除按钮 覆盖并统一为 `type="danger"` 实心红底白字: - 短剧、剧集、轮播图、类型、公告、商品、评论、敏感词 - 管理员、角色、角色分类、菜单 - 创作者分集删除、撤回、冻结 - 订单退款、权益回收 - 短剧封面“移除” 表格内按钮均为 `size="small"`;`danger-action`、danger link、danger plain 扫描结果均为 0。删除确认弹窗带 `confirmButtonClass: 'el-button--danger'`。 浏览器实测危险按钮: ```text background = rgb(245, 108, 108) color = rgb(255, 255, 255) height = 28px(表格按钮) white-space = nowrap ``` 删除确认按钮同样为 `rgb(245,108,108)` 背景、白字。 ### 布局回归 ```text 1440×900: 品牌图/文字中心线均为 25px logo text color = rgb(48,49,51) document scrollWidth = clientWidth = 1440 1024×768: 菜单父文字 left=42px,父行高 44px 菜单子文字 left=42px,子行高 40px,子图标 0 短剧弹窗宽 680px,内容 scrollWidth=clientWidth 数字框 180px ×2,底部按钮可见 锁屏弹窗 440×360,确认/取消按钮 96×36 375×812: 菜单父/子文字 left 均为 42px 短剧弹窗宽 345px,无横向滚动,底部按钮固定可见 数字框仍为 180px ×2 锁屏弹窗宽 359px,按钮不裁切 表格页: 短剧/剧集/轮播图/类型/公告/商品/治理/角色分类 document scrollWidth = clientWidth = 375 红色删除按钮无换行 ``` ### 门禁 ```text $ pnpm lint:eslint > fruitshorts-admin@1.0.0 lint:eslint > eslint --fix --ext .js,.ts,.vue ./src $ pnpm build:pro [warn] Circular chunk: element-plus -> vue-chunks -> element-plus. Build successful. Please see dist-pro directory ``` 浏览器控制台 error 0,资源 404 0。 ### 提交 ```text 38c8f51 docs(plans): 增加 K23.1 后台修正计划 e77db70 fix(web): 修正浅色品牌文字颜色 f5b399a fix(web): 统一危险操作按钮样式 6f52230 chore(web): 合并 K23.1 后台修正 ``` 功能提交与合并提交 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ### K23.1 布局复核修补 - danger 禁用态:管理员行实测背景 `rgb(245,247,250)`、文字 `rgb(168,171,178)`、`not-allowed`,可点击删除仍为红底白字。 - 锁屏弹窗:卡片实测 `440×440`,footer 底边 `588`、卡片底边 `604`,取消/锁定按钮底边 `569`,均完整在卡片内。 - 封面预览:桌面图片内容区 `94×126`(96×128 卡片内),`object-fit:cover`;375 下卡片恢复 `96×128`,纵向排列且与信息区无重叠,页面无横向溢出。 - 门禁:`pnpm lint:eslint` 零错误,`pnpm build:pro` 成功。 - Web 提交 `c8bf4a8`,合并提交 `73da0bb`。 - 2026-10-19 K23.1 工作台菜单:单子节点渲染合并父子 meta,仅父 icon 兜底;实测工作台文字 left=42px、内容管理文字 left=42px,内容子项仍无图标且 left=42px。Web 提交 `bb03a5a`,合并 `f023c7d`,lint/build 通过。 - 2026-10-19 K23.1 二级菜单背景:inline 与 popper 的 hover/active 背景强制 transparent;浅色文字为主色、深色为白色,一级背景保持不变。浏览器实测浅/深色均为 `rgba(0,0,0,0)`。Web 提交 `f6f4d26`,合并 `9f92d11`。 - 2026-10-19 K23.1 剧集顶部布局:左侧“当前短剧”+剧名+集数,右侧“切换短剧”+320-360px 下拉;1024 左右中心差 0.004px、下拉宽 327.68px,表头完整且 250px 固定操作列可见;375 纵向堆叠、下拉 100%、页面 scrollWidth=clientWidth=375。Web 提交 `122aa21`,合并 `046400a`,lint/build 通过。 - 2026-10-19 K23.1 列表图片预览:新增 `AppImage` 并接入短剧封面、轮播图、App 用户头像、创作者作品封面、分集封面;短剧封面 44×56、轮播图 160×90 尺寸保持不变。浏览器实测短剧/轮播图 viewer 可打开、下一张切换、关闭;375 页面无横向溢出。当前用户数据无头像且管理员会话不可进入创作者中心,后两处完成代码接入但未取得对应业务数据点击证据。Web 提交 `8a84f24`,合并 `5db565b`。 - 2026-10-19 K23.1 轮播图编辑弹窗:680px 三段布局,240×135 图片预览、跳转配置、排序/状态;移除原始 URL 输入,加入图片与条件跳转校验、上传校验与进度遮罩。1024 弹窗宽 680px、正文 scrollWidth=clientWidth、footer 可见;375 弹窗宽 345px、图片区纵向、卡片 240×135、信息区无重叠、页面无横向溢出。Web 提交 `f281dcf`,合并 `685d4fd`,lint/build 通过。 - 2026-10-19 K23.1 剧集编辑弹窗:680px 三段布局,基础信息、视频文件、付费与状态;视频区固定 240×135,移除原始 URL 输入,加入 MP4/MOV、500MB、上线视频必填校验和上传进度。1024 弹窗宽 680px、视频卡 240×135、数字框 180px、footer 可见;375 弹窗宽 345px、视频区纵向且不重叠、页面无横向溢出。格式校验实测显示“视频仅支持 MP4 或 MOV 格式”。Web 提交 `2b5dda7`,合并 `83e535d`,lint/build 通过。 ### K24 客户端 UI 对齐设计稿(2026-10-19) 范围:设计系统、3 Tab 底部导航、首页沉浸层、发现页;详情/播放器/我的/登录留 K25/K26。 设计口径:采用 `README.md` + `screens/01-home.html` + `screens/02-discover.html` 的 `#0B0C10 / #16181F / #1D1E28 / #FF5B3D / #FF4321 / #9278EA`,不使用 `DESIGN.md` 顶部 YAML 的 `#ffb4a5/#cdbdff`。 落地: - `AppColors` 改 Obsidian Flare,保留 `blue/blueSoft/ink/card` 等旧名并新增 `secondary/lavender/onSurface/subtle/success/border`。 - 字体 Outfit(标题)+ Plus Jakarta Sans(正文)共 7 个 OFL TTF,放在 `assets/fonts/`,两份 OFL 许可证随包;`.gitignore` 放行 fonts 目录。 - 新增 `GlassPanel`:`BackdropFilter` 16/24px、白 8-14% 描边、圆角 16/24/26。 - 底部导航 3 Tab:`tab-home/tab-discover/tab-my`,删除 `tab-my-list`;选中橙色图标、 文案与 3px 圆点,背景 `#0B0C10/95` + blur;My List 区块并入 Me 页。 - 首页保留 `PlayerPage(homeMode: true)` 播放链路,新增 `HomeFeedOverlay`:剧名/HOT/ Season/搜索/更多,头像 Follow +、Like/Comment/Bookmark/Share 玻璃钮,底部 26px 玻璃信息卡、题材 chips、集数、双行简介、1.5px 进度、Follow Series/Episodes。 下载与 PiP 收进“更多”底部菜单。 - 发现页移除顶层 3D 轮播;新增品牌行+头像、44px 搜索框(现有过滤与 searchRequest 聚焦)、 分类 chips、观看记录 Continue Watching(无记录隐藏)、4 个快捷入口、2 列 Trending (3/4.2、16px 圆角、HOT/Free 有数据才显示、viewCount 展示)。 数据缺口:`Drama` 无评分、完结、VIP、剧级点赞/评论数;对应 UI 隐藏,不补假数据。 Continue Watching 只有 `episodeId`,能匹配到当前剧集列表时才显示集号。 门禁原始输出: ```text $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:48 +347: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 26.4s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 构建说明:用户当时有 `flutter run --device-id=00008101...` 占用主工作区 `build/`, 因此在 `/tmp/reelshort-k24-final` 的独立 git worktree 执行 `flutter pub get` 与 `flutter build apk --debug`,避免并发构建冲突。 Git: ```text eae1c53 feat(theme): 接入 Obsidian Flare 设计系统 e8c1859 feat(ui): 重做三 Tab 首页与发现页 4553606 merge: K24 客户端 UI 对齐设计稿 ``` 提交与合并 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ReelShort `develop` 与 `feature/flutter-k24-design-system` 已推送。 真机验收步骤(设备侧取证由用户执行): 1. `flutter run` 打开 App,确认默认进入 Home 沉浸 feed;底部仅 Home/Discover/Me 三个 Tab。 2. Home 检查剧名/HOT/Season、右侧头像+Follow、Like/Comment/Bookmark/Share、底部玻璃卡、 集数、简介、橙色进度、Follow Series/Episodes;“更多”内有下载与 PiP。 3. 点 Discover,确认品牌行、搜索框、分类 chips、4 快捷入口、2 列 Trending;输入剧名过滤, 清空恢复;有观看记录时显示 Continue Watching,无记录时不显示。 4. 点 Me,确认账号内容与 History/Watchlist/Liked 在同一页;切回 Home 播放进度与收藏状态保留。 5. 深色/浅色系统、320×568 与 390×844 小屏检查无溢出、文字不截断。 ### K25 客户端详情页 + 播放器(2026-10-19) 范围:详情页与 `homeMode=false` 卡片播放器;`homeMode=true` 首页沉浸 feed 保持 K24 现状。 详情页: - 16:9 hero(当前模型只有 `posterUrl`)、底部渐变、玻璃返回/分享/收藏/更多。 - 集数 pill、24px 标题、Season 副标题、`viewCount`、题材标签;评分/完结/榜单项无字段隐藏。 - 16px 玻璃简介卡,2 行 + More/Less。 - 橙色全宽 Start Watching/继续观看 + 48×48 收藏/下载;举报保留在更多菜单。 - Episodes 标题 + All 范围 chip;46×44 横向集数 chips,锁定项紫色;2 列剧集卡, 390 宽下缩略图实测 96×64,含 Free/Coin、时长、锁态与下载入口。 播放器(从详情进入): - 顶部返回、剧名 + Ep.x、更多(下载/PiP)。 - 9:10 圆角 16 视频框、Auto-Next/1.0x pill、中心橙色播放、右侧 36px 点赞/全屏。 - 1.5px 进度条:白色 30% 缓冲、`#FF5B3D` 播放段、白色 thumb;左中右时间/集数信息,保留拖动 seek。 - Episodes / Comments / Next Ep 三按钮;末集 Next 置灰。 - `PlayerFeaturedComment` 复用 `CommentController`,无评论隐藏;VIP/总数无字段不显示。 - 保留票据三态、自动连播、PiP、下载、点赞/收藏/评论与播放会话协议。 数据缺口:`coverUrl`、评分、完结、Top 排名、Trailer/4K、VIP、剧级点赞/评论总数、场景标签、 集数范围、倍速能力均无模型字段;按 INBOX 隐藏或不伪造。 门禁原始输出: ```text $ dart analyze lib test Analyzing lib, test... No issues found! $ flutter test 00:30 +352: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 26.3s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 构建说明:主工作区仍有用户侧 `flutter run`,因此在 `/tmp/reelshort-k25-final` 独立 worktree 执行 `flutter pub get` 与 `flutter build apk --debug`,避免并发占用 `build/`。 Git: ```text ec163f9 feat(detail): 重做详情页视觉结构 689063a feat(player): 新增卡片式播放器 04fddce merge: K25 客户端详情页与播放器 ``` 提交与合并 author/committer 均为 `Zhou Weidong <502333560@qq.com>`。 ReelShort `develop` 与 `feature/flutter-k25-detail-player` 已推送。 真机验收步骤(设备侧取证由用户执行): 1. 从首页/发现进入详情,检查 16:9 hero、玻璃按钮、集数/播放量/题材、简介展开。 2. 点 Start Watching,确认进入卡片播放器:9:10 视频框、进度条、三按钮行。 3. 拖动进度条、切换下一集、末集按钮置灰;点 Episodes/Comments 打开现有弹窗。 4. 有评论时检查精选评论卡与 View all;无评论时应隐藏。 5. 从播放器更多菜单验证下载/PiP;点全屏进入横屏,退出后回到卡片布局。 6. 检查收藏、举报、续看进度、票据失败重试与自动连播未回退。 ### K25 修复:详情/选集进入恢复全屏(2026-10-19) - `player_view.dart` 移除 `!homeMode && !fullscreen` 的卡片播放器分支,详情 CTA 与集数 chip 进入后统一走原有全屏 `PageView` + `_PlaybackOverlay`,与首页 feed 一致。 - 删除仅卡片模式使用的 `_buildCardPlayer/_buildCardVideo/_cardTopBar/_cardActions/` `_showCardMore/_CardIconButton/_CardActionButton`,并删除 `card_seek_bar.dart`、`player_featured_comment.dart`。 - `player_logic.dart` 删除仅卡片模式使用的 `hasNextEpisode/playNextEpisode`; 票据、进度、PiP、下载、互动、选集、评论与自动连播逻辑保持原样。 - 删除 `player_card_view_test.dart`,新增 `player_fullscreen_test.dart`: 发现卡片进入、详情 CTA、详情集数 chip 三条路径均断言存在 `rail-like/rail-save/` `rail-comments/rail-share`、PiP、Clear screen 与 Slider,且不存在 `card-player-*` Key。 - 清理仅卡片模式使用的 `autoNextOn/viewAll/enterFullscreen` l10n key。 门禁原始输出: ```text $ dart analyze lib test No issues found! $ flutter test 00:28 +351: All tests passed! $ flutter build apk --debug Running Gradle task 'assembleDebug'... 23.7s ✓ Built build/app/outputs/flutter-apk/app-debug.apk ``` 构建仍在 `/tmp/reelshort-player-fullscreen` 独立 worktree 完成,避免与用户侧 `flutter run` 并发占用主 `build/`。 Git: ```text 1c94b72 fix(player): 恢复详情与选集全屏播放 eefabb6 merge: 修复播放器全屏进入方式 ``` 提交与合并 author/committer 均为 `Zhou Weidong <502333560@qq.com>`; `develop` 与 `fix/flutter-player-fullscreen` 已推送。真机验收由用户执行。