2026-10-09-har-pick-dedup-retry-交付说明.md 13 KB

2026-10-09 交付说明 —— 开放 API 挑店改版 + 去上游化 + 下游回调重试(汇安融 HAR)

对象:java 分支 3 个提交 + 1 个 jar + 1 个 dist.zip + 1 个必跑 SQL。 状态:已提交、未 push(push 由你做)。origin/java = f09e9cce; 写本文件时 git rev-list --count origin/java..java = 8(上批 region 5 个 + 本批 3 个;本文件自身的提交不计入)。

★ 2026-10-09 晚间补记 —— 批后又追加 3 个提交(六位订正 / 编号生成 / 评审修复)

正文写于当天下午(批 = 3 提交 + jar 8662237e… + dist 4b87bb40…);此后 java 分支又追加:

  • 680406f9 省市编码订正为 GB/T 2260 完整六位(20 文件:har-region-cn.json 重生成、7 个测试类改钉、8 处 javadoc、前端 har.ts/har-openapi.md/建店表单;db/har-shop-region.sql 追加「短码订正」幂等 UPDATE 段)
  • 128a2276 建店门店编号自动生成(前端 2 文件:utils/shop-code.ts + 建店表单;SP + 时间戳 + 16 位 CSPRNG,只读、无「重新生成」按钮)
  • 946a3639 建店弹窗警示随编号自动生成同步 —— 删去已不成立的「重复提交会被后台拒绝」,改为如实陈述(独立评审 🟡1)

现行产物(换这一套,覆盖 §二里记的指纹):

  • jar:5e5bfb4747be…(143,153,224 字节,17:08:09;含六位订正)
  • dist.zip:ab7c0c96fbb8…(9,377,220 字节,17:23:49;含编号生成 + 弹窗警示修复)

部署顺序不变;两个 SQL 都要跑:db/har-callback-retry.sql 且 db/har-shop-region.sql(后者自 2026-10-09 起含短码订正段);无重登、无新菜单。本补记同时把两处残留短码示例订正为六位(评审 🟡2)。写本补记时 git rev-list --count origin/java..java = 12,本提交计入则 13。

需求原文与裁定

① 开放 API 挑店改版(C)

原文:「开放api不用传市了,传省份或者ip,ip精确度更高所以优先级高于省份字段,两个可以同时传也可以只传其中一个,只传省份且有多个市级门店的情况下随机使用其中一个门店」

裁定与落地:

  • IP 解析失败 / 该 IP 解析出归属地但无店 ⇒ 回退省份码继续挑(两者都没有才报错)
  • 稳定散列(同一 out_biz_no 恒挑同一家);out_biz_no 为空 ⇒ 随机
  • city_code 字段删除;误传须响亮失败(Jackson UnrecognizedPropertyException,不静默忽略)
  • 400 文案三句:省份编码不存在: 999999 / 用户 IP 无法解析出归属地: 1.2.3.4 / 该地区没有可用门店: 省 320000(带市时 该地区没有可用门店: 430100(省 430000))
  • 显式门店码(third_shop_id)优先——传了就不挑

② 开放接口去上游化(B)

原文:「开放接口中暴露了上游har,把名字换了,不要暴露给调用方」

裁定与落地:

  • 范围:开放 API 路径 + 文档 + 订单号前缀 + 顾客页 URL 里露出的字样
  • 新路径词 alipay:/payment/openapi/har/** → /payment/openapi/alipay/**。 双白名单同名改(SecurityConfig 的 permitAll + TenantApiKeyAuthFilter 的前缀/放行清单)——只改一处就是「被放行 + 无人认证」的零鉴权(2026-09-24 踩过)
  • 订单号前缀 HAR → AP(仅新单;存量单号不动)
  • 顾客页 URL /#/checkout/har/<token> → /#/checkout/alipay/<token>(路由名 HarCheckout 不变;后端 PATH_PREFIX 与前端路由段必须同名改,单边改 = 整页 404)
  • biz 不再回吐给调用方;留空时服务端按 trade_type 派生(alipay_un_stage ⇒ har_ali_un_stage)
  • 后台接口路径未动(HarOrderController 只改了一句 javadoc;/payment/har/checkout/... 等后台端点原样)

③ 下游回调失败自动重试(A)

原文与裁定:「1、维持租户级兜底,一个租户本来只有一个回调地址 2、存量不补推 做」;另:「直接用我们后台页面生成的checkout码进行支付,会给商家回调吗?如果没有的话也要做」

裁定与落地:

  • 对齐乐收赢:首推一次 + 调度器每 60s 捞一轮(FAILED 且未达上限、饿最久优先、单轮 50 条),上限 8 次(含首推),har.callback-max-retries 可配
  • 缺回调地址也落 FAILED(不再停 PENDING);每次尝试写 notify_count / last_notify_time
  • 轮询腿真流转(rows>0 且状态/退款时间真变)后补发回调 —— 上游通知窗口只有约 25 秒,丢了的靠这条收敛;退款抢占/回退两条腿也各补一帧;create 不推
  • 存量 FAILED 不补推(SQL 封顶 notify_count=8)

一、提交清单

# commit 内容 规模
C 94d120d1 feat(汇安融): 开放 API 挑店改版 —— 传顾客 IP 或省码(IP 优先、省码回退),不再收市码 8 文件 +263/−109
B d16f6f75 feat(汇安融): 开放接口去上游化 —— 路径改 /payment/openapi/alipay、订单号前缀改 AP、biz 不再回吐 24 文件 +312/−70
A 6a0c72a6 feat(汇安融): 下游回调失败自动重试 —— 对齐乐收赢:首推一次 + 每分钟一轮,上限 8 次 11 文件 +774/−77

全批 36 个文件(33 改 + 3 新:OpenapiAuthWiringTest.java、HarCallbackServiceTest.java、db/har-callback-retry.sql)。

拆分方法(时序重放):三件事改在同一批文件上(HarOrderService、har-openapi.md、多个测试类被三件事同时动过),逐文件 git add 拆不干净。做法:把后做任务的 hunk 逐条 Edit 反向抹掉 → 只留 C 的树 → 提交 → 从 logs/_split-2026-10-09/ 的基线字节级还原(逐文件 cmp 核过)→ 再抹掉 A → 提交 B → 还原全部 → 提交 A。每阶段做了残留令牌扫描(只看 changed line,排除 hunk 头回声)确认没有串味;git commit 带显式 pathspec,两个预暂存的 D logs/*.log.gz 没有被卷进去。拆分中间树不是全绿的,只跑各阶段相关测试类;全量只在最终树跑。

二、验证读数

阶段测试(各阶段只跑相关类)

  • C:111 条 / 0 失败(HarShopRegionPickTest 28、HarOrderServiceTest 61、HarShopServiceTest 18、HarOrderCreateJsonContractTest 4)
  • B:112 条 / 0 失败(7 类,含新 OpenapiAuthWiringTest 2 条 —— 关掉「白名单改了、filter 没认」的零鉴权盲区)
  • A:90 条 / 0 失败(HarCallbackServiceTest 8、HarOrderPollSchedulerTest 8、HarOrderServiceTest 74)

日志:logs/_split-2026-10-09/stage-{c,b,a}-test.log

全量

1266 条 / 0 失败 / 0 错误 / 0 跳过 / BUILD SUCCESS(上批 region 1232 ⇒ +34)。 日志:logs/_split-2026-10-09/final-full-suite.log

jar

  • java/target/payment-platform-1.0.0.jar:143,152,531 字节,mtime 2026-10-09 13:37:33, sha256 8662237ebc127bb5b28b43b96d54c578036e85c96d21861e4169ddc127ee29f6 (覆盖上批 region 的 2F51689B… —— 换 jar 认这一份)
  • 回读取证(logs/_split-2026-10-09/jar-readback-batch.py,17/17 OK,直接从 jar 里读 class 常量池):
    • C:pickForOrder / ipRegionResolver / 用户 IP 无法解析出归属地 / 用户 IP 与省份编码至少填一个 / 该地区没有可用门店 / 省份编码不存在 均在场;DTO provinceCode、getIp 在场;cityCode 不在(已删)
    • B:resolveBiz / har_ali_un_stage 在场;裸 HAR 常量不在(前缀已改 AP)
    • A:notifyCount / lastNotifyTime / callbackMaxRetries / notifyEnterprise / transitioned 在场

dist.zip

  • frontend/dist.zip:9,376,648 字节,mtime 2026-10-09 13:38:56, sha256 4b87bb40d71df00401df5c78df246cf5b77b84d756d4bee6c7ccad76f491dcdf
  • 281 条目 = 277 文件 + 4 目录;0 条反斜杠条目;testzip 干净(dist-zip-verify-batch.py 打包并回读)
  • 产物串扫:/payment/openapi/alipay、/checkout/alipay/、用户 IP 无法解析出归属地、随机挑一家、首推只发一次、AP1234567890 各在场(各命中 1 个 chunk);openapi/har、没配就不会推送、HAR1234567890 零命中
  • 一个探针假阳性,如实记录:/checkout/har/ 在 1 个 chunk(index.DziFqCFC.js)命中 —— 那是 Vite 动态导入表里页面组件的目录名(views/checkout/har/index.vue,页面目录未改名),不是路由路径;路由已是 /checkout/alipay/:token(源码里 checkout/har 只作为 import 路径存在)。探针期望写错,不是代码错

前端类型检查(同机差分)

pnpm ts:check exit 2、289 条错误 —— 与基线(上批 logs/_tscheck-region-code-400.txt)逐文件分布 diff 为空:零新增、零消失(该基线本身就是非零,只能差分用)。

三、部署要求(按顺序)

  1. 先跑 SQL,再换 jar:
    • 必跑:java/src/main/resources/db/har-callback-retry.sql —— 加 notify_count / last_notify_time 两列 + 部分索引 idx_har_order_callback_retry + 存量 FAILED 封顶 notify_count=8(「存量不补推」)。列不存在时回调状态会永远停在 PENDING,捞取腿每分钟报错
    • 若以下前置批次的 SQL 还没跑过,按各自批次说明补跑:db/har-shop-region.sql(地区批)、db/har-checkout-link.sql(收款链接批)
    • 本批动过的两个 SQL(db/har-order-refund-menu.sql、db/har-checkout-link.sql)只有注释文字改动(diff 逐行核过),不必因本批重跑
  2. 换 jar + 换 dist.zip,必须同批:顾客页路径前缀 har→alipay 后端(PATH_PREFIX,jar 里)与前端路由(dist 里)各执一份,单边换 ⇒ 新链接整页 404;开放接口新前缀在 jar 里、开放文档页在 dist 里,同理。 jar 指纹 8662237e…(143,152,531 字节)、dist.zip 指纹 4b87bb40…(9,376,648 字节),见上。
  3. 无需重登、无需新菜单:后台接口路径未动,本批无新权限点、无新菜单。
  4. 验收路径:
    • 开放 API:用新前缀 /payment/openapi/alipay/** 走一单;旧前缀 404/不放行是有意的
    • 挑店:传 ip 或 province_code(city_code 误传应响亮 400);同一 out_biz_no 恒挑同一家
    • 回调:把某租户回调地址指到非 2xx / 不可达地址,观察 notify_count 每分钟 +1、last_notify_time 更新、8 次封顶
    • 收款链接:后台生成链接,URL 应为 /#/checkout/alipay/<token>,打开能进顾客页

四、未验项(如实列)

  1. 前端无执行级验证(项目无前端测试框架):本批前端改动 = 一个路由段(+注释)、两个 md 文档;证据 = 构建通过 + 产物回读 + ts:check 零新增。「顾客页真打开能付」「后台页面能开」没跑过(需登录态 / 真环境)。
  2. 挑店没在真实调用方签名请求下端到端跑过;IP 解析(外呼)只到单测层(mock)。
  3. 回调重试的真实失败→重试循环没在生产观察过(证据 = 单测 + SQL 封顶;上线后第一次真实失败时留意日志与表)。
  4. pay_url 真机扫码仍未做过(老账,承接批)。
  5. 老 checkout 链接:URL 是读时拼的(buildUrl(token, request),库里不存 URL),token 不变;但已外发过的 /#/checkout/har/ 形链接换版后会打不开(老路由段没了)。该功能此前未部署过(其 SQL 是部署前置),线上应无此类链接在流通 —— 以你的部署记录为准。
  6. third_shop_id 重复仍回 500(老账,未动,地区批已记)。

五、拍板 / 挂账(承接)

  • 收银台腿不带门店码(地区批挂账,待拍板)
  • 收款链接批:F1 限流 / F5 订单号前缀(待拍板)
  • 挑店稳定散列 seed 用 out_biz_no(已实现);调用方不传 out_biz_no 时等价于随机 —— 开放文档已写明

2026-10-09 独立评审后新增挂账(0 🔴 / 2 🟡 / 5 💭;🟡2 已随本批修复)

  • 🟡1 行为层后半:「提交成功前保留同一编号」是否改 —— 现状 = 每次打开换新(弹窗文案已如实,946a3639);要改须同时设计换店时的重新生成入口(只读格 + 无「重新生成」按钮会把想换一家店的运营卡死),待拍板
  • 短码订正 SQL 段 guard 偏宽:length(x) < 6 会把 ''/任意短垃圾(如 99)补成看似合法的六位;建议 BETWEEN 2 AND 5(异常留在 < 6,只读核对④才拦得住),未动
  • HarRegionControllerTest:90 松懈断言 startsWith("32")(88 行已另钉 length == 6,组合偏松);建议 startsWith("3200") 或 equals,未动
  • utils/shop-code.ts 注释动机(「能在电话/微信里念得出来」)与只读自动生成格不符;建议改注释真实动机,未动
  • logs/ 未被 gitignore:git add -A 会连截图/生成器/台子/日志一起收走(本仓提交一直走显式路径);建议加 ignore 条目,未动
  • 省直辖县级行政区划 4 码(419000/429000/469000/659000)截图不可见、按补零规则推;建店选到这些省前先找上游确认