109 KiB
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 / <!-- 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<String>
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 回执的「待确认」)
- 雪花 id 的全局方案:admin-api 是否统一
Long → String(涉及所有既有页面的id类型), app-api 是否要动(影响 Flutter 客户端解析)——属于平台级改动,等裁定。 /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<String> 新建返回已回退成自然写法 ——
全局生效后它们只是冗余特例(现在是"所有模块一个口径")。
动作 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.yamldev_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 步只在首次搭建环境时执行;已有数据库可跳过。
- 启动 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 的顺序。
- 构建并启动客户端接口服务 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 已启动,不能证明手机可达。
- 准备手机与局域网。
- 手机和 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"。
- 获取 Flutter 运行设备号。
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ flutter devices
预期:真机以 iPhone (mobile) • <UDID> • 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 的设备号。
- 用当前 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 分类验收
- 打开「发现」页。
预期:先出现分类/分区加载态,随后分类条显示保留的 For You、Hot、New 入口,
再显示后端 /app/categories 返回的分类名称。默认种子数据下包括「都市情感」「重生逆袭」
「甜宠爱情」「悬疑复仇」;数据库改过名称时以实际后端返回名称为准。
- 依次点「都市情感」「甜宠爱情」。
预期:顶部对应分类 chip 高亮;下方只展示 /app/discover/feed 中
code == "category-{id}" 的分区卡片。切换分类时内容跟着切换,不会保留上一个分类的卡片。
- 点默认种子中没有已发布内容的「重生逆袭」或「悬疑复仇」。
预期:分区区域显示「这个分类暂时没有剧集。」。这是无分区空态,不应出现自行拼装的推荐列表、 旧分类残留卡片或网络失败文案。
- 切回有内容的分类。
预期:卡片恢复显示;封面和标题与后端发现 feed 一致;点卡片可进入对应详情。
- 失败与重试:停止 8081,冷启动 App 后打开「发现」页。
预期:分类区域显示「分类加载失败。」和「重试」按钮,不白屏。 重新启动 8081 后点「重试」,预期分类和分区恢复显示;必要时下拉刷新。
设备侧只能直接证明服务端分类名称与分类切换结果;category-{id} 的代码映射、
全量去重和并发/重试细节由 test/category/category_controller_test.dart 自动化覆盖。
解读:K2-A 的用户验收点是“分类条来自服务端、内容只取对应 category-{id} 分区、
缺失分区显示空态、失败可重试”;真机没有执行这些步骤,当前结论仍为未验证。
17.4 K2-B 观看记录验收
- 登录后播放任意一部剧的免费集约 10 秒,返回并进入「我的 → 观看历史」。
预期:页面先显示加载态,随后出现该剧卡片;卡片可见封面、剧名、
「第 N 集」、进度条、续看至 mm:ss / mm:ss 和「更新于 ...」。
即使该剧不在首页/发现已加载目录中,也应正常显示,不能因为目录里匹配不到而消失。
- N+1 合并卡片:对同一部剧先后播放两个不同剧集,再播放另一部剧的一集,重新打开观看历史。
预期:同一部剧只出现一张卡片,不因看过多个剧集而重复;卡片展示该剧最新进度和对应集号。
不同剧各自一张卡片。详情是按 dramaId 去重后补取的,单条卡片加载中不应拖住其他卡片。
- 单条删除:点某张卡片右侧的关闭图标。
预期:卡片立即从页面消失;返回再进入观看历史后仍然不存在。
- 单删失败回滚:保持页面已加载,停止 8081,再点另一张卡片的关闭图标。
预期:卡片先乐观消失,随后回到原位置,并出现「删除记录失败,请重试。」。 重新启动 8081 后,删除动作可再次执行。
- 清空:点页面右上方「清空」。
预期:先弹出「清空观看记录?」及「这会从当前账号移除全部观看记录。」; 点「暂不」列表保持不变;点「清空」后列表变空并显示「还没有观看记录。」。
- 清空失败回滚:有记录且页面已加载时停止 8081,点「清空」并在二次确认中点「清空」。
预期:列表先清空,随后恢复原记录,并出现「清空观看记录失败,请重试。」。
- 三态和失败重试:后端正常时进入页面观察加载态;清空后观察空态;停止 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 部不同的剧,进入「我的 → 我的收藏」。
预期:页面展示服务端 GET /app/favorites 返回的全部收藏卡片,封面、标题与首页卡片一致;
列表不显示分页控件,也不发送自造的 page/size。点击卡片可进入详情。
- 取消收藏:点某张收藏卡片右侧的粉色收藏图标。
预期:卡片立即消失;返回再进入我的收藏后仍然不存在。
- 取消收藏失败回滚:保持列表已加载,停止 8081,再点另一张卡片的粉色图标。
预期:卡片先乐观消失,随后回到原位置,并出现「收藏操作失败,请重试。」。 重新启动 8081 后可再次取消。
-
三态和失败重试:后端正常时进入页面观察加载态;取消全部收藏后观察「还没有收藏。」; 停止 8081 后冷启动 App 再进入我的收藏,观察「收藏列表加载失败。」和「重试」。 重启 8081 后点「重试」,预期列表恢复。
-
服务端状态校准:在另一台设备或另一次登录中取消同一部收藏,再回到当前设备的我的收藏刷新。
预期:当前列表以服务端结果为准,不继续显示已被取消的收藏。
解读:K2-C 的用户验收点是服务端全量收藏列表、取消收藏后的乐观回滚和三态; 未登录引导另见 17.7,设备侧列表与回滚表现仍待用户取证。
17.6 K2-D 下载授权验收
- 登录后进入「我的 → 下载」。
预期:先出现加载态,随后显示策略卡:
「剩余 X / Y 个授权」、「授权有效期 N 天」以及后端返回的 expirePolicyText 原文;
下面显示「仅 Wi-Fi 下载」开关和「最高清晰度」下拉框。
- 验证 §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 |
点「清空」并二次确认后授权列表变空,重新进入仍为空 |
- 申请授权成功:进入任意可下载剧集的详情页或播放页,点下载图标(详情页右上角下载按钮, 或播放页右侧下载入口)。
预期:服务端接受时出现「下载授权已就绪。」;进入我的下载后能看到对应剧名、集号、 授权状态、任务状态、进度百分比、清晰度(后端有值时)和到期时间。
- 授予失败透传后端
message:选择付费/未解锁剧集,或把当前设备配额用满后再次申请。
预期:SnackBar 原样显示后端返回的 message,不能替换成前端自造的“付费/未解锁/配额满”判断。
只有后端没有可用 message 时才回退到「申请下载失败。」。验收时记录手机屏幕上看到的完整原文;
当前环境没有执行这一步,因此具体拒绝文案仍属未验证。
- 仅 Wi-Fi/清晰度成功:切换「仅 Wi-Fi 下载」,再选择
AUTO、720P或1080P。
预期:开关/下拉值立即变化,没有崩溃或重复选项;退出下载页再进入,值从
GET /app/downloads/policy 重新读取且保持。
- 设置失败回滚:保持下载页已加载,停止 8081,再切换仅 Wi-Fi 或清晰度。
预期:值先乐观变化,随后恢复原值,并出现「下载设置更新失败。」。
- 单删和清空。
- 点单条右侧关闭图标:预期该授权立即消失,重新进入后仍不存在。
- 停止 8081 后点另一条关闭图标:预期先消失再恢复,并提示「删除下载授权失败。」。
- 点「清空」:预期先弹出「清空下载授权?」及「这会吊销当前设备的全部下载授权。」; 点「暂不」不变,点「清空」后列表为空并显示「还没有下载授权。」。
- 停止 8081 后清空:预期列表先清空再恢复,并提示「清空下载授权失败。」。
- 状态枚举后端驱动:查看授权卡上「授权:...」「任务:...」后的文本。
预期:显示后端原值,如 ACTIVE/EXPIRED/REVOKED、
QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED,前端不做中文映射或本地推算。
现有数据只可能展示实际出现的状态;其余枚举由下载 API/控制器测试覆盖,
设备侧未逐一构造全部后端状态,回执不能写成“所有状态已真机验证”。
- 三态和失败重试:后端正常时进入观察加载态;清空授权后观察空态; 停止 8081 后冷启动 App 再进入下载页,观察「下载设置加载失败。」和「重试」。 重启 8081 后点「重试」,预期策略卡和授权列表恢复。
本批不落盘、不断点续传、不提供离线播放;即使授权状态为 ACTIVE,也不能据此验收文件已下载。
解读:K2-D 的用户验收点是策略卡、8 个接口的可见结果、后端 message 透传、
仅 Wi-Fi/清晰度、单删/清空以及状态枚举原样展示。URL/进度两个接口没有本批 UI 入口,
只能由自动化测试证明方法契约;真机拒绝文案和全部状态仍未验证。
17.7 未登录 AuthGate 与失败重试
- 在「我的」页退出登录。
预期:账号回到未登录状态。
- 依次进入「我的 → 观看历史」「我的 → 我的收藏」「我的 → 下载」。
预期:三个页面都显示登录引导: 「登录后可继续」、「观看记录、收藏、评论和下载需要登录。」和「去登录」按钮; 不显示网络错误、白屏或空列表冒充无数据。
- 在未登录的详情页点收藏或在详情页/播放页点下载。
预期:出现底部登录提示;点「去登录」进入登录页,点「暂不」关闭提示且不发业务请求。
- 完成登录后返回观看历史、我的收藏或下载页。
预期:页面自动从登录引导切换到加载态,再展示对应数据;不需要杀 App 才能刷新。
- 失败重试统一检查:先让页面加载成功,再停止 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 <haifeng_hello@163.com>;该口径已作废,不再作为后续提交依据。 - 两个历史提交均已 push 到
develop;保留原始历史,不通过回滚或 force push 改写,不动main。 - 已裁定:后续提交统一使用
Zhou Weidong <502333560@qq.com>,author 与 committer 均按此口径。 - 本回执提交
cfce77a曾误用jianghaifeng <haifeng_hello@163.com>/trae-solo-agent <solo-agent@trae.ai>,现已通过 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<EngagementController>
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 树打开「我的 -> 观看历史」:
ProviderNotFoundException:
Could not find the correct Provider<CatalogRepository>
above this _WatchRecordsBody Widget
首页 feed 返回 2 条 banner、discover feed 返回 0 条 banner:
Expected: exactly one matching candidate
Actual: Found 0 widgets with type "CoverCarousel"
解读:两处红灯分别复现了用户报告的 Provider 缺失和轮播为空。
22.3 修复
lib/app.dart在现有 Repository Provider 附近注册Provider<CatalogRepository>.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 门禁原始输出
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test test/watch/watch_history_provider_test.dart test/discover/home_banner_source_test.dart
00:01 +2: All tests passed!
$ flutter test test/discover/cover_carousel_test.dart test/watch/watch_records_page_test.dart
00:00 +12: All tests passed!
$ flutter test
00:16 +286 ~1: All tests passed!
解读:完整套件 286 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;
输出中的 [WatchRecordsController] ... 异常堆栈来自故意注入失败的既有测试。
22.5 Git
5058853 fix(flutter): 注册目录仓库并修正首页轮播数据源
6cc5bfc merge: 合并首页轮播与观看历史真机修复
3e8703b..6cc5bfc develop -> develop
修复提交与合并提交的 author/committer 均为
Zhou Weidong <502333560@qq.com>。
22.6 用户真机验证步骤与预期
以下设备侧取证由用户执行,开发机测试不能替代真机证据。
- 使用最新
develop重新构建并安装,登录用户设备d-mvc33zh5-pemv3e。 预期:flutter run终端不再出现ProviderNotFoundException。 - 打开「我的 -> 观看历史」。 预期:页面正常打开;有历史时显示剧目与进度,无历史时显示空态,不再进入 Provider 错误页。
- 回到发现页并等待首页 feed 加载完成。
预期:顶部轮播显示后端
/app/home/feed返回的 2 张 banner,底部有两个圆点。 - 等待 4.5 秒。 预期:轮播自动切到第 2 张;手动拖动后仍可从当前页继续自动轮播。
设备侧截图、日志与最终验收结论由用户执行。
2026-10-17 K10 统一空列表占位
26.1 范围、分支与计划
叙述:新增统一 EmptyState,替换任务书表格中的 10 处纯文字空态。只改空态
视觉、三语 l10n 与测试,不改列表数据、接口、加载/失败逻辑。
- 客户端分支:
feature/flutter-k10-empty-states,基线19babb8。 - 文档分支:
docs/k10-empty-states,基线 K10 任务书3f84bc6。
开工前先写并提交计划:
f1d56b1 docs(plans): 增加 K10 统一空态实施计划
26.2 先红:组件与页面
组件文件尚不存在:
$ 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"
页面测试已能运行,但五处空态仍是旧实现:
$ 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 组件实现
新增:
128 lib/core/widgets/empty_state.dart
规格:
- 默认 96x96、圆角 28、
AppColors.blue11% 背景,图标 42。 - compact 64x64、圆角 18、图标 28。
- 标题 16/w600,副标题 13 muted,最多 2 行。
- 可选 TextButton 最小高度 44,主题主色。
- 深色主题图标
blueSoft,浅色主题图标blue。 - 外层 Semantics 只读标题;图形和文本排除重复语义,操作按钮仍可访问。
新增三语副标题:
en: New content will appear here.
zh: 有新内容时会显示在这里。
id: Konten baru akan muncul di sini.
26.4 10 处替换
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 定向与全量绿灯
组件测试:
$ flutter test test/empty_state/empty_state_test.dart
00:00 +4: All tests passed!
组件与五处页面:
$ 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!
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ 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)同步到独立临时副本后构建:
$ 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
6d08330 feat(flutter): 统一空列表占位组件
0466b4b merge: 合并 K10 统一空态组件
19babb8..0466b4b develop -> develop
功能提交与合并提交 author/committer 均为
Zhou Weidong <502333560@qq.com>。
26.8 用户真机验证步骤与预期
以下设备侧取证由用户执行,Codex 未跑真机。
- 使用最新客户端
develop,分别切换深色和浅色主题进入一个空列表页面。 预期:显示 96x96 柔和蓝色底、图标、标题与可选说明,不白屏、不溢出。 - 打开收藏、下载、观看记录、通知页面并清空对应数据。
预期:均显示统一
EmptyState,原有文案与清空/刷新逻辑正常。 - 打开无评论的剧目评论弹窗。 预期:显示 compact 64x64 图标占位,弹窗仍可滚动与输入。
- 打开无剧集详情或首页无剧目场景。 预期:显示对应空态;重试按钮行为保持不变。
- 模拟接口失败。 预期:仍显示原错误图标、错误文案和重试,不显示空态占位。
- 在 320px 宽小屏检查长标题、副标题和按钮。 预期:最多两行、可换行且无横向溢出,按钮点击区域不小于 44pt。
设备侧截图、语音朗读与最终验收结论由用户执行。
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 生成文件中报:
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 创建独立分支:
fix/ios-shared-preferences-foundation
pubspec.yaml:
dependency_overrides:
shared_preferences_foundation: 2.5.6
执行:
$ flutter pub get
Resolving dependencies...
Downloading packages...
! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available)
Got dependencies!
锁文件:
$ 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"
确认缓存包完整:
$ 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 依赖树原始输出
$ 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 编译验证
为排除先前版本的构建缓存,先清理再重新解析:
$ 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
$ flutter pub get
! shared_preferences_foundation 2.5.6 (overridden) (2.6.0 available)
Got dependencies!
完整无签名设备构建:
$ 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
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 用户后续验证
- 拉取最新客户端
develop并执行flutter pub get。 预期:终端显示shared_preferences_foundation 2.5.6 (overridden)。 - 从 Xcode 16.2 直接 Run 或执行
flutter run到 iPhone。 预期:Swift Compiler 不再报Unexpected , separator,App 可完成构建启动。 - 打开设置或任何依赖
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/。
开工前先写并提交计划:
e7c11c3 docs(plans): 增加 K8 登录页视觉实施计划
实施文件:
lib/pages/auth/auth_view.dartlib/pages/auth/auth_widgets.dartlib/pages/auth/auth_form_widgets.darttest/auth/auth_visual_test.dart
auth_view.dart 初次实现达到 654 行,触发项目单文件 500 行红线。随后按页面
组合、卡片/分段控件、表单控件与品牌组件拆分,最终行数:
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。
$ 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 用例:
$ 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 圆角、1pxoutlineVariant边框、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 全量门禁与构建
构建前检查并发进程:
$ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew'
输出仅有当前检查命令自身,没有 Flutter run/build 进程后开始门禁。
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:19 +292 ~1: All tests passed!
解读:292 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的
[WatchRecordsController] 堆栈来自既有故意注入失败的测试。
$ 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
45c3c64 feat(flutter): 重设计登录页视觉
99f64ab merge: 合并 K8 登录页视觉重设计
6cc5bfc..99f64ab develop -> develop
修复提交与合并提交 author/committer 均为
Zhou Weidong <502333560@qq.com>。
23.6 用户真机验证步骤与预期
以下设备侧取证由用户执行,Codex 未跑真机。
- 拉取最新客户端
develop,冷启动并进入登录页。 预期:全屏深色背景,顶部只有克制的蓝色光晕,品牌方块、App 名和印尼 tagline 清晰可见。 - 观察表单卡片和输入框。 预期:卡片为深色圆角面板;输入框高 52、圆角 14,邮箱和锁图标存在。
- 点击邮箱输入框并切换到密码框。 预期:聚焦输入框出现 1.5px 克莱因蓝边框,图标高亮;键盘 Next 会跳到密码, Done 会提交。
- 点击登录/注册 pill 两段。 预期:选中段蓝底白字,标题、副标题和按钮文案同步切换。
- 点击密码右侧眼睛按钮。 预期:密码明文/密文切换,按钮点击区域不小于 44pt。
- 输入错误邮箱或不安全密码并提交。 预期:卡片内出现浅红胶囊错误提示,文案与当前语言一致。
- 在 iPhone SE 尺寸打开键盘并滚动。 预期:无横向溢出,提交按钮可滚动到键盘上方并点击。
- 登录/注册提交时观察按钮与触觉。 预期:按钮显示 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。
开工前先写并提交计划:
496d273 docs(plans): 增加 K9 首页剧目网格实施计划
24.2 先红:首页网格测试
叙述:先新增 test/discover/home_all_dramas_grid_test.dart,未改实现与既有测试。
$ 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
关键参数:
crossAxisCount: 2
mainAxisSpacing: 16
crossAxisSpacing: 12
childAspectRatio: 2 / 3
horizontal padding: 16
卡片使用 RemoteImage、14px 圆角、底部深色渐变;标题 2 行、类型 1 行。
点击直接调用现有 logic.openCard(...)。分类数据直接使用
CategoryController.visibleDramas。
新增三语 allDramas 并执行 flutter gen-l10n:
$ 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';
定向绿灯:
$ flutter test test/discover/home_all_dramas_grid_test.dart
00:01 +5: All tests passed!
覆盖:无继续观看、全部剧目标题、2 列 delegate、左右/行列间距、2:3 封面、 分类切换、无剧目空态、分类缺失空态、失败重试与 320px 小屏。
文件规模:
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;没有删除测试用例。
$ flutter test test/widget_test.dart
00:05 +17: All tests passed!
24.5 全量门禁与构建
首次静态分析发现新增测试有一个未使用 import:
warning - test/discover/home_all_dramas_grid_test.dart:10:8
Unused import: 'package:fruitshorts/core/data/repositories/catalog_repository.dart'.
1 issue found.
移除后:
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:18 +297 ~1: All tests passed!
解读:297 项通过,1 项联调测试因客户端接口服务未启动按设计跳过;输出中的
[WatchRecordsController] 堆栈来自既有故意注入失败的测试。
构建前检查:
$ ps -axo pid=,etime=,args= | rg 'flutter_tools.snapshot (run|build)|xcodebuild|gradlew'
输出仅有当前检查命令自身,没有并发 Flutter run/build 后执行:
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 8.4s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
24.6 Git
cf03178 feat(flutter): 首页改为两列全部剧目网格
2c381be merge: 合并 K9 首页全部剧目网格
99f64ab..2c381be develop -> develop
功能提交与合并提交 author/committer 均为
Zhou Weidong <502333560@qq.com>。
24.7 用户真机验证步骤与预期
以下设备侧取证由用户执行,Codex 未跑真机。
- 使用最新客户端
develop进入首页「Home/发现」tab。 预期:页面不再出现「继续观看」标题或横向卡片。 - 向下查看分类 chips。
预期:chips 下方显示当前语言对应的「全部剧目」/
All Dramas/Semua Drama。 - 观察列表布局。 预期:一行 2 个剧目,左右各 16,列间距 12、行间距 16;封面为 2:3 圆角。
- 向下滚动长列表。 预期:继续按 2 列懒加载,滚动中无整页白屏或卡顿。
- 依次点击「为你推荐 / 热门 / 最新 / 服务端分类」。 预期:网格内容随分类更新;「为你推荐」显示去重后的全部剧目。
- 点击任意剧目卡片。 预期:沿用原详情流程打开对应剧目。
- 在有长标题或空分类数据时检查卡片。 预期:标题最多 2 行、类型最多 1 行,不横向溢出;空态和失败重试文案保留。
设备侧截图、日志与最终验收结论由用户执行。