2026-10-09-har-pick-dedup-retry-交付说明.md 19 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。

★ 2026-10-09 晚间补记 ② —— 开放文档再收缩(trade_type / biz 隐藏)+ 后端兜底

两个提交(写本节时 git rev-list --count origin/java..java = 16,本节提交计入则 17):

  • eaaa742b 开放文档按裁定收缩(har-openapi.md 52+/152−、harDoc.ts 注释同步):删建店整节、 auth_code / province_code / extra_params / stage_num / percent / trade_type / biz 七行、 主扫/被扫两场景表、按地区挑店说明块、与第 5 章的差异说明;响应表与回调示例同步收窄; 小节号原 10.2~10.6 → 10.1~10.5(30 处引用两向核对)
  • c07a10b6 trade_type 留空由服务端兜底 alipay_un_stage:DTO 去 @NotBlank, HarOrderService.resolveTradeType 供落库 / precreate / pay / resolveBiz 派生读同一份; 两条新测试先红后绿(null 走 precreate 腿、空串走 pay 腿)

现行产物(换这一套,覆盖上面两条补记里的指纹):

  • jar:sha256 859a6c54cea599e4f40673be32ec3f2f81a6ead2850ef6dfb0c941ab609ff666(143,153,263 字节,18:06:19)
  • dist.zip:sha256 41c1b1ac749c9708d48a86a7fc52b30a52d7c436ee60bdc16ca0b7ebd710d5b8(9,373,646 字节,18:08:11;281 条目 = 277 文件 + 4 目录项,与上一套同形)

判据(各带正对照):jar 内 HarOrderCreateDTO.class 常量池已无「交易方式不能为空」、「商品名称不能为空」仍在; HarOrderService.class 里 resolveTradeType 在。dist 内文档 chunk(index.CWrhnCFM.js)锚点「本章接口路径前缀」在, trade_type / alipay_un_stage / 「留空即可」均 0 命中,| \title` | 原样返回 |与out_biz_no命中为正对照。 后端的mvn test` 全绿读数:1268 / 0 失败 / 0 错误(控制台汇总与 100 份 surefire XML 求和两读一致)。

★ 文档从简、报文没简:开放查询响应仍回吐 trade_type(VO 里连同 stage_num / percent), 回调 content 仍带 trade_type —— 按「隐藏」裁定文档不再列它们,但字段仍在报文里; 把它们从报文里去掉属于改契约,未做。

部署步骤不变:先跑 db/har-callback-retry.sql + db/har-shop-region.sql,再 jar + dist 同批换; 无新 SQL、无重登、无新菜单。

★ 2026-10-09 晚间补记 ③ —— 轮询 10 秒 + 回调上限 3 次(用户两条裁定)

两个功能提交 + 本节一提交:

  • f2ef6b13 订单轮询间隔 60s → 10s(HarOrderPollScheduler#pollPendingOrders 的 fixedDelay / initialDelay; retryFailedCallbacks 保持 60s / 75s 不动)。只改「发货」节奏:单笔订单两次查询的间距仍由各自 next_query_time 游标控制(30s 起步退避),对上游的查询密度不变,只减少「已到期却还没被捞走」的空等。 该文件此前没有任何调度间隔断言,本次补两条钉住用例(poll=10s、retry=60s,先红后绿)。
  • 5396cb4c 回调失败重试上限 8 → 3 次(含首推 = 首推 1 + 重试 2):HarProperties.callbackMaxRetries 8→3、 db/har-callback-retry.sql 存量封顶 UPDATE 8→3(含注释与只读核对)、HarCallbackServiceTest 改钉 (schema 用例改为「提取脚本封顶值」并断言 == 默认值 == 3;retryFailed_pinsScanQuery 自定义值 3→5 —— 原值恰逢新默认值会让「写死常量」的变异不再变红、判别力归零,改 5 后变异实测红 expected [5,FAILED] but was [3,FAILED]、还原后绿)、har-openapi.md 两处 8→3。

现行产物(换这一套,覆盖上一条补记的指纹;jar + dist 同批):

  • jar:sha256 46d7cad9da2f32c8ef69d142f72f784f052f41896ddb6d6da98021b7c0139a78(143,153,266 字节,18:50:59)
  • dist.zip:sha256 1fd3d9f94483f29315f563bf36e87982360679f50f3611dd0a9868c732100fe1(9,373,683 字节,18:54:42; 281 条目 = 277 文件 + 4 目录项、0 反斜杠、testzip None)

判据(各带正对照):jar 内 javap -v 回读 —— pollPendingOrders 注解 fixedDelay=10000l, initialDelay=10000l、 retryFailedCallbacks 仍 60000l / 75000l;HarProperties 构造器 iconst_3 → putfield callbackMaxRetries (正对照:connect / read timeout 5000 / 15000 原样);jar 内嵌 db/har-callback-retry.sql 已是 notify_count = 3 / < 3。 dist 内文档 chunk(index.C6vqgUS8.js)「3 次(含首推)」在、「8 次(含首推)」0 命中、正对照「本章接口路径前缀」在; 与工作区旧 zip(sha 前缀 83f4536f…,9,286,480 字节,18:13 出现 —— 非本方会话产物,内容属 18:08 构建的另行重打包) 规范化逐条目比:唯一实质差异就是该文档 chunk(72714 → 72714 字节,前缀 35679 / 后缀 35967 相等,中段即两处 8→3)。

读数:全量 1270 / 0 / 0 / 0(上批 1268 + 两条新钉住用例;控制台汇总与 100 份 surefire XML 求和两读一致)。

部署:已跑过 har-callback-retry.sql 的环境无需重跑(列与索引已在;脚本新值 3 供生产首次执行 / 清库重跑); jar + dist 同批换、无重登、无新菜单。★ 若部署环境显式配过 har.callback-max-retries(application.yml / 启动参数), 配置会覆盖 Java 默认值 —— 需一并改 3。乐收赢 leshouying.downstream-notify-max-retries 仍为 8,未动(不在裁定范围)。 验收订正:§三 第 4 条(回调)的封顶数按 3 次观察 —— 正文 §③ / §三 里写的 8 为本轮之前的写法。

push 状态订正:写本节时 origin/java = 41e6c634(此前提交已全部 push,含 18:11 的 application.yml 提交); 本轮 3 个提交(f2ef6b13、5396cb4c、本节)待你 push。

需求原文与裁定

① 开放 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 🟡:🟡1 已裁定维持现状、🟡2 已随本批修复;5 💭 挂账)

  • 🟡1 行为层后半「提交成功前保留同一编号」—— 已裁定:维持现状,不做行为修法。依据(用户 2026-10-09 委托决策,约束原文「不能影响交互逻辑和业务逻辑」):三类候选修法均越线 —— 粘住编号 = 改弹窗打开后的展示(交互),加「重新生成」按钮 = 新增控件(交互),后端拒绝重复建店 = 新增拒绝规则(业务)。文案层修复(946a3639)已消除缺陷实质:旧承诺「重复提交会被后台拒绝」是假话,现已如实。当前生产 kf 建店路由尚未开通(2026-10-09 实测 404,上游侧待开)⇒ 上游无法建出隐藏门店,生产无敞口;路由开通后残余风险 = 运营在「待核实」后主动重开重提,弹窗文案已明示「请勿」。若日后仍要该行为,须作为独立需求(含换店逃逸口)走流程
  • 短码订正 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)截图不可见、按补零规则推;建店选到这些省前先找上游确认