Files
FruitServer/docs/plans/2026-10-16-J1.md
T
jianghaifeng f35db2cd3b feat(framework): 全局 Long 字符串化,ID 一律以字符串下发
用户裁定方案 (a):雪花 id 精度问题全局修,统一契约。

- 新增 JacksonLongToStringConfig(fruitshorts-framework):Long/Long.TYPE → ToStringSerializer,
  经 Jackson2ObjectMapperBuilderCustomizer 挂进 Spring Boot 的 ObjectMapper;admin-api 与
  app-api 都是 scanBasePackages=com.fruitshorts,一处生效两端一致
- 例外:PageResult.total 是计数不是 id,用 mixin 显式保持数字(前端分页组件要数字);
  Integer/int(pageIndex/pageSize/Result.code 等)不受影响
- 反序列化方向不变:请求体 'id:123'、路径变量、查询参数都接受字符串
- 契约 api-phase1.md §0 新增「ID 字段一律以字符串下发」总口径,§2.11/§2.12 改为引用 §0;
  I1 里逐字段加的 @JsonSerialize 与 Result<String> 回退成自然写法,避免多套口径
- 测试:新增 LongToStringSerializationTest(普通/嵌套/List/Map/分页 + 反序列化 + 19 位路径变量),
  契约测试的 standalone mapper 也挂上模块(否则测试假绿);C 端 4 条 id 断言改成 isString
- mvn test:commerce 33 / admin-api 21 / app-api 49 / worker 12 = 115 条全过
2026-10-16 16:44:13 +08:00

92 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# J1 全局 ID 字符串化(Long → String)+ 契约统一 —— 计划(2026-10-16)
> 依据:`docs/plans/INBOX.md` 18:30 条目。用户裁定:雪花 id 精度问题**按方案 (a) 全局修改、统一契约**。
> 起因:I1 实测「UI 点下架 → `200002 公告不存在`」,根因是 19 位雪花 id 超出 JS `Number.MAX_SAFE_INTEGER`
> (9007199254740991),浏览器 `JSON.parse` 会把它四舍五入。
## 一、方案
### 1. 后端:fruitshorts-framework 里注册全局序列化模块
- 新增 `com.fruitshorts.framework.web.JacksonLongToStringConfig`:
- `SimpleModule` 把 **`Long.class` 与 `Long.TYPE`(`long`)→ `ToStringSerializer`**;
- 通过 `Jackson2ObjectMapperBuilderCustomizer` 挂进 Spring Boot 自动配置的 `ObjectMapper`。
- 两个入口(admin-api / app-api)都是 `scanBasePackages = "com.fruitshorts"`,framework 在扫描范围内,
一处生效即两端生效;**不改任何 yml**。
- **不动 `Integer` / `int`**:`pageIndex` / `pageSize` / `Result.code` / `quantity` / `durationSec` …
仍是数字。
- **序列化方向**:`Long` 值一律输出成 JSON 字符串(含普通字段、嵌套对象、`List`、`Map` 值、分页 list 元素)。
- **反序列化方向**:Jackson 默认允许 `"123"` → `Long` 的标量强转,所以**请求体里 id 传字符串可以直接绑定**;
路径变量 `/…/{id}` 与查询参数走 Spring 的 `ConversionService`,本来就是 String → Long,不受影响。
这两条都要有测试兜住。
### 2. 一个刻意的例外:`PageResult.total` 保持数字
`PageResult.total` 是 `long` 计数,不是 id。放行它会让后台 ~13 处 `el-pagination :total=` 与 C 端分页全部变成
字符串,收益为零、风险不小。所以给这个字段加 `@JsonSerialize(using = NumberSerializer.class)` 显式保持数字,
并在契约 §0 里写明「**id 一律字符串;分页 total 是计数,仍是数字**」。
⚠️ 这是我自己拍的口径,已单独列进回执,若要求"Long 一律字符串"我再把这一行去掉。
### 3. 契约统一(`docs/api-phase1.md`)
- §0 通用约定新增「**所有 ID 字段(主键与外键)一律以字符串下发**」+ 示例 + `total` 例外说明。
- §2.11 / §2.12(I1 商品/公告)里逐模块写的 "id 以字符串下发" 改成引用 §0。
- **代码侧**:I1 给 `ProductRespVO` / `NoticeRespVO` 加的 `@JsonSerialize` 与 `Result<String>` 新建返回
**回退成自然写法**(全局生效后它们就是冗余的特例),保持"所有模块一个口径"。
### 4. ReelShortWeb 适配(审计结果见 §二)
把 id 当数字用的地方逐处改成字符串语义。**不建议**的写法(`Number(id)` / `id === 数字`)全部清掉。
### 5. ReelShort(Flutter)适配(最小改动)
**先说结论:客户端 model 里的 id 已经是 `(json['id'] ?? '').toString()` —— id 方向本来就是字符串,不会崩。**
会崩的是被全局规则扫到的 **Long 计数**(`as num?` 在收到字符串时直接抛异常)。逐个 Java 字段核过类型后,
只有两个字段真正会崩:
| Dart 位置 | 字段 | Java 类型 | 处理 |
| --- | --- | --- | --- |
| `lib/core/data/models/auth_session.dart:41` | `expiresIn` | `TokenRespVO.expiresIn` = `long` | 改成兼容解析 |
| `lib/core/data/models/drama.dart:101,117` | `viewCount` | `AppDramaCardVO/AppDramaDetailVO.viewCount` = `Long` | 改成兼容解析 |
| 其余 8 处 `as num?`(`durationSec`/`unlockCoin`/`number`/`totalEpisodes`/`freeEpisodes`/`code`…) | — | `Integer`/`int` | 不动(Java 侧不会被字符串化) |
做法:加一个兼容解析小工具(接受 `num` 或数字字符串),替换上面 3 处。**不做模型改造、不碰业务逻辑**,
避免和正在推进客户端的另一位同事冲突。
## 二、影响面清单(审计结果)
### ReelShortWeb(id 当数字用 → 必须改)
| 文件 | 问题 | 改法 |
| --- | --- | --- |
| `src/utils/creatorImpersonation.ts` | `Number(raw)` 读 localStorage 里的代看创作者 id → 19 位 id 被四舍五入,`X-Creator-Id` 头会带错值 | 存/读都按字符串 |
| `src/views/contentManagement/EpisodeManagementView.vue:58` | `Number(route.query.drama)` 与 `item.id ===` 比较 → 从短剧管理带 `?drama=` 跳过来选不中 | 按字符串比较 |
| `src/views/creator-center/composables/useDramaEdit.js:45` | `Number(route.params.id)` 与 `item.id ===` 比较 → 作品编辑找不到自己那条 | 按字符串比较 |
| `src/views/permission/composables/useMenuManagement.js:50,76,97,117` | 顶级菜单哨兵值是数字 `0`,而 `raw.parentId` 会变成字符串 `"0"` → 下拉回显错 | 哨兵统一成字符串 `'0'` |
### ReelShortWeb(核过不需要改)
- `total.value = res.data.total`(13 处)→ `total` 保持数字,无需改。
- `role.id === selectedRoleId`、`detail.id === row.id`、`useDramaEdit#findIndex`、`useCreatorPrice#find`
→ 两侧都来自接口,同为字符串,仍然相等。
- `formatCount(row.views)` → 内部 `Number(value)`,吃到字符串也正常。
- 全局 `parseInt`/`Number(`/id 算术/id 当数组下标/`a.id - b.id`:审计后**没有命中**。
### FruitServer(会跟着变化、需确认消费方)
- admin-api:所有 `Long` id、`viewCount`、`likeCount`、`memberCount` 等计数器 → 字符串。
- app-api:C 端 `id/dramaId/episodeId/userId`、`viewCount`、`positionMs/durationMs`、
`downloadedBytes/totalBytes`、`TokenRespVO.expiresIn` → 字符串;`Result.code`、`PageResult.total`、
`Integer` 字段 → 仍是数字。
## 三、风险 / 待确认
1. **`PageResult.total` 的例外**(见 §一.2)—— 我自己拍的,一句话可回退。
2. **契约测试的 standalone MockMvc 自带 mapper**:它本来是手搓的 `JsonMapper`,不含全局模块,
会让"字符串化"在测试里**假绿**。所以本轮把模块也挂进 `ControllerContractSupport`,让测试与生产一致;
代价是几条老断言(id 断数字)要改成断字符串。
3. **Flutter 仓库当前只有 `main`**(无 develop)。按条目要求在 develop 提交推送,会新建分支;
客户端由另一位同事推进,若他在 main 上继续,需要把这笔改动带过去 —— 回执里会写清。
4. `Map<Long, X>` 的 **key** 不在本次范围(只要求 Map 值);当前也没有接口返回 `Map<Long, ?>` 给前端。
5. 大 id 只在"新建后立刻用 id 操作"时暴露;老页面用种子小 id,所以**回归要靠端到端实测**,不能只看单测。