pay-querylisted
Install: claude install-skill 1398281322-a11y/java-backend-guardrails
# 支付查单 / 丢回调
## When to Invoke
统一下单超时、用户付了但订单仍待支付、渠道 notify 丢失、面试「回调丢了怎么办」。
## When NOT
回调本身怎么写 → `pay-notify`。对账文件 → `reconciliation`。超时关单 → `delay-job`(关单前必须先查单)。
## 风险(面试考点)
支付请求超时 **只表示没收到应答,渠道可能已扣款**。再换一个 `out_trade_no` 重下单 = 用户付两笔。
不能只靠回调。生产三道:异步 notify + **主动查单** + 日终对账。缺任何一道都会漏单或错账。
关单任务若不管渠道状态,会把已付款订单关掉,然后回调到来变成「已取消却收了钱」。
## 方案选型(轻量优先)
| 时机 | 动作 |
|------|------|
| 下单/支付 HTTP 超时 | 用 **同一个** out_trade_no 调渠道 query |
| 用户回前台 | 查本地,仍 PAYING 则再 query 一次 |
| 定时 | 扫 PAYING 超过 N 分钟(如 2~5 分钟)的支付单,query |
| 关单前 | 先 query,已支付则推进 SUCCESS,不要关 |
| T+1 | 对账兜底 |
Query 结果当「官方回调」走同一套入账逻辑(验金额、唯一流水、状态机),不要写第二套。
## 默认方案
```java
public void compensate(PayTrade t) {
ChannelQuery q = channel.query(t.getOutTradeNo()); // 超时当未知,有限重试
if (q.isSuccess()) {
payNotifyService.applySuccess(q); // 复用入账
} else if (q.isClosedOrNotPay() && t.expired()) {
payTradeService.markClosed(t.getOutTradeNo());
}
// PROCESSING / UNKNOWN → 下次再扫
}
```
关单:
```text
到期 → query
SUCCESS → 走入账(可能回调晚到)
NOTPAY → 关支付单 + 关订单 CREATED
UNKNOWN → 不关,继续 PAYING
```
## 反例
错误:超时立刻 `out_trade_no = UUID` 再调统一下单。
正确:同一单号查单或关旧单后再开新单(产品允许时)。
错误:只扫「创建超过 30 分钟」直接关,不 query。
正确:先问渠道。
错误:查单成功自己写一套 update,和回调逻辑分叉。
正确:同一 applySuccess。
## 验证
- 模拟 notify 丢失,补偿任务能把订单打成已支付。
- 超时后没有第二笔渠道订单(除非产品明确允许)。
- 关单窗口内付款,订单不会停在 CLOSED 且钱在渠道。
## 评审清单
- [ ] 超时当未知
- [ ] PAYING 有补偿扫描
- [ ] 关单前查单
- [ ] 查单与回调共用入账
---