2104 lines
85 KiB
Markdown
2104 lines
85 KiB
Markdown
# 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 回执的「待确认」)
|
||
|
||
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<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.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) • <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 的设备号。
|
||
|
||
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 <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。
|