汇付链路只读排查探针

这一批探针是为 2026-09-11 ~ 2026-09-18 完成的汇付天下/斗拱支付链路交接准备的:遇到线上问题时,
不用回来找人,用它直接回答「这笔钱 / 这个商户在通道侧到底是什么状态」。

硬约束(先读这一节)

1. 只读。 允许调用的只有两类:

  • 本地数据库:**只 SELECT**。
  • 通道:**查单**(v3/trade/payment/scanpay/query)、**关单查询**(v2/trade/payment/scanpay/closequery)、
    商户信息查询(v2/merchant/basicdata/query)、**结算记录查询**(v2/merchant/basicdata/settlement/query)。

严禁:发起支付、关单、退款、交易确认、交易确认退款、下发业务开通、上传图片、以及任何 INSERT/UPDATE/DELETE
(包括「只是顺手刷新一下能力快照」——那是写库,属于 MerchantPaymentReadinessService.refresh 的活,
要走后台的「立即核实」)。

这不是口头约定:这五个探针的代码里**没有任何一条写路径**。MerchantCapabilityProbe 特意绕开了
refresh / capability / refreshQuietly / merchantVerify 这四个会写快照的方法,只用只读的
readiness 与 channelCapabilityNormal。

2. 生产保护。 探针启动时读汇付配置里的 app.payment.huifu.environment(等价于运行时的 profile 判断);
判断为 prod 时打印显著警告。它**只是提示、不是闸门**——因为没有写路径,环境不改变探针能不能跑,
只改变你该不该紧张。用 --huifu-environment=prod 可以直接声明环境(不必给整个配置文件)。

3. 输出脱敏。 以下内容**绝对不打印**:

不打印 本探针的处理
私钥、密钥 从不读进内存。只输出「已配置 / 未配置」(布尔)
完整商户号、完整汇付号 只留**前 6 位** + ***
完整银行卡号 从不选取;结算记录里的卡号类字段整个替换成 ***;本地只印 card_masked 与尾号 4 位
完整公网 IP 只留主机段**前两段** + x.x,端口与路径照常打印(排查要看的正是这两段)
用户 openid、签名材料 回执打印前统一替换成 ***

4. 参数从命令行传。 五个探针与五个运行脚本里**没有任何写死的目标**:不给 --db-url 就不知道连哪个库,
不给 --order-no 就不知道查哪一单,缺参数一律打印用法并以退出码 2 退出,绝不拿默认值瞎猜。

反面教材就在本仓库:.scratch/ 下早先会话留下的一次性探针(*Probe*.java、Inspect*.java)大多把
JDBC URL 写死成 127.0.0.1:8134/talent_card。**那个地址上放的是 2026-09-16 把本机库并入服务器库之前
的旧副本,不是开发库**(见 docs/agents/testing.md)。本目录的探针不接受任何默认地址。

目录内容

文件 作用
PaymentStateProbe.java / run-PaymentStateProbe.ps1 一笔支付:本地状态 vs 通道真实状态,指出差异
MerchantCapabilityProbe.java / run-MerchantCapabilityProbe.ps1 一个商户:本地能力快照 vs 通道真值,并算「能不能收款」
OrderFundBoundaryProbe.java / run-OrderFundBoundaryProbe.ps1 一笔订单:资金边界三列 + 支付/退款/核销/对账问题记录
SettlementRecordProbe.java / run-SettlementRecordProbe.ps1 一个商户按日期区间:通道结算记录
CallbackUrlAuditProbe.java / run-CallbackUrlAuditProbe.ps1 五个回调地址:哪个空、哪个是推导出来的
huifu-probe.properties.example 需要连通道时用的配置文件模板

运行机制

与 docs/audits/2026-09-10/run-probe.ps1 完全一致,没有另创一套:

  1. mvn -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=target/probes/classpath.txt -Dmdep.includeScope=test
    拿到 target/classes 与依赖 classpath;
  2. javac -encoding UTF-8 -cp "target/classes;<cp>" -d target/probes <探针>.java
    ——探针不在 src/main/java 下,所以**不进 Maven 编译**,靠脚本单独编译;
  3. java -cp "target/probes;target/classes;<cp>" <探针> <参数>

两条与仓库环境有关的注意:

  • 同一 checkout 一次只跑一个 Maven 构建。脚本每次都会起一个构建;已经构建过、不想到处撞车时加
    -SkipBuild,它会复用 target/probes/classpath.txt。
  • 建议用 pwsh(PowerShell 7)跑,理由见 docs/agents/testing.md。

需要连通道时:配置文件

连通道的探针要先有 system-id / product-id / 私钥 / 平台公钥。**故意不读 application.yml**:那里面的
私钥与密钥是凭据,探针「读进来只为用、绝不打印」,就没有必要把它读进去。改成读一个由你自备的 properties 文件:

Copy-Item scripts\probes\huifu-probe.properties.example scripts\probes\huifu-probe.properties
# 填好后:huifu-probe.properties 是凭据文件,不要提交进仓库

键名与 application.yml 里 app.payment.huifu.* 一致,前缀可写可不写
(system-id 与 app.payment.huifu.system-id 都认)。enabled 不给时按 true 处理,
否则网关自己的 ensureConfigured 会把查询拦住。

五个探针怎么用

PaymentStateProbe —— 这笔钱通道收了吗

.\scripts\probes\run-PaymentStateProbe.ps1 -PaymentNo HF20260917001 `
    -DbUrl $env:PROBE_DB_URL -DbUser root -DbPassword 123456 -Config .\scripts\probes\huifu-probe.properties

打印 payment_transaction 的 status / funds_confirmation_status / 金额 / 幂等键 / attempt_no /
provider_huifu_id / close_attempts / last_reconciled_at 与 funds_* 证据列,再问一次通道
(org_req_date 取自 created_at,org_hf_seq_id 取自 provider_transaction_no),然后逐条列出差异:
通道已收款而本地没落成已支付、金额不一致、收款子商户漂移、通道说「交易不存在」(23000001)等。
本地状态是 CLOSING / CANCEL_UNCONFIRMED 时,会再问一次**关单查询**(关单在通道侧是异步的,
结论只能靠这条查询)。

注意:本地 provider_huifu_id 为空(V131 之前的支付,由平台商户收款)时,按
HuifuRefundService.refundHuifuId 的同一条回退,用配置里的平台商户号去问通道——否则那批历史流水永远问不出结论。

MerchantCapabilityProbe —— 这家商户能不能收款

.\scripts\probes\run-MerchantCapabilityProbe.ps1 -ApplicationId 128 `
    -DbUrl $env:PROBE_DB_URL -DbUser root -DbPassword 123456 -Config .\scripts\probes\huifu-probe.properties

定位方式三选一:-ApplicationId / -HuifuId / -MerchantId(可加 -StoreId)。
打印本地快照(merchant_stat / auto_settle_status / cash_switch_status / cash_type /
capability_verified_at / capability_response_*)与通道 v2/merchant/basicdata/query 的真值,
逐列指出差异,再用 MerchantPaymentReadinessService.readiness(merchantId, storeId) 给出**能不能收款**
的结论与原因——那句文案与用户下单被拒时的文案同源。

两个容易看错的点,探针会直接说明:

  • capability_verified_at 为空 = 本地**从没核实过**通道状态。在要求通道确认的部署上,闸门会直接拒
    「商户收款能力待确认」,与通道那边实际是否正常**无关**——这就是「通道正常却收不了款」最常见的原因。
  • auto_settle_status 已经不是收款闸门(未核销的钱由延迟户兜住),只作参考,不要拿它下结论。

不给 -Config 时也会给结论,但判定会退化成「跳过通道确认」那一档(merchant_capability_required=false),
与线上默认不一致,探针会明确警告。

OrderFundBoundaryProbe —— 这笔订单的钱走到哪一步了

.\scripts\probes\run-OrderFundBoundaryProbe.ps1 -OrderNo ORD20260917001 `
    -DbUrl $env:PROBE_DB_URL -DbUser root -DbPassword 123456

打印 benefit_order 的资金边界三列(order_status / verify_status / refund_status)、创建时固定的收款方
(collect_onboarding_id / collect_store_id / collect_huifu_id)、全部支付流水、全部退款记录、核销记录,
以及 payment_reconciliation_issue 里按订单号与流水号/退款号命中的问题记录。

它**不问通道**,因此不需要 -Config(给了只用来看 environment 打生产警告)。三列的口径:
钱进商户基本户要 order_status=1 且 verify_status=1 且 refund_status=0(资金确认重试就是这么筛的);
退款在途(refund_status=1)会让整单退出可核销与可确认范围。

SettlementRecordProbe —— 通道到底结了多少

.\scripts\probes\run-SettlementRecordProbe.ps1 -HuifuId 6666000241335656 `
    -BeginDate 20260911 -EndDate 20260918 -Config .\scripts\probes\huifu-probe.properties

直查 /v2/merchant/basicdata/settlement/query。**pageSize 被钉在 1..50**:通道对 pageSize 的硬上限就是 50,
超了直接返回 00000003「…最大值为50」而且**一条记录都拿不到**(后台此前默认传 100,于是「结算记录」页
永远空白,而通道里其实有流水)。要看得更多只能**缩小日期窗口**,通道不返回总页数。

CallbackUrlAuditProbe —— 五个回调地址自检

.\scripts\probes\run-CallbackUrlAuditProbe.ps1 -Config .\scripts\probes\huifu-probe.properties

逐个打印五个回调地址(脱敏后)与状态:**显式配置 / 由 notify-url 推导 / 为空**,并明确列出
「为空的地址(回调未启用)」。取值规则直接调用应用里那份 HuifuCallbackUrlAudit(同一个类、同一批 getter),
所以不会与启动日志里的结论不一致。

一句最容易被误解的话:**某个回调地址留空不等于「不回调」**。四个非支付回调在配置为空时,会由 notify-url
推导出同主机同前缀的地址(HuifuPaymentProperties#callbackUrl);真正的「回调未启用」只有一种,
那就是这个地址算出来是空串。探针还会标两件事:路径与 PaymentCallbackController 暴露的五个端点对不上
(会 404)、以及某个地址的主机与 notify-url 不同(那台机器也必须能收到并处理这个回调)。

已知边界与坑

  • 验证到什么程度(2026-09-18):五个探针都用 javac 对 target/classes + 依赖 jar 编译通过;
    四个读库的探针在**测试库**(见 docs/agents/testing.md)上实跑过——本地快照、支付/退款/核销/问题记录、
    能力判定、缺参数退出码,都是真实执行的结果。**通道查询没有实跑过**(没有提供凭据,也不该在排查之外
    凭空打通道):PaymentStateProbe 的查单与关单查询、MerchantCapabilityProbe 的商户信息查询、
    SettlementRecordProbe 的结算记录查询都只做到「编译通过 + 请求参数装配正确」。第一次用可以拿一个
    已知答案的单子先试一次,对不上再回来看这一节。
  • 开发库的结构不由迁移维护(flyway.enabled=false)。缺列的表现是探针里那条查询直接报错,
    而不是少打印几个字段。看到 Unknown column 就说明该库还缺某条迁移的等价 DDL,
    按 docs/agents/testing.md 与 scripts/dev-schema-*.sql 补。PaymentStateProbe 的 funds_* 证据列
    单独一条查询取并容忍失败,就是为了不让缺列连累主结论。
  • 口令会出现在进程列表里:-DbPassword 是命令行参数。共享机器上排查时自己注意。
  • --db-url 建议带上 serverTimezone=Asia/Shanghai(应用就是这么配的)。PaymentStateProbe 靠
    created_at 推 org_req_date,时区解释不同会整体偏一天,通道只会回「交易不存在」——那会看起来像「钱丢了」。
  • 探针对通道只做**单次**查询,没有分页遍历:结算记录要翻页请缩小日期窗口;支付与商户信息本来就是单条查询。
  • 本目录的探针**没有配套的自动化测试**(它们是排查工具,不是生产代码路径)。