Files

174 lines
10 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 支付网关容灾与对账体系设计 v3(主动查账 · 事件驱动 · 三级兜底 · 人工异常池)
> 版本:v3.0(2026-08-09)
> 设计约束:
> - 临时隧道是防攻击特性,不引入固定域名;回调丢失是常态,主动查账是核心保障。
> - 兜底分**三级**:① 自动查单补账(ConfirmScheduler)→ ② 退避重试(查单/通知)→ ③ **人工异常处理池**(自动化兜不住时的最终兜底)。
> - 所有资金写入(含人工处理)只走幂等入口(SettlePayment / CloseOrder),不绕过状态机、不手改账本。
> - 高性能:事件驱动 + timer heap,不轮询;不出错:幂等 + 关单前查单 + 冲突进差错。
---
## 一、设计哲学
1. **回调是优化,查单是保障** —— 快路径(回调入账)与慢路径(查单补账)殊途同归,都进幂等 `SettlePayment`。
2. **数据经过网关,就有信息返回** —— 每个环节可自我验证,异常即触发兜底。
3. **三级兜底**:自动补偿 → 退避重试 → **人工异常池**。自动化兜不住才进人工,人工操作也走同一幂等入口。
---
## 二、每环节"正常路径 vs 兜底"
| 环节 | 正常 | 异常→兜底触发 |
|------|------|----------------|
| ① 上游→网关(下单) | 验签→建单→返回凭据 | 验签/参数失败→拒绝并记录(无资金影响) |
| ② 用户→支付宝(支付) | 回调→入账(快路径) | **回调丢失→ConfirmScheduler 到期→查单补账(慢路径)** |
| ③ 网关→上游(通知) | 通知 200→标记 | 失败→outbox 重试 31h→dead→人工重发 |
| ④ 日终对账 | 一致 | 不一致→差错单→人工 |
| ⑤ **任何环节异常不明** | — | **进人工异常池**(兜底链终点) |
---
## 三、核心模块:ConfirmScheduler(订单确认调度器)
内存 min-heap(按 nextCheckAt 排序),事件驱动,**非轮询**。
```
OnOrderCreated → push(now + T1 60s)
OnCallbackReceived → remove + SettlePayment(幂等)
到期 Confirm(tradeNo):
├─ 已 SUCCESS → remove
├─ 仍 WAIT 未到关单时间 → QueryOrder 查单
│ ├─ TRADE_SUCCESS → SettlePayment 补账 → 通知 → remove
│ ├─ 未支付 → push(+30s~5m 自适应)
│ └─ 查单失败 → push(+退避),连续失败 N 次 → 进异常池(query_failed)
└─ 到关单时间 → 最后查一次:已支付补账;未支付 CloseOrder;查单失败 → 推迟关单 + 进异常池
```
冲突(查单金额≠订单金额、状态矛盾)→ **不自动入账** → 进异常池(amount_mismatch / state_conflict)。
---
## 四、人工异常处理池(Manual Exception Pool)★新增核心
### 4.1 定位
**自动化兜底失败后的最终兜底**:任何"不按正常流程"的订单(回调全丢且查单不明、金额矛盾、关单存疑、通知 dead 无法重发)自动进入本池。人工核对证据 → 按流程填写必要信息 → 提交后经幂等入口恢复为正常订单,**订单永久标记"人工处理"**,全程审计。
### 4.2 数据模型(新表 + orders 标记)
```sql
-- 异常订单池(只增不改,resolved 后可 reopen,不可删除)
CREATE TABLE exception_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
out_trade_no TEXT NOT NULL UNIQUE, -- 关联订单
pid TEXT, upstream_name TEXT, channel TEXT,
amount_minor INTEGER, -- 网关记录金额(分)
order_status TEXT, -- 当前订单状态
channel_status TEXT, -- 最近查单结果(TRADE_SUCCESS/CLOSED/QUERY_FAILED/UNKNOWN)
channel_tx_id TEXT, -- 查单拿到的通道交易号
paid_amount INTEGER, -- 人工确认的实际支付金额(默认=amount_minor)
payer TEXT, -- 人工确认的付款方
reason TEXT, -- query_failed / amount_mismatch / state_conflict / close_uncertain / notify_dead / manual
phase TEXT DEFAULT 'pending', -- pending / processing / resolved
resolved_action TEXT, -- settle / close / noop / refund
evidence TEXT, -- 证据时间线 JSON(自动收集,不可手改)
suggestion TEXT, -- 系统建议动作(依据最近查单结果)
handler TEXT, handler_note TEXT,
resolved_at TEXT, updated_at TEXT,
created_at TEXT
);
-- orders 表新增人工处理标记(列表/详情可见,审计可追溯)
ALTER TABLE orders ADD COLUMN exception_id INTEGER DEFAULT 0; -- 关联异常池
ALTER TABLE orders ADD COLUMN handled_by TEXT DEFAULT ''; -- 处理人
ALTER TABLE orders ADD COLUMN handled_action TEXT DEFAULT ''; -- manual_settle / manual_close / noop
ALTER TABLE orders ADD COLUMN handled_at TEXT DEFAULT '';
ALTER TABLE orders ADD COLUMN handled_note TEXT DEFAULT '';
-- 操作审计(谁在何时对哪个异常单做了什么)
CREATE TABLE exception_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exception_id INTEGER, action TEXT, operator TEXT, detail TEXT, created_at TEXT
);
```
### 4.3 自动进池触发点(ConfirmScheduler / notify 集成)
| 触发 | reason | 进入时自动收集的证据 |
|------|--------|----------------------|
| 查单连续失败 ≥ `query_fail_alert`(5 次) | query_failed | 下单/回调/通知记录 + 每次查单结果时间线 |
| 查单金额 ≠ 订单金额 | amount_mismatch | 查单快照(金额、交易号、时间) |
| 回调与查单状态矛盾 | state_conflict | 两方原始数据 |
| 关单时查单失败(存疑) | close_uncertain | 查单错误信息 |
| 通知 dead 且重发仍失败 | notify_dead | 通知任务重试历史 |
| 管理后台手动加入 | manual | 人工填写的说明 |
进池时同时**暂停 ConfirmScheduler 对该订单的自动动作**(避免人工处理与自动查单打架),订单转入"待人工"。
### 4.4 人工处理流程(管理后台「异常订单」页)
```
① 列表(筛选 phase/reason/上游/日期)→ 打开详情
② 详情:基本信息 + 证据时间线(自动收集,只读)+ 系统建议动作
(可点「再次查单」刷新证据:调用通道查单,把最新结果并入时间线)
③ 填写处理表单:
动作:settle(确认入账)/ close(确认未支付,关单)/ noop(无需处理)
实际支付金额(默认=订单金额,可改)
通道交易号(可从支付宝账单人工核对后填入)
付款方(可选)
备注
④ 提交 → 后端校验(金额>0、订单未处理过、动作合法、表单与证据不冲突)
→ 执行:
settle → Service.SettlePayment(幂等,参数含人工金额/交易号/付款方/来源=manual)
→ 成功自动创建通知任务 → 通知上游到账
close → Service.CloseOrder(幂等,单笔)
noop → 仅记录
→ 成功:orders 写 handled_* 标记 + exception_orders 置 resolved + exception_logs 记审计
⑤ 该订单在列表/详情永久显示「⚠️ 人工处理」徽标(含处理人/时间/备注/动作)
```
### 4.5 防呆与安全
| 项 | 设计 |
|----|------|
| 并发防重 | 处理时 `phase=pending→processing` 行锁,同一订单不可并发处理;resolved 后不可再处理(可管理员 reopen) |
| 幂等入口 | settle/close 一律走 Service 幂等方法,**绝不直接 UPDATE 订单状态/金额** |
| 金额校验 | 人工金额必须 >0 且为整数分;与查单金额不一致时强制二次确认(输入框要求重复输入) |
| 证据只读 | evidence 自动收集,人工不可编辑(防掩盖);人工信息只写在 paid_amount/payer/handler_note |
| 权限 | 异常池操作走管理后台认证;敏感动作(settle)可加二次确认口令(可配置) |
| 审计 | exception_logs 全程留痕:谁、何时、看了什么、填了什么、做了什么 |
| 账本 | settle 走 SettlePayment 自动过账本 hash 链;人工无法绕账本 |
### 4.6 与上游通知不到的联动
人工 settle 成功后自动创建通知任务 → 若上游仍不可达 → outbox 重试 31h → dead → 此时订单已 SUCCESS,**dead 通知单独成清单**("上游未确认"),可在异常池关联查看/重发;上游也可主动调 `/api.php` 查单(网关数据正确,拉取是上游的事)。
---
## 五、分阶段落地
| 阶段 | 内容 | 说明 |
|------|------|------|
| **P0-1** | 通知重试增强(31h 窗口)+ dead 管理 API | 修复"付钱未到账" |
| **P0-2** | ConfirmScheduler 最小版(timer heap + 查单补账 + 关单前查单) | 回调丢失自动兜底 |
| **P0-3** | systemd 守护(gateway+new-api)+ 每日 sqlite 备份 | 进程/数据安全 |
| **P1-1** | **人工异常处理池**(表 + 进池触发 + 处理流程 + 管理后台页) | 最终兜底 + 人工台 |
| **P1-2** | 启动配置自检(沙箱/正式、密钥、回调地址) | is_prod 错配早暴露 |
| **P1-3** | 告警(Bark/Webhook:查单连续失败、验签失败率、dead、账本失败、**进池事件**) | 可观测 |
| **P2-1** | 日终对账报告(网关 vs 通道 vs 上游) | 资金最终核对 |
> **存量订单 116**:P0-2 落地后由 ConfirmScheduler 自然发现;若查单亦失败(极端),P1-1 人工池接管——人工从支付宝账单核对后 settle 恢复,标记人工处理。
---
## 六、模块与现有代码对应
| 新组件 | 复用 | 新增 |
|--------|------|------|
| `reconciler.ConfirmScheduler` | `Channel.QueryOrder`、`Service.SettlePayment`、`CloseExpiredOrders` | timer heap、事件接口 |
| `reconciler.ExceptionPool` | admin 框架、审计日志模式、SettlePayment/CloseOrder | exception 表、进池触发、处理流程 API |
| notify 增强 | `notify.Worker`、`notify_tasks` | 重试参数、dead 管理 API |
| admin 页面 | admin 框架 | 异常订单池 Vue 页(证据时间线 + 处理表单) |
| systemd + 备份 | gateway_autostart.sh | service 单元、备份脚本 |
**核心承诺**:所有资金写入(自动或人工)只走 `SettlePayment` / `CloseOrder` 等幂等方法;人工处理同样过账本 hash 链、同样触发通知、同样留审计;订单永久可追溯"是否人工处理过、谁处理的、依据什么"。