- /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 条全过
12 KiB
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 回执的「待确认」)
- 雪花 id 的全局方案:admin-api 是否统一
Long → String(涉及所有既有页面的id类型), app-api 是否要动(影响 Flutter 客户端解析)——属于平台级改动,等裁定。 /app/orders/verify的真发币链路需要 Google Play 凭据(B1-1 已列清单)。