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 条全过
This commit is contained in:
jianghaifeng committed 2026-10-16 16:44:13 +08:00
1 parent c503629bf9
commit f35db2cd3b
16 files changed
+355 -33

No files matched your search

+25 -6
View File
@@ -16,6 +16,29 @@
`code = 0` 成功;非 0 时 `message` 可直接展示给用户。分页统一为 `{ total, list }`。
**ID 字段一律以字符串下发(2026-10-16 J1 起,全局生效)**
所有 ID 字段(主键与外键,例如 `id` / `dramaId` / `episodeId` / `userId` / `orderId` …)
在 JSON 里都是**字符串**,不是数字:
```json
{ "code": 0, "message": "ok", "data": { "id": "2111012753262120962", "dramaId": "5" } }
```
原因:S0 之后主键是雪花 id,19 位十进制数已超过 JS 的 `Number.MAX_SAFE_INTEGER`
(`9007199254740991`)。裸数字下发会被浏览器/IEEE754 客户端四舍五入
(实测库里的 `2111012753262120962` 在浏览器里变成 `2111012753262121000`),
页面再拿它回查就是 `200002 数据不存在`。
约定与边界:
- **id 回传时按字符串传**(路径变量 / 查询参数 / 请求体都接受字符串,服务端自己转 `Long`);
- 实现方式:服务端**全局**把 `Long` / `long` 序列化成字符串(`fruitshorts-framework` 的
`JacksonLongToStringConfig`),**各模块不再逐字段加注解**;
- **例外:分页 `total` 仍是数字** —— 它是条数不是 id,前端分页组件要数字;
`Integer` / `int` 字段(`pageIndex` / `pageSize` / 各类计数如 `totalEpisodes`、状态码 `code`)
也**保持数字**。
**鉴权**:`Authorization: Bearer <token>`。
管理端与客户端用**两套独立密钥**签发,管理端 token 打到客户端接口会直接被拒。
客户端 `access token` 默认 2 小时,`refresh token` 30 天且每次刷新轮换。
@@ -773,10 +796,7 @@ GET /app/notices?pageIndex=1&pageSize=20
| `sort` / `status` | 排序 / 上下架 |
| `effectiveAt` / `expireAt` | 生效与失效时间(可空) |
> **⚠️ `id` 以字符串下发**(2026-10-16 实测补充):S0 换雪花主键后 id 已超过 JS 的安全整数范围
> (2^53-1 = 9007199254740991)。直接下发数字会被浏览器四舍五入(实测 `2111011206851596290`
> 在 JS 里变成 `2111011206851596300`),页面拿这个 id 回查就是"商品不存在"。
> 因此**本节所有响应里的 `id`(含新建返回的新 id)都是字符串**,前端原样拼进 URL 即可。
> `id` 以字符串下发:见 **§0「ID 字段一律以字符串下发」**(本节不再单独规定)。
**校验与错误码**:
@@ -809,8 +829,7 @@ GET /app/notices?pageIndex=1&pageSize=20
**字段**:`title` / `content` / `target`(`ALL` / `USER` / `CREATOR`)/ `type`(`SYSTEM` / `AUDIT` / `PAYMENT` / `ACTIVITY`)/
`status` / `publishAt` / `expireAt`;响应另带 `statusText` / `targetText` / `typeText`。
> **⚠️ `id` 以字符串下发**:同 §2.11 —— 雪花 id 超过 JS 安全整数范围,数字下发会被浏览器四舍五入,
> 导致"下架/编辑"回查不到记录。本节的 `id`(含新建返回的新 id)一律是字符串。
> `id` 以字符串下发:见 **§0「ID 字段一律以字符串下发」**。
**与 C 端口径严格对齐(同一条查询实现)**:C 端 `GET /app/notices` 只返回
`status=1` 且 `target ∈ {ALL, USER}`、`publish_at` 为空或 `<= now`、`expire_at` 为空或未过期的公告,