feat(admin): 商品配置与系统公告管理接口,id 以字符串下发
- /admin/products 六个接口 + 校验(code 唯一、store_product_id 同平台唯一、 COIN_PACK 需 coin_amount>0、VIP 需 vip_days>0、VIP 只建不启) - /admin/notices 六个接口,与 C 端 /app/notices 共用 OpsNoticeService.pageForApp 一套筛选口径 - 权限点 product:list/product:update/notice:list/notice:update + 菜单 31/32/33(sql/data-phase1.sql 幂等) - 雪花 id 超过 JS 安全整数范围:id 以字符串下发,避免浏览器四舍五入导致回查 200002 - C 端商品查询收敛到 ProductService.findForStore,OrderService 不再自带一份 - 测试:module-commerce 33 / admin-api 16 / app-api 49 / worker 12,共 110 条全过
This commit is contained in:
1 parent
7513ac06bc
commit
8d19a0cc0a
24 files changed
+1761
-48
No files matched your search
@@ -742,6 +742,84 @@ GET /app/notices?pageIndex=1&pageSize=20
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/admin/creator-center/impersonation/targets` | `creator-center:impersonate` | 代看目标下拉的数据源(id / 名称 / 状态) |
|
||||
|
||||
### 2.11 商品(金币包)配置(已实现)
|
||||
|
||||
> 背景:`product` 表 16 个字段早就建好,但后台一直没有入口,运营只能改库;而 C 端 `/app/orders/verify`
|
||||
> 依赖 `store_product_id` 对齐商店商品(对不上就是 `200002 该商品未在后台配置`)。本节补齐 CRUD。
|
||||
> **不改表结构。**
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/admin/products/page` | `product:list` | 分页检索(`type` / `status` / `platform` / `keyword`,分页沿用 `{total,list}`) |
|
||||
| GET | `/admin/products/{id}` | `product:list` | 商品详情 |
|
||||
| POST | `/admin/products` | `product:update` | 新建 |
|
||||
| PUT | `/admin/products/{id}` | `product:update` | 编辑 |
|
||||
| PUT | `/admin/products/{id}/status` | `product:update` | 上架 / 下架(`status`:1 上架、0 下架) |
|
||||
| DELETE | `/admin/products/{id}` | `product:update` | 删除(逻辑删除) |
|
||||
|
||||
**字段**(与表一致,响应里额外带 `statusText` / `typeText`,前端不再写死中文):
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `code` | 商品编码,**全局唯一** |
|
||||
| `name` | 展示名 |
|
||||
| `type` | `COIN_PACK`(金币包)/ `VIP`(会员,第二阶段) |
|
||||
| `platform` | `ALL` / `GOOGLE` / `APPLE` |
|
||||
| `storeProductId` | 商店商品 id,**同一 `platform` 下唯一**(空串表示未绑定,允许多个) |
|
||||
| `coinAmount` / `bonusCoin` | 金币数与赠送(`type=COIN_PACK` 时 `coinAmount > 0`) |
|
||||
| `priceAmount` / `currency` / `displayPrice` | 价格与展示价 |
|
||||
| `regionCodes` | 生效区域,逗号分隔,空=全区域 |
|
||||
| `vipDays` | 会员天数(`type=VIP` 时 `> 0`) |
|
||||
| `sort` / `status` | 排序 / 上下架 |
|
||||
| `effectiveAt` / `expireAt` | 生效与失效时间(可空) |
|
||||
|
||||
> **⚠️ `id` 以字符串下发**(2026-10-16 实测补充):S0 换雪花主键后 id 已超过 JS 的安全整数范围
|
||||
> (2^53-1 = 9007199254740991)。直接下发数字会被浏览器四舍五入(实测 `2111011206851596290`
|
||||
> 在 JS 里变成 `2111011206851596300`),页面拿这个 id 回查就是"商品不存在"。
|
||||
> 因此**本节所有响应里的 `id`(含新建返回的新 id)都是字符串**,前端原样拼进 URL 即可。
|
||||
|
||||
**校验与错误码**:
|
||||
|
||||
| 场景 | 结果 |
|
||||
| --- | --- |
|
||||
| `code` 已存在 | `200001 商品编码已存在:{code}` |
|
||||
| `storeProductId` 在同 `platform` 下已存在 | `200001 该平台下商店商品 id 已被占用:{id}` |
|
||||
| `type=COIN_PACK` 但 `coinAmount <= 0` | `400 金币包的金币数量必须大于 0` |
|
||||
| `type=VIP` 但 `vipDays <= 0` | `400 会员商品的会员天数必须大于 0` |
|
||||
| **`type=VIP` 且要启用**(`status=1`) | `400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)` |
|
||||
| 商品不存在 | `200002 商品不存在` |
|
||||
|
||||
> **VIP 口径(2026-10-16 定,与 B1-1「VIP 订阅本轮不做」一致)**:**允许创建、不允许启用**。
|
||||
> 运营可以先录好商品与商店 product id,等 B2 上线后直接上下架;**启用**才是会误导客户端的动作,拦在这里。
|
||||
|
||||
### 2.12 系统公告管理(已实现)
|
||||
|
||||
> 背景:`ops_notice` 表 0 行、契约原先写着"没有产出方";E1 已交付 C 端 `GET /app/notices`(匿名只读)。
|
||||
> 本节补齐后台写入。**不改表结构。**
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/admin/notices/page` | `notice:list` | 分页检索(`status` / `target` / `type` / `keyword`) |
|
||||
| GET | `/admin/notices/{id}` | `notice:list` | 公告详情 |
|
||||
| POST | `/admin/notices` | `notice:update` | 新建 |
|
||||
| PUT | `/admin/notices/{id}` | `notice:update` | 编辑 |
|
||||
| PUT | `/admin/notices/{id}/status` | `notice:update` | 发布(`status=1`)/ 下架(`status=0`) |
|
||||
| DELETE | `/admin/notices/{id}` | `notice:update` | 删除(逻辑删除) |
|
||||
|
||||
**字段**:`title` / `content` / `target`(`ALL` / `USER` / `CREATOR`)/ `type`(`SYSTEM` / `AUDIT` / `PAYMENT` / `ACTIVITY`)/
|
||||
`status` / `publishAt` / `expireAt`;响应另带 `statusText` / `targetText` / `typeText`。
|
||||
|
||||
> **⚠️ `id` 以字符串下发**:同 §2.11 —— 雪花 id 超过 JS 安全整数范围,数字下发会被浏览器四舍五入,
|
||||
> 导致"下架/编辑"回查不到记录。本节的 `id`(含新建返回的新 id)一律是字符串。
|
||||
|
||||
**与 C 端口径严格对齐(同一条查询实现)**:C 端 `GET /app/notices` 只返回
|
||||
`status=1` 且 `target ∈ {ALL, USER}`、`publish_at` 为空或 `<= now`、`expire_at` 为空或未过期的公告,
|
||||
按 `publish_at desc, id desc` 排序。**后台发布即 C 端可见;下架或过期后 C 端不可见。**
|
||||
(实现上收敛在 `OpsNoticeService.pageForApp(...)` 一处,避免两端条件漂移。)
|
||||
|
||||
**筛选与错误码**:`title` 必填(1–128);`content` 最长 2000;公告不存在 → `200002 公告不存在`;
|
||||
`status` 只接受 0/1,其他值 → `400`。
|
||||
|
||||
## 3. 契约与实现状态对照
|
||||
|
||||
> 最后更新:2026-10-16(`develop` 分支)。未完成项汇总见仓库根 `README.md`「尚未完成」。
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# I1 后台管理端:商品(金币包)配置 + 系统公告管理 —— 计划(2026-10-16)
|
||||
|
||||
> 依据:`FruitServer/docs/plans/INBOX.md` 17:30 条目。用户口径「现在做后台管理端,规范还是按照 specs」。
|
||||
> **本轮=后端接口 + ReelShortWeb 页面一起做**。指令通道已迁到本仓库(`FruitServer/docs/plans/`),
|
||||
> 留痕写 `FruitServer/docs/plans/PROGRESS.md`(本轮新建);**specs 只读、不再写入**。
|
||||
|
||||
## 一、两个缺口(PM 已勘察,直接用)
|
||||
|
||||
1. **商品/金币包配置后台无入口**:`product` 表 16 字段已建、3 行种子、C 端 verify 依赖 `store_product_id` 对齐商店商品,但 admin-api 没有任何商品 CRUD。
|
||||
2. **公告有读没写**:`ops_notice` 表 0 行,E1 交付了 C 端读,后台没有写入口。
|
||||
|
||||
## 二、勘察补充(影响做法,已实测)
|
||||
|
||||
1. **前端挂载机制**(与条目描述略有出入,已核实):`src/utils/routerHelper.ts` 用
|
||||
`import.meta.glob('../views/**/*.{vue,tsx}')` + 后端菜单的 `component` 字符串**动态解析**;
|
||||
`src/router/index.ts` 的 `asyncRouterMap` 只静态声明了 dashboard/content/user/system 等早期分组,
|
||||
commerce/governance/board 这些都没有静态登记。→ 所以新增页面**只需**:① 新建 `.vue`;
|
||||
② `sql/data-phase1.sql` 里加菜单且 `component` 与视图路径一致。**静态路由不必改**(改了也无害)。
|
||||
真正的坑是 `route_name` 唯一(曾因 `CreatorCenter` 重名导致 404),本轮命名会避开。
|
||||
2. **id 保留段**:权限点已用到 1155、菜单已用到 30 → 权限点用 **1160-1163**、菜单用 **31/32/33**。
|
||||
3. **前端既有约定**:api 封装在 `src/api/admin/*`(`import request from '@/axios'` + `PageResult`),
|
||||
页面用 `useRbac().can(code)` 控权、`dateCell` 格式化日期、`PageHeading` 标题;金额/金币这类大数值本项目
|
||||
没有 `formatCount`,实际用的是 `src/utils/format.js`,**我会用其中的数值格式化工具或 `toLocaleString`**,
|
||||
并在回执说明(不新造 util)。
|
||||
|
||||
## 三、口径决定(需在回执与契约里写清)
|
||||
|
||||
**VIP 商品**:**允许创建、不允许启用**(`PUT /{id}/status` 把 `type=VIP` 置为启用 → `400`)。
|
||||
理由:运营可以先把 VIP 商品与商店 product id 录好,等 B2 订阅上线后一键启用;直接拒绝创建会让上线日重新补数据。
|
||||
B1-1 已定「VIP 订阅本轮不做」,所以**启用**才是会产生误导的动作,拦在这里。
|
||||
|
||||
## 四、后端做法
|
||||
|
||||
1. **契约先行**:`docs/api-phase1.md` 新增一节(放在 §2.6 订单与权益之后的新编号),写清两个模块的
|
||||
接口、字段、校验、错误码、权限点。
|
||||
2. **商品**(module-commerce 加 service/VO,admin-api 加 controller):
|
||||
`GET /admin/products/page`(type/status/platform/keyword + 分页)、`GET /{id}`、`POST`、`PUT /{id}`、
|
||||
`PUT /{id}/status`、`DELETE /{id}`;字段按表结构;校验:`code` 全局唯一、`store_product_id` 同 `platform` 唯一、
|
||||
`type=COIN → coin_amount>0`、`type=VIP → vip_days>0`、**VIP 不可启用**。响应带 `statusText/typeText`(前端不写死中文)。
|
||||
3. **公告**(module-system 加 `OpsNoticeService` + VO,admin-api 加 controller):
|
||||
`GET /admin/notices/page`、`GET /{id}`、`POST`、`PUT /{id}`、`PUT /{id}/status`、`DELETE /{id}`。
|
||||
**筛选逻辑收敛到 `OpsNoticeService.pageForApp()` 一处**,C 端 `AppNoticeService` 改为委托它
|
||||
(改我自己 E1 写的文件),这样"后台发布 → C 端可见 / 过期或下架 → 不可见"用的是同一段条件,不会漂移。
|
||||
4. **菜单与权限点**:`sql/data-phase1.sql` 幂等追加(权限点 1160-1163、菜单 31/32/33 与超管授权)。
|
||||
商品挂「订单与权益」(id 22) 下;公告新建「运营管理」(id 32) 分组 + 子菜单 (33)。
|
||||
5. **Swagger**:两个新控制器按 S1 规范加 `@Tag`/`@Operation`,并把分组补进 admin-api 的 `@OpenAPIDefinition`。
|
||||
|
||||
## 五、前端做法(ReelShortWeb,develop 分支)
|
||||
|
||||
- `src/api/admin/product.ts`、`src/api/admin/notice.ts`(按 `commerce.ts` 的写法)
|
||||
- `src/views/commerce/ProductManagementView.vue`(列表/筛选/新建编辑弹窗/上下架/删除)
|
||||
- `src/views/operation/NoticeManagementView.vue`(列表/新建编辑/发布下架/删除)
|
||||
- `sql/data-phase1.sql` 的菜单 `component` 写 `views/commerce/ProductManagementView`、`views/operation/NoticeManagementView`
|
||||
- **不改既有页面**;`route_name` 用 `commerce-products` / `operation-notices`(先 grep 确认不重名)
|
||||
|
||||
## 六、门禁
|
||||
|
||||
- 后端:`mvn test` 存量 **91 条不得跑红** + 新增测试(商品三类校验、公告可见性委托、
|
||||
两个控制器的接入层契约、权限点注解检查)
|
||||
- 前端:`pnpm build:pro` 通过 + eslint 零错误
|
||||
- 端到端:后台登录 → 菜单出现两个新页面 → 建商品 → 用该商品在 C 端 `verify` 能对上
|
||||
(`store_product_id` 命中,不再是 `200002 该商品未在后台配置`);公告发布 → C 端 `/app/notices` 可见 → 下架/过期 → 不可见
|
||||
|
||||
## 七、风险 / 待确认
|
||||
|
||||
1. **不新增静态路由**是否与条目"两边都要加"冲突:我按实测的加载机制做,若你要求同时静态登记,我再补(不影响正确性)。
|
||||
2. `/app/orders/verify` 走真实 Google Play 才有完整链路;本机无商店凭据,端到端验证到"200002 消失、进入校验环节"为止。
|
||||
3. 公告"发布时间"语义:`publish_at` 为空视为立即生效(C 端条件里已按"为空或 <= now"处理)。
|
||||
@@ -0,0 +1,244 @@
|
||||
# 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 已列清单)。
|
||||
Reference in new issue
Block a user