Files
FruitServer/docs/plans/PROGRESS.md
T
jianghaifeng 8d19a0cc0a 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 条全过
2026-10-16 16:33:15 +08:00

12 KiB
Raw Blame History

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 已列清单)。