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
@@ -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