Files
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

6.5 KiB
Raw Permalink Blame History

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,所以回归要靠端到端实测,不能只看单测。