From 0cab094194926e1f4486bcfe80d1f6913c59a44e Mon Sep 17 00:00:00 2001 From: jianghaifeng Date: Fri, 16 Oct 2026 16:48:07 +0800 Subject: [PATCH] =?UTF-8?q?docs(plans):=20J1=20=E5=9B=9E=E6=89=A7=EF=BC=88?= =?UTF-8?q?=E5=85=A8=E5=B1=80=20ID=20=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=8C=96?= =?UTF-8?q?=20+=20=E5=A5=91=E7=BA=A6=E7=BB=9F=E4=B8=80=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E6=A0=87=E4=B8=BA=E5=B7=B2=E5=A4=84=E7=90=86=C2=B7=E5=BE=85?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/INBOX.md | 90 +++++++++++++++++++++++++++++++- docs/plans/PROGRESS.md | 116 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) diff --git a/docs/plans/INBOX.md b/docs/plans/INBOX.md index feefe14..3d20d07 100644 --- a/docs/plans/INBOX.md +++ b/docs/plans/INBOX.md @@ -19,7 +19,7 @@ --- -## [待处理] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一 +## [已处理·待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一 **用户裁定(2026-10-16 18:10)**:雪花 id 精度问题 **「按方案 (a) 全局修改,统一契约」**。 @@ -201,3 +201,91 @@ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v3/api-docs 任务只有 S0/S1/S0-16、A1–A6、B1/B2、C1–C3 —— **没有 I1(后台管理端 商品配置 + 系统公告)这一条**, 所以本轮没有可同步的任务号,我没有动任何既有任务的状态(避免误改 B1/C1/A* 这些别的批次)。 下一步请给 I1 建任务并告知任务号,我补一次状态同步(`codex01` 的 token 已可用)。 + +--- + +### 回执(Codex,2026-10-16):J1 完成,停在干净交接点 + +**提交**(三个仓库各自 `develop`,`main` 未动) +- FruitServer `f35db2c`(develop,已推):全局 Long→String + 契约 §0 + 测试 +- ReelShortWeb `3ec9ea6`(develop,已推):4 处 id 数字语义适配 +- ReelShort `94ff9f1`(**新建 develop**,已推):客户端 Long 计数兼容解析 +**留痕**:`docs/plans/PROGRESS.md`(本轮追加 J1 段,含原始输出);计划 `docs/plans/2026-10-16-J1.md`。 + +**门禁数字** +| 门禁 | 结果 | +| --- | --- | +| `mvn test` | **BUILD SUCCESS** — commerce 33 / admin-api 21 / app-api 49 / worker 12 = **115 条全过**(存量 110 未跑红,新增 5) | +| `mvn … package -DskipTests` | BUILD SUCCESS | +| `pnpm ts:check` / eslint / `pnpm build:pro` | 0 错误 / 0 error / Build successful | +| `dart analyze lib test` / `flutter test` | No issues found! / All tests passed!(+167) | + +**每个仓库改了什么** + +FruitServer(后端) +- 新增 `fruitshorts-framework/.../web/JacksonLongToStringConfig.java`(`Long`/`long` → 字符串; + `PageResult.total` 用 mixin 保持数字;`PlainLongSerializer` 供 mixin 使用) +- `docs/api-phase1.md`:§0 新增「ID 字段一律以字符串下发」总口径(含示例、边界、`total` 例外), + §2.11/§2.12 改为引用 §0 +- I1 的特例回退:`ProductRespVO` / `NoticeRespVO` 去掉 `@JsonSerialize`, + `ProductAdminController` / `NoticeAdminController` 的 `create` 回到 `Result` +- 测试:新增 `admin-api/.../web/LongToStringSerializationTest.java`(5 条); + `ControllerContractSupport`(admin + app 两处)挂上模块,避免契约测试假绿; + C 端 4 条 id 断言改成 `value("…") + isString()` + +ReelShortWeb(4 个文件) +- `src/utils/creatorImpersonation.ts` — 代看创作者 id 全程字符串(原来 `Number(raw)`,19 位会被四舍五入) +- `src/views/contentManagement/EpisodeManagementView.vue` — `Number(route.query.drama)` → 字符串比较 +- `src/views/creator-center/composables/useDramaEdit.js` — `Number(route.params.id)` → 字符串比较 +- `src/views/permission/composables/useMenuManagement.js` — 顶级菜单哨兵 `0` → `'0'`(与接口的 `"0"` 对齐) + +ReelShort(Flutter)逐文件清单(**只有这些,没夹带别的**) +- `lib/core/data/models/json_numbers.dart`(新增)— `jsonToInt`:接受数字或数字字符串 +- `lib/core/data/models/auth_session.dart` — `expiresIn`(后端 `long`)改用 `jsonToInt` +- `lib/core/data/models/drama.dart` — `viewCount`(后端 `Long`)两处改用 `jsonToInt` +- `test/core/json_id_parsing_test.dart`(新增)— 8 条回归用例 + +**三个需要你确认的口径** + +1. **`PageResult.total` 保持数字(唯一例外)** —— 它是条数不是 id,前端分页组件(el-pagination / Flutter 分页) + 都要数字。实测 `total = 2 int`。若你要求"Long 一律字符串",删掉 `JacksonLongToStringConfig` 里那一行 mixin 即可。 +2. **反向(请求方向)未做限制**:请求体 `"id":"…"`、路径变量、查询参数都接受字符串并转 `Long`(有测试)。 + `Integer`/`int`(`pageIndex`/`pageSize`/`Result.code`/`totalEpisodes`/`durationSec`…)保持数字 —— 实测已确认。 +3. **客户端适配范围比"只改 id 解析"宽了一点点,原因是物理上必须**:客户端 id 本来就是 + `(json['id'] ?? '').toString()`,不会崩;真正会崩的是被全局规则扫到的 **Long 计数**, + 所以改了 `expiresIn` 与 `viewCount` 共 3 处(`json_numbers.dart` 是新增工具)。 + 其余 8 处 `as num?` 对应的 Java 字段都是 `Integer`/`int`,**按最小改动原则没动**。 + ⚠️ **ReelShort 仓库原来只有 `main`**(无 develop),我按条目要求新建了 `develop` 并在上面提交。 + 客户端由另一位同事推进,若他继续在 `main` 上走,需要把这笔 `94ff9f1` 带过去。 + +**用户可照做的验证步骤(每步都有预期结果)** + +准备:`cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests`,起两个 jar; +`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/v3/api-docs` → 200。 + +1. **复现 PM 那条(最直观)**:注册一个 C 端账号 + ``` + curl -s -X POST http://127.0.0.1:8081/app/auth/register -H 'Content-Type: application/json' \ + -d '{"targetType":"EMAIL","target":"你的邮箱@example.com","password":"Passw0rd!23","nickname":"手测","deviceId":"manual-1"}' + ``` + 预期:`"expiresIn": "7200"`(字符串)。 +2. **用响应里的 user id 调后台**:`curl -s "http://127.0.0.1:8080/admin/app-users/page?pageSize=50" -H "Authorization: Bearer "`, + 拿新用户的 `id`(预期是 `"2111…"` 这种**带引号的字符串**、`total` 是**不带引号的数字**),再 + `curl -s "http://127.0.0.1:8080/admin/app-users/" -H "Authorization: Bearer "` → 预期 `code=0`、`data.id` 与列表一致 + (**修复前这里返回 `200002 数据不存在`**)。 +3. **JS 逐位一致**:把第 2 步的响应存成 `r.json`,然后 + ``` + node -e 'const d=JSON.parse(require("fs").readFileSync("r.json","utf8"));console.log(String(d.data.id))' + ``` + 预期与 `psql -c "select id::text from app_user where uid=''"` **逐位相同**。 + 对照:`node -e 'console.log(String(JSON.parse("2111015748376858626")))'` → `2111015748376858620` 附近(会被改写),说明精度问题真实存在。 +4. **后台界面(:3005)**:`admin/admin123` 登录 → 商品配置新建一个商品 → **点它那一行的状态/编辑**, + 预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是 `0`。 +5. **短剧管理 → 剧集管理带参跳转**:从短剧管理点某剧的「剧集」,URL 带 `?drama=`, + 预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。 +6. **客户端**:`cd ReelShort && flutter test`(预期 All tests passed);有设备时 `flutter run` 打开首页看播放量/详情 + 不报解析异常(后端 `viewCount` 现在下发字符串)。 +7. **异常路径**:请求体里 id 传**数字**(老客户端行为)也应被接受:`PUT /admin/products//status?status=1` 用数字 id 路径同样 200。 + +**禅道**:J1 在禅道里同样没有对应任务(项目 2 下仍是 S0/S1/S0-16、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。 +**服务现状**:8080/8081 是我刚重建起回的新 jar(自测时在跑);:3005 的 dev server 也在。若被环境回收,按上面「准备」起回来即可。 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 52a360a..29ad3db 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -242,3 +242,119 @@ DELETE 1 # 公告 I1 复测公告:印尼开服 1. 雪花 id 的全局方案:admin-api 是否统一 `Long → String`(涉及所有既有页面的 `id` 类型), app-api 是否要动(影响 Flutter 客户端解析)——属于平台级改动,等裁定。 2. `/app/orders/verify` 的真发币链路需要 Google Play 凭据(B1-1 已列清单)。 + +--- + +## 2026-10-16 J1:全局 ID 字符串化(Long → String)+ 契约统一 + +计划文件:`docs/plans/2026-10-16-J1.md`(先写计划再动手)。用户裁定「方案 (a) 全局修改,统一契约」。 + +### 动作 1:后端全局序列化(fruitshorts-framework) + +新增 `JacksonLongToStringConfig`:`SimpleModule` 把 `Long.class` / `Long.TYPE` → `ToStringSerializer`, +经 `Jackson2ObjectMapperBuilderCustomizer` 挂进 Spring Boot 的 `ObjectMapper`。 +两个入口都是 `scanBasePackages = "com.fruitshorts"`,一处生效两端一致,**没改 yml**。 + +一个刻意的例外:`PageResult.total` 是条数不是 id,用 mixin 显式保持数字(前端分页组件要数字)。 +为此给 mixin 加了个 `PlainLongSerializer`(`NumberSerializer` 没有无参构造,`using=` 装不上)。 + +I1 里给商品/公告 VO 逐个加的 `@JsonSerialize` 与 `Result` 新建返回**已回退成自然写法** —— +全局生效后它们只是冗余特例(现在是"所有模块一个口径")。 + +### 动作 2:契约 §0 总口径 + +`docs/api-phase1.md` §0 新增「**ID 字段一律以字符串下发**」+ 示例 + 边界(`total` 与 `Integer` 保持数字、 +id 回传按字符串传),§2.11/§2.12 的逐模块表述改成引用 §0。 + +### 动作 3:契约测试的 standalone mapper 也挂上模块(否则假绿) + +`ControllerContractSupport`(app-api / admin-api 两处)自带手搓 `JsonMapper`,不含全局模块 —— +不改它的话,测试断言数字 id 也会过,而生产早就变成字符串了。挂上模块后: + +``` +$ mvn test +[INFO] Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-module-commerce) +[INFO] Tests run: 21, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-admin-api) +[INFO] Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-app-api) +[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 (fruitshorts-transcode-worker) +[INFO] BUILD SUCCESS # 共 115 条(存量 110 + 新增 5) +``` + +新增 `LongToStringSerializationTest` 5 条:普通字段 / 嵌套对象 / List / Map 值 / 分页(total 数字 + list id 字符串) +/ 请求体 `"id":"…"` 反序列化成 Long / 19 位路径变量绑定成 Long。 +另外把 C 端 4 条 id 断言从数字改成 `value("…") + isString()`(原来"数字也能过"的宽松比较不算证据)。 + +### 动作 4:Web 与客户端适配 + +见回执的逐文件清单。要点:Web 审计出 4 处把 id 当数字用的地方(代看 id 过 `Number`、 +两处 `Number(route.…)` 与 id 比较、菜单顶级哨兵 0 与 "0" 不一致);Flutter 侧 id 本来就是 +`(json['id'] ?? '').toString()`,会崩的是被全局规则扫到的 **Long 计数**,只改了 `expiresIn` 与 `viewCount`。 + +``` +$ pnpm ts:check → 0 错误 +$ pnpm exec eslint … ./src → 0 error +$ pnpm build:pro → Build successful +$ dart analyze lib test → No issues found! +$ flutter test → All tests passed! (+167) +``` + +### 动作 5:端到端复现"修复前后"(真实服务) + +``` +① 复现 PM 那个场景:C 端注册 → admin 列表/详情 +$ curl -X POST :8081/app/auth/register -d '{"targetType":"EMAIL","target":"j1.selftest@example.com",…}' + → "expiresIn": "7200" # long 字段,已是字符串 +$ curl -H "Authorization: …" :8080/admin/app-users/page?pageSize=50 + total = 2 int # 分页 total 仍是数字(例外生效) + 命中用户: uid=U79DG5IOM id='2111015748376858626' id类型=str +$ curl -H "Authorization: …" :8080/admin/app-users/2111015748376858626 + code= 0 message= ok # 修复前这里就是 200002 + data.id= '2111015748376858626' uid= U79DG5IOM +``` + +``` +② JS 侧逐位一致(真实响应喂 node) +$ node -e '…' +响应原文里的 id(JSON 字符串): "2111015748376858626" +typeof 解析后 : string +String(parsed.id) : 2111015748376858626 +库里 app_user.id : 2111015748376858626 +逐位相等? : true +---- 反面对照:同一串数字当裸数字解析 ---- +裸数字 -> String(parsed.id) : 2111015748376858600 +逐位相等? : false +``` + +``` +③ 全局范围:换两个模块(商品 / 创作者)走"新建 → 用响应 id 查详情 → 改状态" +新建商品响应 : {"code":0,"data":"2111015841427492865"} +商品详情 : code=0 id='2111015841427492865' +商品上架 : {"code":0} +新建创作者响应 : {"code":0,"data":"2111015841771425794"} +创作者详情 : code=0 id='2111015841771425794' name=J1 探针工作室 +``` + +``` +④ C 端字段类型(匿名可读的链路) +/app/home/feed : drama card id='5'(str) viewCount='3100000'(str) +/app/dramas/5 : id='5'(str) viewCount='3100000'(str) totalEpisodes=72(int) episode id='7'(str) +/app/episodes/7/play : episodeId='7'(str) dramaId='5'(str) durationSec=228(int) unlockCoin=0(int) +``` + +解读:**Long → 字符串**(id、viewCount、expiresIn、coinAmount),**Integer/int 与分页 total 仍是数字**, +与 §0 的规则和例外完全对上。 + +### 动作 6:自测数据清理(含一次口径纠正) + +``` +DELETE 1 # 自测用户 app_user(含 credential 2 / session 2 / device 2 / consent 3 / wallet 1) +DELETE 1 # 商品 j1_probe_coin +DELETE 1 # 创作者 J1PROBE +全库扫描 uid / 邮箱 / 雪花 id → 无任何残留(NOTICE: 扫描完成) +现存:creator 只剩 PM 那条 id=25(CR20261015001)、app_user 只剩 id=28(UHR-DLTH8)、 + product 3 行种子、ops_notice 0 行 —— 都不是本轮数据。 +``` + +⚠️ 口径纠正:我第一版清理脚本用 `psql -t -A -c "delete … returning 1" | wc -l` 数"行数", +**数到的其实是 psql 的命令标签**(实测删 0 行时输出 `DELETE 0`,也是 1 行),所以那轮打印的 +"删除 1 行"是假数字。真正的证据是后面的**全库残留扫描**(零命中)与现存数据核对,已按此结论写回执。