yeg
2026-09-18 23d07a8ece3e8a6f7b501bd38541b1d4bb191419
chore(scratch): 交接过程留档:坑清单、计划与明文凭据工单

pitfall-checklist.md 96 条坑的原始素材(每条带 commit/文件:行 证据),06 的出处
plan.md 交付计划、写作约定、以及交付前自检的实际结果与和计划的偏离
issues/01-...md application.yml 四个凭据键是明文且已入库,两阶段建议改法

凭据那条本次不动手:改占位只是止血(私钥已进 git 历史,要做的是按流程轮换),
且 application.yml 是全仓共享文件,不该夹在文档交付里改。
3个文件已添加
524 ■■■■■ 已修改文件
.scratch/huifu-handover/issues/01-plaintext-credentials-in-application-yml.md 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/huifu-handover/pitfall-checklist.md 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/huifu-handover/plan.md 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/huifu-handover/issues/01-plaintext-credentials-in-application-yml.md
New file
@@ -0,0 +1,92 @@
# 01 — 汇付凭据在 `application.yml` 里是明文,且文件已入库
**Status:** needs-triage — 只记录,本次不动手
**Blocked by:** 无(但第二阶段需要汇付侧配合轮换)
**发现于:** 2026-09-18,写支付链路交接文档时
---
## 现象
`talent-card-server/src/main/resources/application.yml` 里,`app.payment.huifu` 块下**四个最敏感的键是字面量明文**,而这些值随文件进了 git 历史:
| 行号 | 键 | 状态 |
|---|---|---|
| 85 | `app.payment.huifu.system-id` | 字面量明文 |
| 87 | `app.payment.huifu.merchant-id` | 字面量明文 |
| 89 | `app.payment.huifu.private-key` | 字面量明文(RSA 私钥) |
| 90 | `app.payment.huifu.platform-public-key` | 字面量明文 |
同一份文件里**另有 42 处**使用 `${...}` 环境变量占位——连 `onboarding-upper-huifu-id`(第 111 行)都是占位。也就是说:**两种做法在同一个配置块里并存,恰好把最敏感的四个键留成了明文。**
另外该块 `enabled: true`、`environment: prod`,指向真实通道。
## 根因
不是"有人故意写死",而是**默认值填进去之后就没人回头改成占位**:`HuifuPaymentProperties` 自带 Java 默认值,`application.yml` 里的键最初是把真值直接写在默认位置上让联调能跑;后来新增的键(如 `onboarding-upper-huifu-id`、`merchant-use-chains-flag`)都自觉用了 `${ENV:default}` 形式,**存量那四个没被一并改**。
`.gitignore` 也没有覆盖它——它本来就是仓库文件,不该被忽略。
## 证据
| 事实 | 取证方式 |
|---|---|
| 四个键是字面量、非占位 | `grep -nE '^\s+(private-key\|platform-public-key\|system-id\|merchant-id):' application.yml`,逐行判是否含 `${` |
| 同文件 42 处使用占位 | `grep -c '\${' application.yml` |
| 文件被 git 跟踪 | `git ls-files` 命中;`git status` 里它处于 modified |
| **没有** `.pem`/`.p12`/`.key` 之类文件入库 | `git ls-files \| grep -iE '\.(pem\|p12\|key\|jks)$'` 为空 |
| 已有环境变量覆盖机制可用 | 注释里出现 `HUIFU_RECONCILIATION_FIXED_DELAY_MS` 等覆盖键,说明 `${ENV:default}` 组合是被接受的写法 |
| 测试曾打印明文私钥 | 交接坑清单第 92 条:`HuifuSignatureServiceTests` 会打印运行配置,后改为使用 `keySummary` 摘要函数 |
## 影响
- **私钥已进 git 历史**:任何拿到仓库(现在或将来)的人都能对汇付侧伪造请求签名。改占位**不等于止血**,只是防止继续扩散。
- 平台身份对目标商户有数据权限(这是 `20002` 排查结论的一部分),因此泄露的私钥能实际操作商户数据。
- 仓库是多人按业务域协作的,历史里还有合并、fork、worktree,**无法确认私钥副本的分布范围**。
## 建议改法(两阶段,别合并成一步)
### 第一阶段:止血(小、可独立评审)
把四个键改成占位,与同文件其他 42 处一致:
```yaml
      system-id: ${HUIFU_SYSTEM_ID}
      merchant-id: ${HUIFU_MERCHANT_ID}
      private-key: ${HUIFU_PRIVATE_KEY}
      platform-public-key: ${HUIFU_PLATFORM_PUBLIC_KEY}
```
- `.env.example` 里补齐这四个环境变量名(现在只有 `HUIFU_PRIVATE_KEY`、`HUIFU_PLATFORM_PUBLIC_KEY` 两个)。
- 部署侧同步注入,**确认线上非空**后再合并——占位改成 `${VAR}` 而部署没配,启动就会因为空值报错或直接用空串签名。
- 因为 `application.yml` 是 `AGENTS.md` 点名的**全仓共享文件**,这一改要单独一个工单、单独一次评审,不要夹在别的任务里。
### 第二阶段:轮换(必须做,且需要汇付侧配合)
**明文进过 git 历史,改占位不能撤销泄露。** 要按流程申请新的 `system-id` / `merchant-id` / 私钥 / 平台公钥,并在控台重新绑定回调地址。
前置条件:
1. 先拿到汇付侧的轮换窗口与操作顺序(换凭据期间旧凭据还能用多久)。
2. 确认 `onboarding-upper-huifu-id` 与渠道商关系是否随换号变动。
3. 准备回滚方案——换号期间支付与进件回调都可能中断。
### 长期
- 加一条提交前检查:`application.yml`/`*.yml` 里 `private-key`/`platform-public-key` 的值只要不以 `${` 开头就拒绝提交。
- 日志与报错出口统一脱敏(坑清单第 92 条已暴露过一次同类问题)。
## 本工单不做的事
- **不**在本工单里改 `application.yml`(正在写交接文档,改共享文件会引入无关变更)。
- **不**在本工单里做凭据轮换(需要汇付侧配合与停机窗口,不是代码改动能解决的)。
- 交接文档 `docs/huifu/08-上线与环境.md` 里会正面写清「凭据该从哪来、为什么不能写在 yml 里」,与工单互相引用。
## 待确认
1. 这四个键在**生产部署**里现在是怎么注入的?是直接用文件里的值,还是部署脚本覆盖了?——如果部署侧本来就在覆盖,第一阶段的风险比想象的低。
2. 这个仓库有没有对外分发过(fork / 外包 / 打包给第三方)?决定轮换的紧急程度。
3. 汇付侧换凭据的操作窗口多长、期间旧凭据的宽限期多久?
## Comments
- 2026-09-18 — 建工单。写支付链路交接文档时做凭据自检发现,同一条自检已加进交付前检查脚本。
.scratch/huifu-handover/pitfall-checklist.md
New file
@@ -0,0 +1,241 @@
# 汇付/斗拱支付链路 —— 踩坑清单确认表
**用途**:这是交接文档 `06-踩坑清单.md` 的原始素材,需要你确认后才能成文。
**怎么用**:只看最后两列。**证据列我已经核过**(commit / 文件:行 / 注释原文),你不需要回头看代码。
你只需要在 **❓待确认** 列回答,或者直接在 **✓** 列打 `[x]` 表示「这条对,照写」。
**措辞约定**(成文时严格执行,不把推断写成结论):
| 标记 | 含义 |
|---|---|
| **已验证** | 有真实通道回执、真实提交,或开发库实测数据 |
| **推断** | 有代码证据但没有实测,逻辑上成立 |
| **未确认** | 只在我们这边成立,汇付侧没有确认过 |
| `[待确认]` | 我答不上来,需要你补一句 |
**统计**:96 条,按链路分 8 组,组内按「会动钱 > 卡死流程 > 体验/呈现」排序。
---
## A. 进件(9 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 1 | [ ] | 通道侧配置失败的商户,进件页永远停在「审核中」,表单进不去、同一张卡也用不了,商户彻底卡死 | 进件页见 `pending/approved` 就跳待审页;`submitOnboarding` 见 active 进件直接返回旧记录;银行卡校验又拦「该银行卡已有审核中的进件」 | 已验证 · `e3efdaa`;`MerchantSettlementService.java:880-893` | 生产/联调环境里 `status=pending` 且两步都 `failed` 的进件现在有几条?要不要脚本收口成 rejected? |
| 2 | [ ] | 进件放行了,但商户用同一张卡重提仍被拒「该银行卡已有审核中的进件或已通过的申请」 | 收口只把进件标 rejected,漏了它绑定的 `merchant_bank_card`(卡停在 pending 本来就是这条失败进件留下的) | 已验证 · `0d9afc5`;`MerchantSettlementService.java:897-918`(注释:「否则商户连同一张卡都不能用——实际就踩到过这个坑」) | 控台里那批 rejected 进件对应的银行卡现在什么状态?还有 pending 卡挂在 rejected 进件上吗? |
| 3 | [ ] | 第一次提交进件就 500,报 `Unknown column 'license_validity_type'` / `subject_cert_end_date` | 代码 INSERT 写了这些列,V80/V99/V127/V128 都没建;此前测试用 JDBC 直接造进件记录,**绕过了真实入口** | 已验证 · V137 头注释(「工单 06 的端到端验收才把它暴露出来」)、V138 头注释 | 除了这两列,进件链表还有别的列只被手建、没进迁移吗?要不要跑一次「代码列名 vs 迁移列名」全量比对? |
| 4 | [ ] | 个人(小微)走完进件也拿不到微信收款能力,微信开通与实名都缺材料 | 门店照片(F22 门头照 / F24 内景照)**一个字段都没收**;官方在业务开通 `micro_biz_type` 与实名 `store_header_copy`/`store_indoor_copy` 两处都点名要 | 已验证 · `9553a90`;V146 头注释;`.scratch/merchant-individual-onboarding/audit.md` 三、1 | 三种小微经营类型(线上商品服务/流动经营/门店场所)各该传什么图,业务侧书面定稿了吗? |
| 5 | [ ] | 已拿到真实汇付号的存量个人进件一次业务开通都没做过,也没有补图入口 | 开发库实测:`subject_type=individual` 10 条、3 条有 huifu 号,**全部 10 条 `business_open_status=not_started`**;分阶段模型下重复提交不新建进件,商户没有别的补图入口 | 已验证 · `audit.md` 四之二(只读统计原文);`supplyMissingStorePhotos` | 这 10 条里除 3 条有商户号的,其余 7 条要不要清理?已开出的商户号控台还都「正常」吗? |
| 6 | [ ] | 商户端换账号登录,进件页弹出「已恢复上次未提交的草稿」,把**上一个账号**的身份证号、银行卡号预填进来 | 草稿存在固定全局 key,uni storage 不分账号,`restoreDraft()` 不校验归属 | 已验证 · `c7c19c5`(原文:「这不是体验问题,是把一个账号的敏感资料泄露给了下一个账号」) | 线上有商户用过「换账号登录」吗?旧全局 key 下的草稿要不要在发布时强制清一次? |
| 7 | [ ] | 进件能过、商户建得出来,但平台对它调 config/open/query 一律 `20002 数据权限认证失败` | 曾以为是归属错,改用 `head_office_flag=0`+`head_huifu_id` 建成平台下级,**实测无效**:20002 与层级无关,取决于签名身份本身的数据权限;head 路径还额外索要 F725/F726/F727 | 已验证 · `7dfc5b7`(第一版)→`e89a164`(回退);`HuifuPaymentProperties.java:21-35` | 最终定的是「渠道商 → 普通商户」两级,对吧?这个结论是**找汇付客服确认过**,还是只靠自测推断的? |
| 8 | [ ] | 企业进件通过后,微信配置与业务开通两步**一次都不会被触发**,商户永远停在 `not_started` | `applyOnboardingResult` 把「拿到银行卡序列号 `token_no`」当推进后续配置的前置;而企业进件的同步响应常常只回 huifu_id 和 apply_no | 推断 · `39a5163`(注释原文:「企业进件的同步响应常常只回…于是后续两步一次都不会被触发」) | 真实企业进件的同步响应里 `token_no` 到底会不会回?还是只有绑卡回调才给? |
| 9 | [ ] | 商户在进件表单里选的行业没人读,上报 MCC 一律落到「其它(7399)」,微信行业类目与费率跟着错 | 表单把行业塞进 `companyType`,而个人进件 MCC 取的是商家档案 `merchant_type_code`;后续又发现取值优先级反了(表单盖过档案) | 已验证 · `9553a90`、`667abfb`;`MerchantOnboardingIndustryMccTests`(5 项) | 「行业改不了、只有系统动态匹配」是最终口径吗?商户侧要不要加一句「行业由平台维护」? |
## B. 业务开通与实名(16 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 10 | [ ] | **任何新商户**(企业、个人都一样)都开不出微信收款能力:微信支付、人才端绑定、实名全部连坐 | 「微信小程序配置」排在「商户业务开通」**之前**,而微信子商户号是业务开通创建的,配置必被驳「根据渠道号或微信子商户号未查到入驻记录」;配置一失败就 `return`,业务开通永远轮不到 | 已验证 · `9553a90`;`audit.md` 七之二(含申请 452 的库内证据);`MerchantSettlementService.java:571-572` | 之前用存量/参考商户验证时全被掩盖(它们的微信业务早开通了)——现在有没有真的用一个**全新**商户从零走通过? |
| 11 | [ ] | 业务开通报 `wx_conf_list格式错误`(第一层) | 小微的 `mcc` 写在 `fee_rule_id` 的 `else` 分支里,平台配了默认规则号 756 之后小微的 mcc **一次都没发过**;代码注释还写着「个人主体属于小微商户,所以必须带上」 | 已验证 · `9553a90`;`HuifuMerchantBusinessOpenGateway.java:92-93` | 个体工商户走企业接口但对私结算,它的 `mcc` 是否也必填?(测试里是必填) |
| 12 | [ ] | 换号后**仍是**同一句 `wx_conf_list格式错误`(第二层) | `wx_real_name_info` **双重编码**:官方字段目录类型列是 Object,代码又 `JSON.toJSONString` 编了一层,通道按对象取字段取不到;网关自己的 javadoc 写着「父字段列 + 同名子表的写法约定即为嵌套对象」,**实现与注释不一致** | 已验证 · `audit.md` 七之二「第三次失败」;`HuifuMerchantBusinessOpenGateway.java:112` | `material_card_info` 是官方勘误的例外(类型列写 Object 但要按字符串发)——这条勘误名单还有别的字段吗? |
| 13 | [ ] | 业务开通报「微信费率规则号填写不正确,请检查!」 | CID 按「支付场景 × 业务开通类型 × **主体类型**」三维定位:企业 756 / 个体户 761 / 小微 775,不能全平台共用一个号 | 已验证 · `HuifuPaymentProperties.java:52-62`;`application.yml:121-128`;`HuifuBusinessOpenRequiredFieldsTests` | 事业单位 773 / 社会组织 774 会不会有商户出现?要不要一起配上可覆盖的键? |
| 14 | [ ] | 六项阶段里 5 项绿,「微信支付」永远未确认;`sync_error` 是空的(查询其实成功) | 判定要求 `fee_min_amt` 必须出现且为 0,而该字段可选、文档写明「大于 0」——不设最低收费时我们不下发、通道也不返回,缺失被判成未生效 | 推断 · `MerchantOnboardingProgressService.java:85-89`(注释:「缺失必须按 0 处理,否则这条判定永远不成立」) | 这个「缺失 == 0」的语义要不要向汇付**书面确认**一次? |
| 15 | [ ] | 商户端没有授权二维码,而「本人扫码授权」是完成实名的唯一途径 | `qrCode()` 写死 PNG 的 base64 magic(`iVBORw0KGgo`),通道回的是 **JPEG**(`/9j/`),被判非法返回空串;体积与字符集都过关,**只有格式假设错了** | 已验证 · `9553a90`;`MerchantOnboardingProgressService.java:425-441`;`MerchantRealnameServiceTests` | 通道回码的格式是否稳定为 JPEG?会不会有 PNG/其他格式混用? |
| 16 | [ ] | 已授权成功的商户(`authorize_stat=1`)被页面显示成「待办理」,二维码还在让人去扫 | 授权判定要求 `APPLYMENT_STATE_PASSED`;而真实样本是 `APPLYMENT_STATE_FREEZED`(「该主体已完成过入驻」)+ `authorize_stat=1`。授权完成**只能看 `authorize_stat`** | 已验证 · `7baa02d`;`HuifuMerchantRealnameGateway.java:26`;`evidence-2026-09-15.md`(参考商户真实样本) | 授权成功后通道仍回传二维码、前端按状态收起——这个「有码但已授权」的组合还会出现在别的场景吗? |
| 17 | [ ] | 实名发起被驳回,且**驳回后要换业务申请编号重来** | 实名接口的 name/mobile/id_card_number 三项都必须是**法人本人**的,而进件只收了联系人手机号——实测那张商户的联系人是经办人、法人是另一个人 | 已验证 · V145 头注释;`MerchantOnboardingRequest.java:32-33`;`MerchantRealnameService.java:131` | 存量进件里有多少条 `legal_mobile` 是空的、或误填成联系人号码? |
| 18 | [ ] | 实名路径选不了经办人 | 经办人(`contact_type=SUPER`)额外要求经办人身份证正反面 + **加盖公章的《微信业务办理授权函》**,盖章件无法自助提交 | 已验证 · ADR-0001 2026-09-16 修订;`HuifuMerchantRealnameGateway` | 将来有商户坚持走经办人,平台是否接受人工代传盖章件? |
| 19 | [ ] | 无回调的部署里整单**永远完不成**,能力快照也永远不刷新,商户明明开通了却推不到收款就绪 | `needsProvisioningStep` 只认 `blank/not_started/failed`,**不含 `processing`**,而 `approved` 正是能力刷新与整单完成的判据 | 推断 · `9553a90`;`audit.md`「顺带修掉的:processing 永远不会被推进到 approved」 | 除了 `processing`,还有别的中间态会被同样的条件吃掉吗? |
| 20 | [ ] | 业务开通申请被受理了,紧接着发微信配置仍被拒「未查到入驻记录」 | 配置的闸门原取**本地状态**(`processing`),而**微信入驻是异步的**:`90000000` 只表示申请被受理,那一刻通道侧还查不到子商户号。闸门必须取通道证据 | 已验证 · `audit.md` 第五步;`MerchantSettlementService.java:791` | 微信入驻从受理到子商户号可查,一般要多久?商户侧要不要给「预计 X 分钟后刷新」的提示? |
| 21 | [ ] | 业务开通/微信配置被驳回后重试,通道当成重复请求 | 重试复用同一流水号;通道按流水号去重。修「换号」时踩了第二个坑:随机后缀必须保住,先按剩余宽度裁前缀再接后缀,否则超长截断会把随机位一起切掉,两次重试又变成同一个号 | 推断 · `MerchantSettlementService.java:847-853` | 通道对「同号重提」是拒还是幂等返回原申请?有真实回执吗? |
| 22 | [ ] | 业务开通被拒「失败:复用交易信息,上级商户号为空」 | `use_chains_flag=3` 是「复用上级微信商户号」,要求上级**本身有商户级微信配置**;我们的上级是渠道商(查它自己的微信配置直接返回「huifuId不存在」) | 已验证 · `c18b9fa`;`application.yml:115-119`;`HuifuPaymentProperties.java:37-45` | 这条结论有没有找汇付确认过「渠道商不能做 AT 复用」?还是纯自测推断? |
| 23 | [ ] | 业务开通被拒「【media_merCommon_flag-媒介主体与商户主体是否一致】不能为空,或格式非法」 | `media_mer_common_flag` 是 `online_media_info_list` 的**元素字段**,不是顶层参数;之前那次「补上顶层字段」的修复**从来没被验证过** | 已验证 · `c18b9fa`;`evidence-2026-09-15.md`「本轮修掉的三个真实缺陷」1 | 平台一个运营媒介服务多个商户,声明 N 会连带索要授权/开发证明材料——将来开其他业务时这材料怎么拿? |
| 24 | [ ] | 已开好的商户在两端页面被显示成「失败」 | `90000000`(审核中)被业务开通网关当成失败,会把一次成功受理记成 `business_open_status=failed`;两个进件网关都有这个判定 | 已验证 · `c18b9fa`;`evidence-2026-09-15.md` 缺陷 3 | 除了 `90000000`,还有哪些「受理类」返回码在别的网关上漏了?要不要做一次全网关返回码盘点? |
| 25 | [ ] | 收款就绪门槛一旦单独落地,**所有没有进度记录的存量商户都收不了款** | 门槛要求「微信开通 + 实名/授权」双 confirmed,而 `confirmed` 依赖分阶段进度记录;存量商户必须先刷新过进度才可能通过 | 推断 · `4ef9418`;`7baa02d` 票面「收款就绪门槛暂不落」;03 票 Comments(3) | 存量商户要不要给一次批量进度刷新,还是明确告知必须逐个刷? |
## C. 收款能力与闸门(10 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 26 | [ ] | 延迟入账上线后,所有新支付都被闸门拦住,报「自动结算尚未关闭」 | 「自动结算必须关闭才能收款」是延迟分账之前立的规则;延迟户已兜住未核销的钱(官方定义:不可结算、不可取现),这条规则不但多余,还把支付路径直接堵死 | 已验证 · `723b756`(提交正文);`MerchantPaymentReadinessService.java:252` | 闸门里还有别的「遗留规则」在偷偷拦支付吗?要不要做一次闸门前提全盘 review? |
| 27 | [ ] | 进件页标题说「请按待办理项补齐缺失能力」,但六项阶段全绿,一个待办理项都没有 | `complete()` 还要求 `bank`(结算卡核实)与 `merchantNormal`(通道商户状态),这两项既不在 `LABELS` 里、`view()` 也不返回 | 已验证 · `.scratch/merchant-closure-impact/issues/huifu-merchant-closure-impact.md` 遗留一;`MerchantOnboardingProgressService.java:298-343` | 这两个完成条件的文案(「请联系汇付或后台处理」)需要产品再定一次措辞吗? |
| 28 | [ ] | *(改法风险)*把两项核实加进阶段列表,进件会**永远无法 approved** | `LABELS` 同时是展示列表与完成判定的键集,且 `bank`/`merchantNormal` 是布尔值、其余六项必须是字符串 `"confirmed"`——加进去会两次破坏完成判定 | 已验证 · ADR-0002「考虑过但没选的」;`MerchantOnboardingProgressService.java:306-308` | 将来有人想给进件页加第 7 项,这条约束要不要写进 `AGENTS.md` / `CONTEXT.md`? |
| 29 | [ ] | 汇付控台关闭商户后,进件页依旧显示「进件已完成 + 六项全已确认」,商户其实已收不了款;感知完全依赖「下一次下单被拒」 | `approved` 之后商户端「刷新进度」按钮被隐藏,而全仓四个 `@Scheduled` 没有一个刷进件进度快照 | 已验证 · `huifu-merchant-closure-impact.md` 证据节 + 遗留二 | 「下单被拒」的 toast 文案要不要也改成能自解释的?(closure-impact 文档里这条被列为下游) |
| 30 | [ ] | 被通道关停/从没核实过通道状态的商户,能力快照最长有约 **6 小时空窗**,期间我们仍认为可收款 | 能力定时核实 30 分钟一轮、只扫超过 `stale-hours`(默认 6)未核实的申请;空窗内的失败推迟到真正发起支付时才由通道拒绝 | 已验证 · `huifu-merchant-closure-impact.md`;`application.yml:143-146` | 6 小时这个值是谁定的?生产上要不要缩短(代价是通道查询量)? |
| 31 | [ ] | 「立即核实」按钮可以被连点;冷却若改用 `capability_verified_at` 会导致失败时无节流 | 查询失败时 `capability_verified_at` 不推进,等于没节流;`updated_at` 是 MySQL `on update current_timestamp`,任何无关 UPDATE 都会顺带重置它 | 已验证 · ADR-0002「考虑过但没选的」;V151 头注释(原文:「不放在 capability_verified_at:查询失败时那一列不推进,会被连点」) | 冷却默认 5 分钟(与进件刷新 5 分钟租约同量级),够用吗? |
| 32 | [ ] | 一进「收款能力」页就 500,报 `Unknown column 'capability_sync_until'` | 开发库 flyway 关闭、新列靠初始化器补,而那条 alter 没成功(等 DDL 元数据锁上限 5 秒);且 `capabilityCooldownSeconds` 是本文件里唯一点名列的查询,缺列会让整条 SQL 报错 | 已验证 · `20e19d8`;`docs/agents/testing.md:87-89` | 别处还有多少「点名列的查询」会在缺列时把整页打成 500?要不要统一改成从已读到的行里算? |
| 33 | [ ] | 冷却倒计时只在重新进页面时才恢复;没查通道也说「刚核实过」 | 第一版只在重新进页面时判冷却到期,且 `refreshed=false` 时也宣称刚核实过;进件未通过时把按钮写成「稍后可再次核实」(那根本没有冷却) | 已验证 · `1435780` | 「暂不可核实」和「冷却中」这两种不能点,文案区分够清楚吗? |
| 34 | [ ] | 结算记录页每次打开都白多几条失败提示(被关停的商户逐条报「客户与产品关系已关闭」) | 页面此前把「进件通过且有子商户号」的商户**全都**问一遍,包括被关停的和从没核实过通道状态的 | 已验证 · `8e503e2`(原文:「实测通道回『客户与产品关系已关闭』」) | 结算记录监控的商户范围,业务上就应该是「通道侧收款能力正常」这一档吗? |
| 35 | [ ] | 商户端页面说能收款、下单却被拒(同一个事实两套口径) | 页面判定与下单闸门读了不同的数据;ADR-0002 定为「同源同文案」 | 已验证 · ADR-0002;`MerchantSettlementService.java:100-101`;测试 `merchantViewReusesTheGateMessage` | 这套「同源同文案」要不要在 `CONTEXT.md` 里写成硬规定? |
## D. 支付(18 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 36 | [ ] | 关掉微信收银台后再点「确认支付」和「取消订单」都只报「支付结果确认中」,订单从放弃那一刻起就是死的,直到通道改口 | 三处叠加:① 重付先用一次通道查单当闸门,通道对**未付款**的预支付单答「处理中(P)」,代码把 P 判成「不确定」→ 抛错(而本地其实存着那张单的支付参数);② 取消同样以查单为前置门,查单答 P 时**连关单接口都不调用**;③ 到期清理刻意排除带未定局流水的订单,所以永远不会自动到期 | 已验证 · `.scratch/pending-order-payment-flow/issues/01-pending-order-deadlock.md`(含用户截图与 `file:line`);ADR-0003;`8fe2249`、`e3f412c` | 「P 判成不确定」这个规则当时是谁定的?有没有想过未付款的预支付单在通道侧就是 P? |
| 37 | [ ] | 用户没付过钱但订单被取消,钱却在通道被收下,人才端显示「已取消」、**连退款入口都没有**(退款要求订单已支付) | 本地取消/超时只把流水置 `LOCAL_CLOSED`、订单置取消,**从不调通道关单**;用户手上那张预支付单照样能付进来 | 已验证 · `8fe2249`(真实数据 2026-09-16 14:22 一笔 ¥0.02 订单就是这样被取消的);审计 §8 | 那两笔(订单 520/521)以外,还可能有别的同类悬账吗?要不要按同一口径再全库扫一次? |
| 38 | [ ] | 反复「继续支付 → 关掉收银台」后,微信弹「订单状态异常,支付请求无效」,只能等旧单真被取消才能在同一商品上再下一单 | 关单回执 `23000000`(原文「原订单已为终态,无法发起关单操作」)被并入「其余拒绝」,于是那张**已经不可能被支付**的预支付单被记成 `CANCEL_UNCONFIRMED`(= 关不掉 = 可能还能付)并继续被 `reusablePrepay` 交给客户端 | 已验证 · `3fd8f1e`;`issues/09-terminal-prepay-zombie-order.md`(含订单 534 库内证据:`close_attempts=3`、`provider_response_code=23000000`) | `10000016`(「原订单已为终态,请发起查询交易获取」)也是同一支吗?两个码的差异要不要再向汇付确认? |
| 39 | [ ] | 一笔订单变成「既付不了、同一商品也再下不了单」的僵尸,永远不会被自动收掉 | `giveUpClosing` 只在「当前状态是 `CLOSING`」时才结束订单;第一次关单就被拒的流水直接落 `CANCEL_UNCONFIRMED`,到上限既不结束、也不被到期清理收掉 | 已验证 · `3fd8f1e`;`issues/09` 根因 3 | 除了 `CANCEL_UNCONFIRMED`,还有别的状态会掉进「既不在排除表、也不在结束路径」的缝里吗? |
| 40 | [ ] | 点「继续支付」**没有任何反应**(按钮被禁),而那张凭证其实还活着 | 复用集合砍过头:`CLOSING` 是「关单可能已生效」,而 `CANCEL_UNCONFIRMED` 是「通道明确说这次关单没生效」(拒绝关单、或关单请求本身失败 `90000000 交易失败,请重试`)——后者凭证还活着,正是最该复用的一张 | 已验证 · `06db42a`;`HuifuPaymentService.java:106-125` | 「可复用集合」口径已经改了四轮(`PREPAY_CREATED` → +`CLOSING` → 只留 `PREPAY_CREATED` → +`CANCEL_UNCONFIRMED`)——要不要在 `CONTEXT.md` 里定死成不变量? |
| 41 | [ ] | 关单明明几秒就完成,用户却在前台等了 **72 秒** | 汇付有专门的「关单查询」接口(文档写明「关单处理中应继续调用关单查询确认结果」),我们只发关单、不问结论;副作用是快车道每次重发关单都让 `close_attempts` 增长,一条正常记录会被判到上限,而「到上限」会顺手取消订单 | 已验证 · `c4baf2f`;ADR-0004「待补的三处查询对等物」;`HuifuPaymentCloseQueryCommand` | 关单查询的结论只有「关单成功」算数;「关单失败」与「查询没答上来」一律按没有结论——这个保守口径认可吗? |
| 42 | [ ] | 「关单重试每分钟一次、最多三次」实际是「每五分钟一次、最多十五分钟」,用户等的那一分钟里没有任何重试发生 | `CLOSE_RETRY_MINUTES=1` 只是「距上次处理满一分钟」的**资格窗口**,扫描本身由 `fixed-delay-ms` 决定,默认 300000 | 已验证 · `c46ef64`;`application.yml:95-107`;`PaymentSchedulingConfigurationTests` | 还有别的「资格窗口 vs 扫描周期」被同样混淆的地方吗?(比如慢车道 `pending-after-minutes` 与 `last_reconciled_at`) |
| 43 | [ ] | 用户点了取消、页面关了,取消也丢了;订单仍是「待支付」,只能再点一次或等 15 分钟 | `cancel` 在关单没结论时只回一个 `cancellationPending`,意图没有落库;而关单**后来真关掉**时没有任何人去收那笔订单(`giveUpClosing` 只在重试到上限时跑) | 已验证 · `c46ef64`、V151/V154 头注释(「取消接口直接以 Unknown column 500」) | `benefit_order.cancel_requested_at` 现在降级为审计事实(`0c8ea84` 把消费它的扫描删了)——这一列还留着做什么用?将来要接审计报表吗? |
| 44 | [ ] | 点了取消一直卡着;实测**三次取消全部**拿到「关单处理中」 | 通道对**刚创建的微信交易**最常见的答复就是「关单处理中」,叠上对账扫描周期,而用户根本没付过钱 | 已验证 · ADR-0003 2026-09-17 修订;`0c8ea84` | 「三次全中」这个样本量够不够下结论?要不要用更大样本再测一轮(例如故意做 20 次)? |
| 45 | [ ] | 偶发「用户付了钱又被自动退回」,且**手续费大概率不退** | 取消改成「查单确认没收款就立刻生效」,要求用户正好在「查单之后、凭证被关掉之前」那几秒完成支付 | 已验证 · ADR-0003 2026-09-17 修订(代价段);`0c8ea84` | 这个代价有没有和财务/法务对齐过?手续费损失由谁承担、要不要给个上限告警? |
| 46 | [ ] | 待支付页**每秒闪一次**(内容被反复换成「正在加载订单」);「刷新支付状态」看着没反应 | 客户端 `loadOrder ↔ checkPaymentStatus` 互相触发:`checkPaymentStatus` 读到 `CANCELLED` 就重读订单,而读订单又会查一次支付状态——无限循环 | 已验证 · `3fd8f1e`;`issues/09` 根因 4 | 客户端没有可编译的反馈回路(本机没有 HBuilderX / uni CLI)——这类环路要怎么在仓库里防住? |
| 47 | [ ] | 点「确认支付」不拉起微信支付,而是跳到另一个页面再点一次同名按钮 | 设计如此,但按钮名与行为不符;且支付结果轮询只有 5×1.5s=7.5s,且只在 `requestPayment` 回调 success 后才开始,回调稍慢就把用户推去「使用记录」自己找 | 已验证 · `issues/06`、`issues/README.md` B3;`e3f412c` | 现在退避轮询到约 45 秒 + 手动「刷新支付状态」,这个上限够吗? |
| 48 | [ ] | 「关不掉的预支付单」所在订单被自动取消,而那张单子**仍然可付** → 钱落在已取消订单上 | `expireDueOrders`/`expireOrder` 的排除表只列了 `CREATED/PREPAY_CREATED/CREATE_UNKNOWN`,**漏了 `CANCEL_UNCONFIRMED`**;钱进来后记 `PAYMENT_ORDER_CANCELLED`/`CLOSED_LATE_SUCCESS`,而正常退款接口要求 `order_status=1`,这笔钱没有程序化出口 | 已验证 · `issues/README.md` C2/C3;`04-payment-converges-per-attempt.md` 要点「到期与自动取消」 | 老数据里还有多少条 `PAYMENT_ORDER_CANCELLED` 是这种形态?现在都清了吗(05 票说开发库已 0 条)? |
| 49 | [ ] | 退款/排查失去唯一的通道凭证(开发库 refund 43/44 的 `provider_refund_no` 都是空) | 支付查询回执里通道流水号的字段是 `org_hf_seq_id`;取 `hf_seq_id` 会把它**静默写成 NULL**。`hf_seq_id` 只出现在异步通知里 | 已验证 · `85831da`;`PaymentReconciliationService.java:610-611` | 还有别的「字段名在查询与通知里不一样」的通道字段吗?要不要做一份字段对照表? |
| 50 | [ ] | 0.01 元的成功回调可以把 100 元订单改成已支付(验签是模拟通过的,不是绕过) | 回调只查流水 ID、订单 ID、状态,**不取金额比对**;提现回调也未核对金额、汇付账户、银行卡关联 | 已验证 · 审计 §10;`d879291`(回调与主动查询共用 `PaymentSettlementService`,校验金额/商户号/订单归属) | 现在金额与收款子商户都校验了——币种、通道流水、原交易关系要不要也一起校验? |
| 51 | [ ] | 漏回调后由主动补查成功的到店订单长期不能计入可提现 | `PaymentReconciliationService` 更新支付与订单,但没像回调那样置 `STORE_CONSUME.verify_status=1`;随后回调因流水已 `SUCCEEDED` 走幂等分支,也不会补核销 | 已验证 · 审计 §11;`PaymentSettlementService.java:127` | 回调/查询/人工核查现在共用一个幂等成功入口了吗?还是仍有两份实现? |
| 52 | [ ] | 一笔订单存在两张都能被支付的预支付单;两笔都成功时**没有多收款退回流程** | 防重只按 `idempotency_key` 查已有流水,不按订单阻止另一个键创建有效流水(一次订单、两个键 → 两次预授权,审计已复现) | 已验证 · 审计 §9;ADR-0003「考虑过但没选的」;`HuifuPaymentService.java:143-160` | 前端固定键是「订单号+价格版本」——价格版本变化时会不会仍能造出第二张?有回归覆盖吗? |
| 53 | [ ] | 对账台报错:同一笔业务被反复「结掉又开出」时撞唯一键 `(issue_type, provider, business_no, status)` | 结掉那一步会撞上已存在的 resolved 行 | 已验证 · `e3f412c`(「两处超出票面但为落地所必需」);`04` issue Comments | 还有多少张表用了「状态进唯一键」的写法?值得全局搜一次吗? |
## E. 退款与资金(13 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 54 | [ ] | 通道确认收了钱、但订单已取消(悬账),原先只在账目上留一条问题记录等人工,**没有任何自动出口** | 退款链路的硬门是 `OrderFundBoundaryService.requireRefundable()` 的 `orderStatus != 1`;四个 `@Scheduled` 没有一个消费这类问题记录;后台「支付对账」页是唯一能看到的地方 | 已验证 · `issues/README.md` C3;审计 §8 后半段;`e719199` | 悬账自动退款现在默认 5 分钟一轮(`StrandedPaymentRefundRetry`)——生产上要不要把告警同步推给运营? |
| 55 | [ ] | 「本地已关闭但通道其实收了钱」那类悬账**连支付键都看不到**,退款无从发起 | 支付键可见性只认「已成功流水」;旧实现下 `CLOSED` + 一条「取消后才收到钱」的问题记录定位不到原支付流水 | 已验证 · `e719199`;`HuifuRefundService.java:219-221` | 放宽到「已成功,或已关闭且有一条 `PAYMENT_ORDER_CANCELLED` 问题记录」——这个判据够严吗? |
| 56 | [ ] | 实付 100 退了 40 之后,剩下的 60 **永远退不了** | 旧规则「存在退款中或已退款就阻止新退款」;正确规则是按「已成功金额 + 处理中金额 < 实付」控制 | 已验证 · 审计 §14;`e719199`;`HuifuRefundService.java:256` | 现在退款能否发起全靠订单行锁 + 金额额度判定,删掉了「已有生效中退款」的抛错——这个放宽要在评审时重点确认吗? |
| 57 | [ ] | 退款查单确认时把唯一的通道凭证擦成空串,退款排查与对账失去定位依据 | 通道退款流水号**只在发起退款的那次回执里有**,退款查询回执不带它;旧代码无条件覆盖 | 已验证 · `3da8557`;`HuifuRefundService.java:288-289` | 还有哪些「只在某一次回执里出现的字段」被无条件覆盖过?要不要全局搜一遍 `coalesce(nullif(` 与直接 update 的对照? |
| 58 | [ ] | 14:37:31 提交退款、14:43:41 才对账收敛,中间六分钟里钱已退回人才账户,人才端却仍显示「待核销」,商户端还当它是可核销订单,去核销被资金边界拒 | 退款提交时通道只回「处理中」,退款通知和支付通知一样到不了本地;读路径不收敛 | 已验证 · `3da8557`(含开发库实测时间);`HuifuRefundService.java:160-166`(读订单/列表前收敛必须是调用方事务的**第一条**读语句,因为 MySQL 一致性快照就在那条语句上定下) | 「提交后 20 秒内不查、一轮最多两笔」这两个限流值是怎么定的?列表页本身慢的时候会不会还是拖长? |
| 59 | [ ] | 商户端首页「交易总额」把未核销、退款中的订单也算进去,退款后金额不回落(看起来像缓存) | 首页卡片 SQL 只过滤 `order_status = 1`,**漏了核销条件**;退款受理只把订单置「退款中」(`order_status` 仍是 1),而首页统计的读路径没有退款收敛 | 已验证 · `.scratch/order-payment-display/issues/bug-1-merchant-dashboard-total.md`(含修前红的断言 `expected: 1000L but was: 7000L`);`2aabed5` | 同文件遗留:**补贴总额是同一类缺陷**(`subsidyAmountCents` 也只过滤 `order_status=1`)——那是有意还是漏的? |
| 60 | [ ] | 撤销核销只翻库、不碰钱:订单回到「待核销」,钱却留在商户手里,等 D1 结算到银行卡后就**再也追不回来** | 核销时钱已从延迟户划给商户,撤销核销必须调通道的【交易确认退款】把钱退回延迟户 | 已验证 · `5c33cf1`;V147 头注释;`CONTEXT.md`「延迟入账与交易确认」 | 撤销窗口「止于下一次自动结算」——这个窗口长度(D1,实际可能只有几小时)业务上接受吗? |
| 61 | [ ] | 回退被拒时只能给商户一句「撤销失败」,商户不知道是该重试还是该找平台 | 回退结果有四种归途:成功→`pending`;明确拒绝(已结算到银行卡/余额不足)→退回可重试的 `confirmed`;语义未定(`23000003`/`23000004`)→停在 `reverting` 交人工;通道还在处理→在途 | 已验证 · `5c33cf1`;`HuifuPayload.java:63-77`(「判成功等于放行可能没动的钱,判失败等于让本地以为钱没动、于是再动一次」) | `23000003` 在回退路径上的真实语义,有拿到过真实回执吗?有向汇付要过一条明确说明吗? |
| 62 | [ ] | 撤销核销被永久卡住(补偿任务只扫 `pending`/`confirming`,`reverting` 永远没人再看) | 补偿任务状态集合漏了 `reverting`;`delaytrans.confirmquery`(确认查询)有实现却是**死代码**(全仓零调用);且同一天认领的 `confirming` 要等到次日才重认领 | 已验证 · `issues/README.md` F4;`08-query-parity-and-config-cleanup.md`;`ab9b1f3` | 还有别的状态值是「写进去就没人读」的吗?要不要做一次状态机可达性梳理? |
| 63 | [ ] | 确认被认领后进程中断(部署/崩溃)留下 `confirming` 孤儿,那笔钱**永远冻在延迟户** | 补偿任务只挑得到它、从不重发 | 已验证 · `5c33cf1`(「隔天收掉这种孤儿重新确认一次(重发是幂等的)」) | 「隔天」这个兜底窗口在生产的结算节奏下够快吗? |
| 64 | [ ] | 扫码核销与后台核销都只把订单置为已核销,资金一直冻在通道延迟户,商户**永远收不到钱** | 核销有三条入口,此前只有「订单管理里点确认」那条调用了交易确认 | 已验证 · `84913d9` | 现在三条入口都收敛到同一个方法了吗?还是三处各写一遍? |
| 65 | [ ] | 补偿任务每隔几分钟无意义地打一次通道,永远不自愈 | 通道对「已经没有待确认金额」的延迟交易返回 `23000003`,且**同时给 `trans_stat=F`**;旧实现先看交易状态、再看返回码,把幂等成功判成失败 | 已验证 · `063d60e`;`HuifuDelayAccountGateway.java:223-239`(「这个判断放在交易状态之前」) | 还有别的「成功码配 trans_stat=F」的组合吗?要不要把「先判业务码再判交易状态」写成全局约定? |
| 66 | [ ] | 未核销的钱直接落进商户账户,退款时收不回来 | `delay_acct_flag` 是否延迟由订单类型决定;写反或漏配(例如新增订单类型默认不延迟)就会把钱直接分给商户 | 已验证 · `HuifuMiniProgramPaymentGateway.java:56-57`;`5364ba7`;`CONTEXT.md` | 将来新增订单类型时,这条「默认延迟」的规则有测试钉住吗? |
## F. 结算与对账(10 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 67 | [ ] | 后台「结算记录」页一条流水都不显示,而通道里其实有记录 | 斗拱结算记录查询 `pageSize` 上限 50,超了直接返回 `00000003`「基础参数校验失败:【pageSize-分页条数】…最大值为50」,**一条都不返回**;后台默认传 100。实测:20/50 正常返回 2 条,51/100 一条不返回 | 已验证 · `ad49519`;`HuifuSettlementRecordGateway.java:29-37` | 除 pageSize,这个接口还有别的硬上限(日期跨度、总条数)吗?要不要先把官方限制字段全量核一遍? |
| 68 | [ ] | 结算记录页的商家/联系电话/商家类型/状态四个查询条件**完全无效**;日期栏标签写「申请时间」,实际筛的是结算日期 | 前端切到结算记录时只发日期与条数,四个条件根本没发出去;状态下拉在结算记录下是空列表 | 已验证 · `cfd30ec`;`.scratch/settlement-query-failure/verify-filters.sh` | 状态筛的三档(已结算/结算失败/处理中)是与通道 `S/F/P` 一一对应吗?有真实失败流水样本吗? |
| 69 | [ ] | 快速切换标签时,结算流水的响应**覆盖**了已经切过去的进件/银行卡表格:表头是进件的列、内容是结算记录,分页总数也变成结算的条数 | `load()` 写回表格前没有身份校验,两次请求同时在飞时后到者覆盖先到者;而结算记录要逐商户问斗拱,是最慢的一次 | 已验证 · `cfd30ec` | 别的后台页面有同样的「无请求序号」写法吗?要不要一次性统一加请求序号? |
| 70 | [ ] | 一笔已经受理的取现被本地误判成失败,解锁订单并把**同一笔钱再取一次** | 取现结果查询用 `huifu_id + org_req_date + org_req_seq_id` 定位;如果商户在提交与查询之间换了子商户(重新进件),用「当前最新子商户号」去查会得到 `23000001 交易不存在` | 已验证 · V136 头注释 | 这条链路已随 V140 退役——`merchant_withdrawal` 里还有需要人工收尾的旧记录吗? |
| 71 | [ ] | 提现失败当天被当成「已汇总」,钱后来可取了也没有任何出账路径 | 失败记录没有退回 pending 复用同一条 | 已验证 · `85831da`;V135 头注释 | *(同样已退役)*后台补跑接口 `POST /merchant-settlement/auto-withdrawal/run` 还有调用方吗? |
| 72 | [ ] | 定时对账(支付与退款唯一的后台兜底)可能**静默消失** | 间隔靠注解默认值生效、没写进配置;而注解上 `enabled` 默认是 **false**,靠 yml 那一行打开——删掉那行兜底就没了 | 已验证 · `issues/README.md` F6;`ab9b1f3`;`PaymentSchedulingConfigurationTests` | 「注解默认值与 yml 必须一致」现在有测试钉住了——别的定时任务(能力刷新、补偿任务)要不要一起钉? |
| 73 | [ ] | 交易确认补偿任务的开关**从未被读取**,任务永远在跑(javadoc 却宣称有这个开关) | 配置键 `app.payment.fund-confirmation-retry.enabled` 在代码里从未读取、yml 里也不存在 | 已验证 · `issues/README.md` F7;`ab9b1f3` | 现在真的读了吗?默认值取「开」还是「关」? |
| 74 | [ ] | *(生产 bug)*绑卡(业务开通)回调地址**从未在通道侧注册**,即使有公网也收不到 | `app.payment.huifu.merchant-business-notify-url` 在 yml 里根本没这个键,它靠 `notify-url` 推导出一个值——「通道侧到底注册了哪个地址」取决于另一个配置 | 已验证 · `issues/README.md` F3;`ab9b1f3`;`application.yml:92-94`;`HuifuCallbackUrlAudit` | 五个回调地址现在在汇付控台/接口里各注册的是什么?要不要逐条比对一次并留档? |
| 75 | [ ] | 回调丢了以后**没有任何人推进状态**(订单 520/521 的悬账根因就是回调没到达) | 只靠回调推进的状态在本地环境根本收不到(回调地址指向公网 IP,开发机不可达) | 已验证 · ADR-0004;`fd5fc5e`(订单 520/521 的 2 分钱 10:14 才补退成功) | 五个回调端点里还有没有「没有查询对等物」的?ADR-0004 的清单要不要每次新增通道能力都补一遍? |
| 76 | [ ] | *(高风险)*后台仍有「跳过真实支付、直接置为已支付」的入口,且页面权限拦截器没有匹配 `/api/orders/**` | `OrderService.pay` 只更新 `order_status=1`,不要求支付流水或汇付结果;该订单再被核销便进入结算统计 | 推断 · 审计 §7;ADR-0004「不允许为了开发便利开跳过真实通道的置付捷径」 | 这个入口现在还在吗?如果留着,是否已有独立资金来源/凭证/审核流程? |
## G. 工程与环境(13 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 77 | [ ] | 2026-09-15 后台**全站不可用**(所有页面加载到超时,健康检查都超时) | 进件回调先 `select ... for update` 锁住进件记录,然后在事务里同步调汇付 HTTP:拿着行锁 + 一条数据库连接等一个不受控的网络响应,连接池(10 条)很快借光 | 已验证 · `609bcde`(含当天会话状态原文) | `runAfterCommit` 之后,还有哪些回调用「事务内同步调外部」的写法?要不要全局搜一遍 `for update` + 网关调用? |
| 78 | [ ] | 2026-09-15 上午后台**再次**全站不可用 | 启动时补列的 `ALTER` 永久等锁(一个空闲未提交事务持着 SHARED_READ 元数据锁 280 秒),该表所有查询排队,连接池借光;`application.yml` 同时打开了 Hikari `leak-detection-threshold=10s` | 已验证 · `4fff3f4`(含 `conn 201/202` 的原文诊断) | 现在初始化器等锁上限 5 秒、失败只记 error——「宁可少一列也不让启动被挂住」,这个取舍认可吗? |
| 79 | [ ] | 重启过也不代表有新列:`legal_mobile` 缺列时,**任何新进件提交**都报 `Unknown column 'legal_mobile'`,实名被拦成「发起实名还缺:法人手机号」(看起来像商户没填) | `addColumn` 把 `lock_wait_timeout` 压到 5 秒,拿不到锁就放弃并记日志;同一轮循环里它后面的列都加成功、只有它没有 | 已验证 · `.scratch/merchant-individual-onboarding/audit.md`「顺带查到的环境问题」(含逐项排障 4 步);`MerchantOnboardingSchemaInitializer.java:174` | 初始化器要建的 49 列与库里实际列做过一次完整比对——这个「列清单比对」能不能做成常驻的启动自检/测试? |
| 80 | [ ] | `GET /api/merchant/onboarding/status` 第一个请求就 500(`Table 'talent_card.merchant_onboarding_progress' doesn't exist`) | V144 新建的两张表在开发库从未建立;开发库 `spring.flyway.enabled=false`,而初始化器**只有加列路径、没有建表路径**;测试库由 Flyway 建表,所以测试全绿看不出 | 已验证 · `621b519`;`evidence-2026-09-15.md` | 现在有 `MerchantOnboardingSchemaInitializerTests` 断言「迁移新建的进件表必须在初始化器登记 + 两侧列名一致」——这个模式要不要推广到**支付流水表**? |
| 81 | [ ] | 合并后 Flyway 报「同一版本两个迁移」,本地四个迁移全部要改名 | 双方从同一祖先(V147)各自独立占了 V148–V151(git 不报冲突,Flyway 会直接报重) | 已验证 · `1805a7b`;`.scratch/pending-order-payment-flow/README.md` | 「先确认当前最大版本号」这条要不要写进 `AGENTS.md` 并加一个 CI 检查? |
| 82 | [ ] | 迁移在 MySQL 8.0.36 上直接 `ERROR 1064` | 不能写 `add column if not exists`——那是 MariaDB 的扩展(本机实测确认);迁移只执行一次,本来也不需要它 | 已验证 · V143/V145/V146 头注释(同一条坑被记了三次) | 5.7(开发库)与 8.0(测试库)双兼容只在测试库跑不出来——每次新增迁移都实际在 5.7 上跑过一遍吗? |
| 83 | [ ] | 已执行过的迁移改一个字符,测试库直接起不来 | Flyway checksum 不匹配 | 已验证 · `docs/agents/testing.md:91-95` | 历史上有多少次靠改迁移而不是新增迁移来「修」DDL?(`scripts/repair-local-mysql-v*.sql` 是为此付出的代价) |
| 84 | [ ] | 看起来不可能的报错,例如 `No qualifying bean of type 'HuifuPaymentProperties'` | 同一个 checkout 上并发跑两个 Maven 构建会写坏 `target/`;另外 `mvnw.cmd` 是 batch/PowerShell 混合 wrapper,**总是**用 PowerShell 5.1 引导,而 5.1 会把 Maven 打到 stderr 的警告当成终止错误 → 编译中途莫名中断 | 已验证 · `docs/agents/testing.md:37-47`;`50c71b3`、`3ec11f3`、`f7a1861` | 团队里有人用 IDE 自带构建 + 命令行脚本并行吗?要不要在脚本里加 lockfile 防并发? |
| 85 | [ ] | 首页统计的回归测试在 H2 缝上**根本跑不起来**(HEAD 上就红) | `selectDashboardCards`/`selectPeriodSummaries` 内联了 `GovernmentSubsidySql.AMOUNT_CENTS`,用到 MySQL 的 `json_extract` 与 `if()`;H2 2.4.240(MODE=MySQL)两者都没有,`if()` 更是语法错误、无法用 `create alias` 绕开 | 已验证 · `bug-1` 文档「另一件值得记的事」;`.scratch/order-payment-display/H2Probe.java` | 现在有多少条 SQL 只能在真 MySQL 上测?要不要把「H2 只作兜底」写成测试约定? |
| 86 | [ ] | 测试全部报 `Connection refused`,或更绕的 `Unable to determine Dialect without JDBC metadata` | 测试库那个 8.0.36 跑在 **WSL 的 docker** 里;WSL 虚拟机会在空闲时自己关掉,容器跟着停。**光起容器不够**——命令一返回 VM 又空闲关掉,现象是容器每次 `docker ps` 都显示 `Up 2 seconds`,要用一个长命进程挂住它 | 已验证 · `docs/agents/testing.md:120-150`(2026-09-17 实测);`06db42a` | 有更稳的做法吗(把 8134 换成本机常驻实例、或给 VM 设自启)?长期依赖「挂一个 sleep」很容易忘。 |
| 87 | [ ] | 某个用例**从今往后每跑必红**(`PaymentFundConfirmationTests` 补偿用例),不是偶发 | 补偿任务扫全库 + `order by o.id asc limit 50`;用例自己新建的那单 id 最大,排在窗口之外。测试库共享且从不清理,符合条件的历史行累计到 50 条以上就饱和 | 已验证 · `.scratch/payment-fund-confirmation-retry/issues/01-...md`(实测 `eligible=60 oldestId=906 newestId=6549 firstSeen=2026-09-14`);`ab9b1f3` 已修 | 这条已修,但同一机制(全库扫描 + limit + 共享测试库)还在别处吗?要不要一次性搜所有 `limit ?` 的后台任务? |
| 88 | [ ] | 拿旧副本库判断开发库状态,结论全错 | 本机 `8134` 上有三个库是合并前的本机副本,**不再是开发库**;而 `.scratch/` 下很早的探针把 URL 写死成 `127.0.0.1:8134/talent_card` | 已验证 · `docs/agents/testing.md:18-21`;`.scratch/db-merge/README.md` | 那些写死 8134 的一次性探针要不要统一加一行「默认指向旧副本、复用请显式传 URL」的注释? |
| 89 | [ ] | *(环境事故)*开发库里迁移从未创建的历史表被删掉,一批数据丢失 | 「把测试库结构克隆到开发库」「`reset-test-db.ps1 -Database talent_card`」这类操作会删掉 `dependent_card_apply`、`merchant_benefit_rule` 等手建表;2026-09-13 就这样丢过一批 | 已验证 · `docs/agents/testing.md:64-70` | 开发库里还有多少张「迁移从未创建」的表?要不要先盘一份清单入库? |
## H. 凭据与配置(7 条)
| # | ✓ | 现象 | 我推断的根因 | 证据 | ❓ 待确认 |
|---|---|---|---|---|---|
| 90 | [ ] | 查目标商户一律 `20002 数据权限认证失败`;以目标商户自身作 `sys_id` 查自身,SDK 报无响应、HTTP 922 空响应 | 平台身份对不归自己的商户没有数据权限。凭据轮换成目标商户的**直属上级渠道**后 20002 才解除 | 已验证 · `evidence-2026-09-15.md`(第一轮「查询权限对照」表) | 当时汇付客服给的结论是什么?还是只靠轮换凭据试出来的?「渠道商作 `sys_id` 能管下级」有官方依据吗? |
| 91 | [ ] | 在控台的商户关系上做文章(head 层级、总部客群类型)对 20002 完全无效,还平白增加进件失败面 | 20002 与层级无关,取决于签名身份本身有没有那些商户的数据权限 | 已验证 · `HuifuPaymentProperties.java:26-31`;`application.yml:112-115`;`7dfc5b7`→`e89a164` | 「渠道商 → 普通商户」两级是最终形态吗?将来拆多平台/多运营主体时,这套凭据模型还成立吗? |
| 92 | [ ] | 测试输出里出现运行配置的**明文私钥** | `HuifuSignatureServiceTests` 会打印运行配置;已改为使用现成的 `keySummary` 摘要函数 | 已验证 · `evidence-2026-09-15.md`「全量后端测试 179 项」段末 | 除了这个测试,还有别的地方(日志、探针、报错信息)会带出私钥/密钥吗?要不要加一条统一的日志脱敏? |
| 93 | [ ] | 微信费率默认写成 0.60,费率规则号默认为空,导致真实补开时费率与 CID 都是错的 | 默认值是在「企业 756 + 0.70」定稿之前写的;`HuifuPaymentProperties` 的 Java 默认值(`0.60`、`merchantWechatFeeRuleId=""`)与 yml 不一致 | 已验证 · `c18b9fa`;`HuifuPaymentProperties.java:46,51`;`application.yml:120-128` | 0.70 / 756 / 761 / 775 这四个值有汇付书面的费率确认函吗?还是只按官方 Excel 推的? |
| 94 | [ ] | 配置里写着开发也能用的回调地址,实际开发机不可达 | 五个回调地址全指向一个公网 IP;且平台没有公网服务器 | 已验证 · `issues/README.md` F1;`application.yml:91-110` | 生产/联调环境的公网回调地址现在配的是哪个?在通道侧注册了吗?(ADR-0004 把这条单独列为「生产 bug」) |
| 95 | [ ] | 回调地址为空时会静默指向一个不可达地址,没人知道回调其实没启用 | 原实现靠 `notify-url` 推导其余四个地址,缺一个也看不出来 | 已验证 · `ab9b1f3`;`HuifuCallbackUrlAudit` | 五个回调地址里,现在哪几个是空/推导值?要不要在后台做一个只读的「回调注册状态」页? |
| 96 | [ ] | 单测各自正常,合起来异常(「一次提现之后支付用错产品号」) | SDK 的 `initWithMerConfig` 写静态 `configMap["default"]`,未指定配置名的请求读该默认配置;支付/提现/结算三类网关各用自己的锁,彼此不能隔离(对 `dg-java-sdk 3.0.40` 字节码检查确认过) | 已验证 · 审计 §15;`HuifuSettlementRecordGateway.java:25,95-111` | 提现网关已随 V140 退役,但**多个网关仍各自初始化 SDK 默认配置**——要不要统一改成命名配置 + 每次请求显式传配置名? |
---
## 附:汇付侧行为反直觉的 28 条(`05` 的核心内容)
> **2026-09-18 推导结论(原「确认过吗」列作废,答案已自行查明)**
>
> 全仓检索确认:**仓库里没有任何「汇付官方回复」的留档**——无客服对话、无邮件、无工单编号、无官方口头结论。检索词覆盖 客服/客户经理/技术支持/工单/官方回复/汇付答复/对话记录/聊天/截图/邮件/对接群/汇付侧说,命中为零(`V137` 里的「工单 06」是我方开发任务编号,不是汇付工单)。
>
> 唯一一处以「汇付」为主语的转述——`.scratch/merchant-closure-impact/issues/huifu-merchant-closure-impact.md:9`「汇付的说法:控台关闭商户后…」——**无出处、无时间、无渠道、无留档**,按标准只能算【推断】,也就是第 25 条的通道侧行为。
>
> 因此下表 28 条的置信档**没有一条是「已与汇付确认」**。逐条推导结果(`05` 里按这个标注):
| 档 | 条数 | 条目 |
|---|---|---|
| **【官方原件】**(本地有原件可引) | 4 | 13(`huifu-api_shjj_wxsmrzztcx.md`)、18/19(`wx-fee-rule-id.xlsx` 官方 Excel 全表)、27(`huifu-wx-config-doc.md` 原文) |
| **【官方·外部】**(官方写了,仓库内无原件) | 4 | 2、3(关单异步与一分钟限制)、4(`23000000` 有官方码表行;**`10000016` 只有官方返回示例文案、无码表行**)、15、23 |
| **【实测】** | 17 | 1、5、6、8、9、10、11、12、14、16、17、20、21、22、25(本地侧)、28 |
| **【推断】** | 3 | 7 的后半(退款/回退路径语义未定)、24(回退遇 D1 已结算会失败)、26(流水号去重) |
**三条「我方实测与官方字段表冲突」——必须在 `05` 里显式标出,接手的人照官方文档写会和我们现在的实现打架:**
| 条 | 我方实测 | 官方字段表 | 处理 |
|---|---|---|---|
| 21 | 支付查询回执里通道流水号是 `org_hf_seq_id` | 官方支付查询响应表写的是 `hf_seq_id`「汇付全局流水号」,`org_hf_seq_id` 在该页出现 0 次 | 标**需复核**:可能官方表漏列、也可能别的产品线字段名不同。我们按实测走,但写明「官方表与此不一致,新增用途前先取证」 |
| 22 | 退款查询回执**不带**退款流水号 | 官方退款查询响应表列有 `org_hf_seq_id`「退款全局流水号」(必填 N) | 标**需复核**:我们按实测走(无条件覆盖会把唯一凭证擦空),但不要断言「官方说错了」 |
| 14 | 授权二维码是**裸 base64 的 JPEG**(`/9j/`) | 官方示例给的是 `data:image/png;base64,…` | 按实测走,且**不要写死格式假设**——已实测到通道会回 JPEG |
**其余结论的置信档**:A–H 表 96 条里,凡我在「证据」列标了 **已验证** 的即【实测】,标 **推断** 的即【推断】——两者的区别是「有没有真实回执/库内数据」,与我是否找过汇付无关(答案是都没找过)。
| # | 汇付的行为 | 常识/文档会让人以为 | 我们的绕法 | 确认过吗? |
|---|---|---|---|---|
| 1 | 未付款的预支付单在通道侧是「处理中(`P`)」 | 「未付款」应该是不确定/未支付 | `notCollected()` 只承认三种「绝不可能收过钱」的答复:`F`、`I`、`23000001` | ? |
| 2 | 关单是**异步**的,且关单查询是**另一个接口** | 关单接口返回就代表关掉了 | 只发关单不问结论 → 用户实测等 72 秒 | ? |
| 3 | 通道**不允许关闭一分钟以内**的订单 | 想关就能关 | 最短重试间隔被通道钉在 1 分钟 | ? |
| 4 | `23000000` / `10000016`「原订单已为终态」意思是「**它已经结束了**」 | 「现在不让关」= 还没结束 | 不能并入「其余拒绝」,否则死凭证被当成「还能付」继续交回客户端 | ? |
| 5 | 关单请求**本身也会失败**:`90000000 交易失败,请重试` 表示这次关单**没生效**,凭证还活着 | 请求失败 = 状态未知 | 与 `CLOSING`(关单可能已生效)语义完全相反,必须分开建模 | ? |
| 6 | `23000003` 会**同时带 `trans_stat=F`**,但在交易确认路径上是幂等成功(「已无未确认金额」) | 状态 `F` = 失败 | 先判业务码、再判交易状态 | ? |
| 7 | **同一个业务码在不同路径上语义不同**:`23000003`/`23000004` 在交易确认路径已验证是幂等成功,在退款/交易确认退款路径上**含义未定** | 错误码全局唯一语义 | 两个方向的错都很贵(判成功=放行可能没动的钱;判失败=重复出款),未定时停在中间态交人工 | ? |
| 8 | 通道对**刚创建的微信交易**,最常见的关单答复就是「关单处理中」(实测三次取消全中) | 关单应该很快 | 取消不再等关单结论,按用户意图立刻生效 | ? |
| 9 | 业务开通是**申请型**接口,返回 `90000000` 是「受理/审核中」,**不是失败** | 非成功码 = 失败 | 旧网关漏了它,会把一次成功受理记成 `failed` | ? |
| 10 | **微信入驻是异步的**:业务开通被受理(`90000000`)那一刻,通道侧还查不到微信子商户号 | 受理了就该能配置 | 配置闸门必须取通道证据(`qry_wx_conf_list` 里真出现线上场景),不能取本地状态 | ? |
| 11 | 不设最低收费时,通道**不返回** `fee_min_amt`(不是返回 `0.00`) | 缺失 = 未生效 | 缺失必须按 0 处理,否则阶段判定永远不成立 | ? |
| 12 | `apply_status=Y`(申请单通过)**不代表**微信业务也开通了:原申请可能本来就没带微信业务,`wx_conf_result_list` 会整个缺失 | 申请单通过 = 业务开通 | 两项必须分别判定 | ? |
| 13 | 授权是否完成**只能看 `authorize_stat`**:`APPLYMENT_STATE_PASSED` 单独不算;反过来 `APPLYMENT_STATE_FREEZED`(「该主体已完成过入驻」)也可能已授权成功(真实样本就是 FREEZED + `authorize_stat=1`) | 状态字段字面义 | 只看 `authorize_stat` | ? |
| 14 | 授权二维码是**裸 base64 的 JPEG**(`/9j/`),既不是 PNG、也不带 `data:image/...;base64,` 前缀;且授权成功后通道仍会回传该码 | base64 图片常见是 PNG | 判格式不能只看前缀假设;前端按状态收起而不是按「有没有码」 | ? |
| 15 | `media_mer_common_flag` 是 `online_media_info_list` 的**元素字段**,不是顶层参数 | 名字像顶层参数 | 放在列表元素里;且声明「不一致(N)」会连带索要授权或开发证明材料 | ? |
| 16 | `use_chains_flag=3` 要求**上级本身**有商户级微信配置——渠道商没有,所以渠道商做不了 AT 复用 | 上级渠道商天然能复用 | 不下发该标志,让子商户在渠道下拿自己的微信子商户号 | ? |
| 17 | **微信小程序配置必须排在业务开通之后**:微信子商户号是业务开通创建的 | 配置可以从容提前做 | 顺序反了必被驳「根据渠道号或微信子商户号未查到入驻记录」,而且配置一失败业务开通永远轮不到 | ? |
| 18 | CID 是「支付场景 × 业务开通类型 × **主体类型**」**三维**定位:企业 756 / 个体户 761 / 事业单位 773 / 社会组织 774 / 小微 775 | 全平台共用一个费率规则号 | 按主体类型分行配置;拿企业的 756 给小微会被拒 | ? |
| 19 | **场景 12(线上反扫)在官方《微信费率规则ID》表里没有独立行**,与线上公众号/小程序(4/5)共用同一条线上规则 | 每个场景都该有独立行 | 共用线上的那一条,并在配置注释里写清 | ? |
| 20 | 结算记录查询 `pageSize` 上限 **50**,超过直接 `00000003` 且**一条都不返回**(不是截断到 50) | 超限应该截断或报错 | 网关内钉住 `PROVIDER_MAX_PAGE_SIZE = 50` | ? |
| 21 | 支付查询回执的通道流水号字段是 **`org_hf_seq_id`**;`hf_seq_id` **只出现在异步通知里** | 两者是同一个字段 | 取错会**静默写成 NULL** | ? |
| 22 | **退款查询回执不带退款流水号**——它只在**发起退款的那次回执**里有 | 查单能拿到全部字段 | 不能无条件覆盖,否则把唯一凭证擦成空串 | ? |
| 23 | 延迟入账下「**订单已核销**」≠「**商户已拿到钱**」:钱停在通道「待确认」,必须显式调交易确认才按分账串分配;未核销的钱官方定义是「不可结算、不可取现」 | 核销即到账 | 核销时调交易确认,并把结果落 `funds_confirmation_status` | ? |
| 24 | 交易确认退款(回退)在钱被 D1 结算划到商户银行卡之后就会**失败**(基本户余额不足) | 随时能回退 | 撤销核销的窗口实际上止于下一次自动结算 | ? |
| 25 | 控台关闭商户后该商户**所有**支付相关功能受限,但我们本地的进件状态仍是 `approved`、能力快照最长 6 小时后才被发现 | 关停会推通知给我们 | 定时核实通道能力;感知仍依赖「下一次下单被拒」 | ? |
| 26 | 通道按**请求流水号去重**:业务开通/实名/配置被驳回后重试**必须换新号**,否则被当成重复请求 | 重试无副作用 | 换号时必须保住随机后缀(先按剩余宽度裁前缀再接后缀,否则超长截断把随机位一起切掉) | ? |
| 27 | 微信商户配置是**添加**接口:成功后**不支持修改和删除**,每种参数最多 5 组 | 配置可以反复改 | 一次配对,不指望改 | ? |
| 28 | `20002 数据权限`与商户层级**无关**:改了层级(head 字段)仍然 20002,取决于签名身份本身的数据权限;同一身份操作自己的数据全部 `00000000` | 归属错了就换层级 | 凭据轮换成目标商户的直属上级渠道 | ? |
---
## 附:疑似但未找到证据(10 条,成文时按 `[待确认]` 落笔)
这些我在本次调查里**没找到证据**,不写进正文的结论,只在文档里标注「未验证」。你如果知道答案,直接在这里补。
1. 审计 §7 那个「后台跳过真实支付直接置为已支付」入口现在是否真的不存在(`OrderService.pay` 与 `AdminAuthInterceptor.page()` 的当前状态)
2. 核销后交易确认在「商户被通道关停」下能否成功、钱是否还留在延迟户(closure-impact 写明「代码里看不出来,**需要实测**」)
3. 真机上核销码图片取不回来的根因(候选:「http 图片被微信真机拦掉」vs「图片请求 404」),决定是「等后台切 HTTPS 合法域名后自然消失」
4. `provider_refund_no` 之外是否还有别的「只在某一次回执里出现的字段」被无条件覆盖(只找到退款流水号这一处修复,没找到全局排查记录)
5. 进件的 `store_address_code` 与门店照片在真实通道上的接受度(audit.md 写「未验证……不能用『模拟测试通过』代替」)
6. 业务开通的 `micro_biz_type` 是否与独立实名接口重复(audit.md 七记为「仍未官方确认」的唯一一条)
7. 补贴总额(`subsidyAmountCents`)口径是否有意不带核销条件(类注释与交易总额不一致,文档标「需要产品和结算口径确认」)
8. `DashboardSummaryService.summary()` 死副本(文档说「谁把它接上去,这个 bug 就会回来」,没找到删除记录)
9. `PaymentFundConfirmationRetry.enabled` 的默认取值(README F7 建议实现它,实现的默认值未见明确记录)
10. 进件进度 `sync_owner/sync_until` 的 5 分钟租约与 `capability_sync_until` 的 5 分钟冷却是否必须同值(ADR-0002 只说「同量级」)
---
**我需要的**:~~A–H 表里 ❓待确认 列的回答~~ —— **2026-09-18 已自行推导,不再需要人工确认。**
处理方式:`❓待确认` 列的问题我已逐条回查代码与留档,能定的定了,定不了的按下面的规则落笔:
- **能定** → 直接写入 `05`/`06`,并按四档标置信度(【官方原件】/【官方·外部】/【实测】/【推断】)。
- **定不了** → 写 `[待确认:<具体问题>]` 内联标注,并在 `06` 末尾汇总成「待确认清单」。**不删**——因为「我们也不知道」本身是接手人必须知道的信息。
- **凡属「汇付侧这么规定的」而无本地原件** → 一律标【官方·外部】并给官方 URL,**绝不写成「已与汇付确认」**。
.scratch/huifu-handover/plan.md
New file
@@ -0,0 +1,191 @@
# 汇付/斗拱支付链路 —— 交接文档计划
**状态**:① 阶段(大纲 + 坑清单待确认)
**对应代码窗口**:2026-09-11 ~ 2026-09-18(149 个提交)
**读者**:接手支付链路维护的后端同事,需要能独立判断「这是我们还是汇付的问题」,并可能继续做汇付侧对接
---
## 1. 交付物清单
### 1.1 主文档(新增,`docs/huifu/`)
| 文件 | 内容 | 目标行数 |
|---|---|---|
| `README.md` | 导航:先读什么、术语对照表(含 `_Avoid_` 禁用同义词)、权威来源地图(哪个问题该查哪份)、维护契约 | ~120 |
| `01-链路总览与代码地图.md` | 端到端流程图(图1)+ 三端代码地图 + 「我要改 X 该去哪」索引 + **硬规定**小节 | ~250 |
| `02-进件与收款能力.md` | 进件时序图(图2)+ 收款能力判定图(图3)+ 六阶段模型 + 该链路最易再犯的 3–5 条坑 | ~250 |
| `03-支付与资金.md` | 支付尝试状态机(图4)+ 资金状态机(图5)+ 延迟入账/交易确认/退款收敛 + 该链路坑 3–5 条 | ~250 |
| `04-结算与对账.md` | 对账收敛图(图6)+ 斗拱自动结算 + 结算记录页 + 差错台账 + **退役提现链路说明** + 该链路坑 3–5 条 | ~250 |
| `05-汇付对接注意事项.md` | ★ 协议契约(签名/验签、回调 ACK 与重试、错误码分层、请求头、字段陷阱)+ **反直觉行为 28 条** | ~250 |
| `06-踩坑清单.md` | 96 条全量(一条一行:现象 \| 根因 \| 证据)+ 置顶「必读 15 条」+ 末尾「待确认汇总」 | ~280 |
| `07-排障手册.md` | 症状 → 排查路径 → 探针(指向 `scripts/probes/`)+ 常见误判 | ~200 |
| `08-上线与环境.md` | 环境与凭据、五个回调地址、定时任务开关、上线验收清单、迁移编号规约、测试纪律 | ~200 |
### 1.2 参考材料(`git mv` 迁入)
`docs/huifu/reference/` —— 从 `.scratch/merchant-wechat-onboarding/` 迁入。**必须分两类放、分两类标注**,这不是排版问题:把「我方归纳」标成「汇付原文」,是交接文档里最不该出的错。
**A. 官方原件(页面抓取件 / 官方 Excel)** —— 目录 `reference/official/`
| 文件 | 大小 | 性质 |
|---|---|---|
| `huifu-business-open-doc.md` | 129KB | 官方页抓取件 · 商户业务开通(1022 行) |
| `huifu-business-modify-doc.md` | 124KB | 官方页抓取件 · 商户业务开通修改(955 行) |
| `sb-_sidebar.md` | 46KB | 官方页抓取件 · 官网侧边栏导航(仅作索引) |
| `huifu-api_shjj_wxsmrz.md` | 21KB | 官方页抓取件 · 微信实名认证 |
| `huifu-wx-config-doc.md` | 11KB | 官方页抓取件 · 微信商户配置 |
| `huifu-fee-rate-query-doc.md` | 11KB | 官方页抓取件 · 商户费率信息查询 |
| `huifu-api_shjj_wxsmrzztcx.md` | 9KB | 官方页抓取件 · 微信实名认证状态查询 |
| `config-query-doc.md` | 10KB | 官方页抓取件 · 微信商户配置查询 |
| `huifu-multifee-query-doc.md` | 5KB | 官方页抓取件 · 商户多费率配置查询 |
| **`wx-fee-rule-id.xlsx`** | 15KB | **官方 Excel 原件** · 《微信费率规则ID》全表(CID 三维定位的唯一权威) |
**B. 本项目自撰(不是官方原件)** —— 目录 `reference/notes/`
| 文件 | 大小 | 性质 |
|---|---|---|
| `evidence-2026-09-15.md` | 27KB | **我方实测日志**(Ticket02 只读核验,含探针回执原文与我方裁定) |
| `official-contracts-2026-09-15.md` | 16KB | **我方自撰的官方文档核查摘要**(作者自己在头部列了 S1–S13 官方 URL 来源,正文是二次转写) |
两个目录各放一份 `README.md`:
- `official/README.md`:**这是汇付官方页面抓取件与官方 Excel 的本地快照,快照日期 2026-09-15,非本项目文档;字段以汇付官网当前版本为准。** 附带说明抓取方法(官方文档配置把 `/partners/api/doc/` 作为 Markdown 根路径,例如 S2 对应 `paas.huifu.com/partners/api/doc/shgl/shjj/api_shjj_wxshpzcx.md`)。
- `notes/README.md`:**这两份是「我们自己写的」,不是汇付的原文,引用时不得当作官方依据。** `evidence-*.md` 的证据效力是「我方在真实通道上跑出来的」;`official-contracts-*.md` 的证据效力是「我方读官网后的归纳」,**引用它时必须回官方 URL 复核,或引用 `official/` 下的抓取件原文。**
### 1.3 排查工具(新增,`talent-card-server/scripts/probes/`)
命名与运行方式沿用 `talent-card-server/docs/audits/2026-09-10/PaymentFlowAuditProbe.java` + `run-probe.ps1` 的既有模式。
| 探针 | 回答什么问题 |
|---|---|
| `PaymentStateProbe` | 给定订单号/支付流水号,比对本地状态与通道真实状态,指出差异 |
| `MerchantCapabilityProbe` | 给定商户/进件,比对本地能力快照与通道 `basicdata` 真实值 |
| `OrderFundBoundaryProbe` | 给定订单,打印资金边界 + 资金确认状态 + 相关差错记录 |
| `SettlementRecordProbe` | 给定汇付号 + 日期,直查通道结算记录(含 `pageSize=50` 的教训) |
| `CallbackUrlAuditProbe` | 单跑版回调地址自检(`HuifuCallbackUrlAudit` 现在只在启动时跑) |
`scripts/probes/README.md` 写安全约束:**只读、不改状态、输出脱敏(不打印私钥/密钥/完整卡号)、生产环境默认拒绝运行任何写操作。**
### 1.4 顺带的小改动
| 改动 | 说明 |
|---|---|
| `CONTEXT.md` | 增量补术语 ≤6 条(业务开通、微信实名、收款能力快照、资金回退),带 `_Avoid_` 禁用同义词。**只补术语**,不塞硬规定(它是 AGENTS.md 点名的共享文件,定位是术语表) |
| `.scratch/huifu-handover/issues/01-plaintext-credentials-in-application-yml.md` | 明文凭据的 issue(见下) |
| `talent-card-server/docs/huifu-payment-integration.md` | **原地不动**。在 `README.md` 里作为「附录 A:英文实现说明」链接;`05` 开头注明「实现细节见附录 A,冲突时以附录 A 为准」 |
---
## 2. 六张流程图(Mermaid 内嵌,分散放进对应文档)
| # | 图 | 类型 | 放哪 |
|---|---|---|---|
| 1 | 端到端总览:进件 → 业务开通 → 实名 → 收款能力 → 下单 → 支付 → 核销 → 资金确认 → 结算 | `flowchart` | `01` |
| 2 | 进件时序:企业/个体双路径 + 通道回调 + 分阶段开通 | `sequenceDiagram` | `02` |
| 3 | 收款能力判定与收款闸门(什么状态下能收款) | `flowchart` | `02` |
| 4 | 支付尝试状态机(复用/关单/新建、取消、悬账) | `stateDiagram-v2` | `03` |
| 5 | 资金状态机(延迟入账 → 核销 → 交易确认/回退;退款收敛) | `stateDiagram-v2` | `03` |
| 6 | 对账定时任务与差错收敛(1 分钟扫描 + 退避 + 后台立即对账) | `flowchart` | `04` |
每张图下方配 2–3 行文字兜底说明(纯文本环境下也能读懂)。
---
## 3. 写作约定
1. **语言**:中文正文;类名、字段名、错误码、通道返回码保留英文原文。
2. **术语**:严格沿用 `CONTEXT.md`,遵守其中的 `_Avoid_` 禁用同义词(预支付单≠支付单,支付尝试≠支付次数量词,关单≠取消,悬账≠差错,延迟户≠中间户)。`README.md` 放一张术语对照表方便速查。
3. **置信度标注(四档,2026-09-18 修订)**:原先的三档「已验证/推断/未确认」不足以支撑交接——因为**「已验证」和「汇付官方这么规定的」是两回事**,接手人据此做的判断完全不同。改为四档,每条结论**必须**带一个:
   | 档 | 含义 | 判据 |
   |---|---|---|
   | **【官方原件】** | 汇付官方文档写了,且**本地有原件可引** | `docs/huifu/reference/official/` 下有抓取件或官方 Excel |
   | **【官方·外部】** | 官方文档写了,但仓库内没原件 | 只能给官方 URL,需联网复核 |
   | **【实测】** | 我们自己在真实通道/真实库上验证过 | 有真实回执、库内数据或实测记录 |
   | **【推断】** | 只有代码逻辑可推 | 无实测、无官方依据 |
   **重要事实(2026-09-18 全仓核查结论)**:**仓库里没有任何「汇付官方回复」的留档**——没有客服对话、邮件、工单编号或官方口头结论。经全量检索(客服/客户经理/技术支持/工单/官方回复/汇付答复/对话记录/聊天/截图/邮件/对接群/汇付侧说…)确认命中为零;`V137` 里的「工单 06」是我方开发任务编号,不是汇付工单。唯一一处以「汇付」为主语的转述(`.scratch/merchant-closure-impact/issues/huifu-merchant-closure-impact.md:9`「汇付的说法:控台关闭商户后…」)**无出处、无时间、无渠道、无留档**,只能算【推断】。
   因此:**本套文档里不出现「已与汇付确认」这种说法。** 凡是我们自己试出来的,一律写【实测】,并明确「这是自测结论,汇付侧未书面确认」——这一句对接手人比结论本身更重要,因为他将来照这个结论做了新对接、被通道拒了,才知道该去找汇付要依据而不是怀疑自己。
4. **证据自包含**:`06` 每条必须写全「现象 + 根因」,`commit`/文件引用是补充不是论据——即使 `.scratch/` 被清理,文档仍立得住。
5. **保鲜头部**:每份文档第一行固定
   `> 适用提交: <sha> | 最后验证: 2026-09-18 | 何时会失效: <具体条件>`
6. **硬规定所在位置**:`01-链路总览与代码地图.md` 的「硬规定」小节(不进 `CONTEXT.md`):
   - 页面判定与下单闸门必须**同源同文案**(ADR-0002)
   - 可复用集合只含 `PREPAY_CREATED` + `CANCEL_UNCONFIRMED`
   - **先判业务码,再判交易状态**
   - `LABELS` 同时是展示列表与完成判定的键集——加第 7 项阶段会破坏完成判定(ADR-0002「考虑过但没选的」)
   - 通道状态必须存在**不依赖回调的查询路径**(ADR-0004)
   - 通道侧的收款能力以**快照**为唯一口径,与下单闸门同源(ADR-0002)
7. **前端**:`01` 的代码地图覆盖三端入口;`02/03/04` 各链路末尾加 5–10 行「前端触点」(页面文件 + api 文件 + 触发哪个端点)。不单独写前端文档。**并在 `01` 写明:uvue 客户端没有可编译的反馈回路(本机无 HBuilderX / uni CLI),改前端只能真机验证**——第 46 条那个「待支付页每秒闪一次」就是这么产生的。
---
## 4. 维护契约(写进 `README.md`)
**谁改支付链路,谁更新对应文档。** 下列事件一旦发生,必须同步改文档:
| 触发事件 | 要更新 |
|---|---|
| 新增/修改通道能力(业务开通、实名、费率) | `02`、`05`、`06` |
| 新增迁移 | `02`~`04` 相关链路 + `08` 的迁移编号 |
| 改动收款闸门条件 | `02`、`06` |
| 改动可复用集合 / 关单逻辑 | `03`、`01` 的硬规定 |
| 新增/修改回调地址 | `05`、`08` |
| 通道返回码语义变化 | `05`、`06` |
| 任何「先查一下再决定」的新逻辑 | `01` 的硬规定(ADR-0004 的查询对等物清单) |
---
## 5. 不属于本次范围(明确划出)
| 事项 | 去哪 |
|---|---|
| `application.yml` 四个明文凭据键改成环境变量占位;私钥轮换 | `.scratch/huifu-handover/issues/01-...md`(独立工单) |
| `OrderService.pay` 那个「跳过真实支付」入口的去留 | 待确认清单第 1 条 |
| 补贴总额口径(是否该带核销条件) | 待确认清单第 7 条,需产品/结算口径确认 |
| `DashboardSummaryService.summary()` 死副本是否删除 | 待确认清单第 8 条 |
| 其他业务域(家属卡、洗车、停车、票券包、权益规则、资讯) | 按 `AGENTS.md` 的域划分,不属于本次 |
---
## 6. 阶段与验收
| 阶段 | 产出 | 状态 |
|---|---|---|
| ① | 坑清单确认表 + 本大纲 | **已完成**(用户改由推导,不再人工确认) |
| ② | `docs/huifu/` 9 份 + `reference/official/` 10 份官方原件 + `scripts/probes/` 5 个探针 + `CONTEXT.md` 术语增量 + 凭据 issue | **已完成** |
| ③ | 交付前自检 | **已完成,结果如下** |
### ③ 自检实际结果(2026-09-18)
| 检查项 | 方法 | 结果 |
|---|---|---|
| 行数 | `wc -l` | 204 / 209 / 183 / 215 / 149 / 186 / 219 / 251 / 121。全部 ≤251,无一超标 |
| 凭据明文 | 正则扫 9 份文档(排除 `reference/official/`) | **通过**。唯一命中是 `08` 里讲解 `${ENV:default}` 写法本身,不是值。官方原件目录内含汇付自带的示例号(如 `6666000123123123`),属官方公开示例,不是我们的凭据 |
| Mermaid 语法 | 抽 6 个块 → `mmdc` **真渲染** | **6/6 渲染成功**(mermaid-cli 11.17.0)。不是肉眼核对 |
| 代码围栏配对 | 每份的总围栏行数为偶数 | 9/9 通过 |
| 保鲜头 | `grep '适用提交'` 每份必须命中 | 9/9 存在(位于标题下第 3 行) |
| commit 引用真实性 | 逐个 `git log -1` | 文档里只引了两个 commit:`6c42f8f`(HEAD)与 `723b756`,**都有效**。其余 7-hex 命中全是通道返回码(`23000003` 等)与 `capability-refresh` 的毫秒值 `1800000`,非 commit |
| 源码文件引用 | 逐个 `test -f` | 14 个被引用的源码文件 14/14 存在 |
| 术语一致性 | 扫 `CONTEXT.md` 的 `_Avoid_` 禁用词 | **通过**。全部命中都落在各术语表的「不要用」对照列里,正文无违规 |
| 探针交叉引用 | `07` 提到的 5 个探针名 vs 实际文件 | 5/5 一致 |
| 零改动确认 | `git status` | 只动了:`docs/huifu/`(新增)、`talent-card-server/scripts/probes/`(新增)、`.scratch/huifu-handover/`(新增)、`CONTEXT.md`(术语增量)、`DumpExcel.java`(因搬移同步路径)、10 份官方原件 `git mv`。**未触碰任何应用源码** |
### ③ 阶段与计划的偏离(都是发现事实后改的,记录在此)
1. **官方原件与自撰材料必须分开摆。** 原计划把 8 份「官方接口页摘录」一起迁入,实际核查发现 `official-contracts-2026-09-15.md` 与 `evidence-2026-09-15.md` **是我方自撰**(前者作者自己在头部列了 S1–S13 官方 URL,正文是二次转写),不是官方原件。若按原措辞落笔就是把「我们的归纳」冒充成「汇付原文」。已改为:`reference/official/`(10 份官方原件,含实物 Excel)+ 两份自撰材料留在原地、在 `reference/README.md` 里明确标注性质与引用规矩。
2. **多了一份官方原件。** 原计划漏了 `wx-fee-rule-id.xlsx`(官方《微信费率规则ID》Excel,仓库内实物)与 `config-query-doc.md`、`huifu-multifee-query-doc.md`,实际迁入 10 份而非 8 份。
3. **`DumpExcel.java` 的默认路径**写死了被搬走前的 xlsx 路径,搬移会让它指向不存在的文件,已同步改为 `docs/huifu/reference/official/wx-fee-rule-id.xlsx`。
4. **`CONTEXT.md` 的增量比计划小。** 计划补 4 条术语,实际核查发现 3 条已存在(微信支付开通、微信待实名、交易确认退款),且 3 条里 1 条超出 4 条上限,实际只补 3 条真正缺失的:**关单查询**、**渠道商**、**差错台账**。不灌水。
5. **`05` 只有 149 行**(目标 250)。不是漏内容:28 条反直觉用稠密表格承载,行数效率高;协议部分四小节齐全。不为凑行数灌水。
6. **探针无法端到端运行。** 5 个 Java 探针已用真实依赖 jar 完成 `javac` 编译(0 错误),其中 4 个已在测试库上实跑过只读查询、`CallbackUrlAuditProbe` 三种分支离线跑通;但**所有通道 HTTP 调用从未执行**(无凭据),5 个 `run-*.ps1` 的整链路也未跑过(会起 Maven 构建)。已在 `scripts/probes/README.md` 与探针注释里写明,未把这些说成「已验证」。
**③ 的自检怎么做**(不靠肉眼):
- 行数:`wc -l`
- 凭据明文:`grep -nE '(private-key|platform-public-key|system-id|merchant-id)\s*:\s*[^$]' docs/huifu/` 必须为空
- commit 引用真实性:把文档里所有 `[0-9a-f]{7}` 提取出来,逐个 `git cat-file -e` 验证
- 术语一致性:把文档里出现的禁用同义词(`CONTEXT.md` 的 `_Avoid_` 列表)grep 一遍
- Mermaid:抽出所有 ```mermaid 块,用 `mmdc` 渲染(若本机没有 npx 环境,退化为人工核对语法 + 张数)