# 汇付链路只读排查探针 这一批探针是为 **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;" -d target/probes <探针>.java` ——探针不在 `src/main/java` 下,所以**不进 Maven 编译**,靠脚本单独编译; 3. `java -cp "target/probes;target/classes;" <探针> <参数>` 两条与仓库环境有关的注意: - **同一 checkout 一次只跑一个 Maven 构建**。脚本每次都会起一个构建;已经构建过、不想到处撞车时加 `-SkipBuild`,它会复用 `target/probes/classpath.txt`。 - 建议用 `pwsh`(PowerShell 7)跑,理由见 `docs/agents/testing.md`。 ## 需要连通道时:配置文件 连通道的探针要先有 `system-id` / `product-id` / 私钥 / 平台公钥。**故意不读 `application.yml`**:那里面的 私钥与密钥是凭据,探针「读进来只为用、绝不打印」,就没有必要把它读进去。改成读一个由你自备的 properties 文件: ```powershell 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 —— 这笔钱通道收了吗 ```powershell .\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 —— 这家商户能不能收款 ```powershell .\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 —— 这笔订单的钱走到哪一步了 ```powershell .\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 —— 通道到底结了多少 ```powershell .\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 —— 五个回调地址自检 ```powershell .\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`,时区解释不同会整体偏一天,通道只会回「交易不存在」——那会看起来像「钱丢了」。 - 探针对通道只做**单次**查询,没有分页遍历:结算记录要翻页请缩小日期窗口;支付与商户信息本来就是单条查询。 - 本目录的探针**没有配套的自动化测试**(它们是排查工具,不是生产代码路径)。