10 KiB
Executable File
10 KiB
Executable File
支付网关容灾与对账体系设计 v3(主动查账 · 事件驱动 · 三级兜底 · 人工异常池)
版本:v3.0(2026-08-09) 设计约束:
- 临时隧道是防攻击特性,不引入固定域名;回调丢失是常态,主动查账是核心保障。
- 兜底分三级:① 自动查单补账(ConfirmScheduler)→ ② 退避重试(查单/通知)→ ③ 人工异常处理池(自动化兜不住时的最终兜底)。
- 所有资金写入(含人工处理)只走幂等入口(SettlePayment / CloseOrder),不绕过状态机、不手改账本。
- 高性能:事件驱动 + timer heap,不轮询;不出错:幂等 + 关单前查单 + 冲突进差错。
一、设计哲学
- 回调是优化,查单是保障 —— 快路径(回调入账)与慢路径(查单补账)殊途同归,都进幂等
SettlePayment。 - 数据经过网关,就有信息返回 —— 每个环节可自我验证,异常即触发兜底。
- 三级兜底:自动补偿 → 退避重试 → 人工异常池。自动化兜不住才进人工,人工操作也走同一幂等入口。
二、每环节"正常路径 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 标记)
-- 异常订单池(只增不改,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 链、同样触发通知、同样留审计;订单永久可追溯"是否人工处理过、谁处理的、依据什么"。