Sfoglia il codice sorgente

docs(交付): HAR 批交付说明 —— 挑店改版 + 去上游化 + 回调重试(3 提交 + jar/dist 指纹 + 必跑 SQL)

- 三件事的需求原文、裁定、逐条落地范围
- 提交清单 94d120d1 / d16f6f75 / 6a0c72a6 与拆分方法(时序重放)
- 验证读数:阶段 111/112/90,全量 1266 绿;jar 8662237e… 回读 17/17;dist.zip 4b87bb40… 281 条目 0 反斜杠
- 部署顺序:先跑 db/har-callback-retry.sql(列不存在 ⇒ 回调永远 PENDING)再换 jar+dist(同批,路径前缀两边各执一份)
- 如实列未验项(前端执行级 / 挑店真调用方 / 重试真失败 / pay_url 扫码 / 老 checkout 链接)
alphaH 6 giorni fa
parent
commit
947454daa2
1 ha cambiato i file con 123 aggiunte e 0 eliminazioni
  1. 123 0
      .claude/plan/2026-10-09-har-pick-dedup-retry-交付说明.md

+ 123 - 0
.claude/plan/2026-10-09-har-pick-dedup-retry-交付说明.md

@@ -0,0 +1,123 @@
+# 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 个;本文件自身的提交不计入)。
+
+## 需求原文与裁定
+
+### ① 开放 API 挑店改版(C)
+
+原文:「开放api不用传市了,传省份或者ip,ip精确度更高所以优先级高于省份字段,两个可以同时传也可以只传其中一个,只传省份且有多个市级门店的情况下随机使用其中一个门店」
+
+裁定与落地:
+
+- IP 解析失败 / 该 IP 解析出归属地但无店 ⇒ **回退省份码继续挑**(两者都没有才报错)
+- 稳定散列(同一 `out_biz_no` 恒挑同一家);`out_biz_no` 为空 ⇒ 随机
+- `city_code` 字段**删除**;误传须响亮失败(Jackson `UnrecognizedPropertyException`,不静默忽略)
+- 400 文案三句:`省份编码不存在: 99` / `用户 IP 无法解析出归属地: 1.2.3.4` / `该地区没有可用门店: 省 32`(带市时 `该地区没有可用门店: 4301(省 43)`)
+- 显式门店码(`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` 时等价于随机 —— 开放文档已写明