Feature: specs/016-omg-payment | 状态: ✅ 方案A+B 后端已实现(2026-08-07,tasks T042–T048);测试(T049)/前端配合(T050)待后续 | 关联: api.md §A4/B5/B8
本文档解决一个核心隐患:异步回调不可靠导致漏单。当前后端是纯被动等回调,回调一旦丢失,用户付了钱订单却永远停在「未支付」。下面是问题分析 + 补单方案,留待后续实现。
| 场景 | 触发原因 | 当前覆盖 | 影响 |
|---|---|---|---|
| ① 回调延迟 | OMG 服务端 POST 晚到(网络抖动/排队) | OMG 自动重试(5–15分/次,当天最多4次);后端处理完即回 `1 | OK` |
| ② 回调丢失(漏单) | 后端宕机/重启/断网,OMG 重试4次仍未送达,之后不再重试 | ❌ 无任何兜底 | 🔴 用户钱扣了,订单永远 pay_status=0 |
漏单的真实触发条件:后端在 OMG 重试窗口(当天4次)内一直不可用。
OmgPayController.notify 只能等 OMG POST 过来,不主动核对。OmgPay.queryTrade()(调 QueryTradeInfo/V5,自带验签+关联校验,返回 TradeStatus 0未付/1已付/10200095失败)已实现,目前只用于门店凭证验证,没暴露成补单接口。POST /pay/omg/query(§B5)标了「可选」,未实现。TestTask.java 已废弃(全量废弃,含超时退款/抽成返还等),不能往里加。notify 在所有分支(成功/验签失败/金额不符/异常)都返回纯串 1|OK。这是 OMG 协议要求(回非 1|OK 会触发重试,但重试对校验类失败无用)。需区分两种「回调失败」:
| 情况 | 后端行为 | 正确性 |
|---|---|---|
| 校验失败(假回调/金额对不上) | 回 `1 | OK` 吞掉,不改订单 |
| 送达失败(后端没收到) | 无从回 `1 | OK`,OMG 重试4次后放弃 |
另:若
markSuccess成功后、推送前崩溃——订单状态已更新,无碍(推送漏发不影响,用户刷新订单即见已支付);若markSuccess之前崩溃,OMG 会重试时补上。死穴仅「重试窗口内后端持续不可用」。
前端结果页轮询时,订单长时间仍 payStatus=0 即调用,后端去 OMG 查真实状态并补单。
POST /pay/omg/query| 项 | 内容 |
|---|---|
| Method | POST |
| Header | token: <用户JWT> |
| 入参 | query:orderid = ddId |
| 鉴权 | 登录用户 + 订单本人 |
| 复用 | OmgPay.queryTrade() + IPosOrderOmgPaymentService.markSuccess() + handlePaymentSuccess()(同回调核销链路) |
返回
{ "code": 200, "data": { "payStatus": 1, "reconciled": true } }
// payStatus: 0未付 / 1已付(含本次补单) / 2失败 ; reconciled: 本次是否触发了补单
核心逻辑(实现时照此)
1. 校验登录 + 订单归属(token → userId == order.userId)
2. order.payType 必须为 "7";若 order.payStatus 已为 1 → 直接返回成功(不重复处理)
3. payment = paymentService.getLatestByDdId(ddId) // 最新一条 OMG 流水
4. 门店凭证 cred = storeOmgService.getEnabledCredential(order.mdId) → OmgPayConfig
5. resp = omgPay.queryTrade(baseUrl, cfg, payment.getMerchantTradeNo)
6. 按 resp.TradeStatus 分支:
"1" 已付:
- 先判断 payment.payStatus == 1 → 已被回调处理过,直接返回成功(幂等)
- 否则 markSuccess(payment.id, resp.TradeNo, resp.PaymentType, 1, ...) + handlePaymentSuccess(order)
- 返回 { payStatus:1, reconciled:true }
"0" 未付:
- 返回 { payStatus:0, reconciled:false } // 前端继续轮询(延期支付会长期是0,正常)
"10200095" / 其他:
- paymentService.markFail(...) ; 返回 { payStatus:2, reconciled:false }
补单与回调可能并发/先后到达。markSuccess 必须按 trade_no 幂等(回调侧已如此);补单前先查 payment.payStatus==1 则直接返回,避免重复 markSuccess + 重复推送。
防的是「用户付完就关 App、回调又丢了」的极端情况——用户不会再回来触发方案A。
新建独立定时任务(RuoYi quartz 或 @Scheduled,严禁改废弃的 TestTask.java):
pos_order_omg_payment 中 pay_status=0 且 create_time 在补单窗口内的流水,且对应订单未取消(state != 4)ExpireDate(1~3 天),TradeStatus 在用户实际缴款前一直是 0;窗口应覆盖到 ExpireDate 之后(如发起后 7 天),过期停止扫描避免无限轮询SELECT ... FOR UPDATE,避免同一流水被多节点同时补单(幂等能兜底,但减少无效调用)接入文档 frontend-integration.md §4.3 的结果页轮询,当轮询超过阈值(如 30s)仍 payStatus=0 时,改为主动调补单接口而非继续干等:
轮询 paymentInfo → payStatus=0 持续 > 30s
→ 调 POST /pay/omg/query?orderid= // 后端查 OMG 真实状态补单
→ 据返回 payStatus 展示成功/失败/继续等
(实现方案A后,回头更新 frontend-integration.md 的轮询段)
OmgPayController 加 POST /pay/omg/query(@Anonymous @Auth),复用 queryTrade + applyPaidResult(markSuccess+handlePaymentSuccess),严格幂等(T044)applyPaidResult()(notify 成功分支改调此) + reconcileByQuery()(/query 与定时任务共用,含跨事务中断自愈)(T043)OmgReconcileTask(@Scheduled 每3分钟 + Redisson 分布式锁)扫漏单,复用 reconcileByQuery(T046);scan mapper+service(T045);@EnableScheduling(T046);omg.reconcile.* 配置(T047)orderLogHelper.logSync(ddId, ..., "OMG查询补单成功/失败/金额不符"),便于对账frontend-integration.md §4.3 轮询逻辑接入 /query —— ⏸️ 推迟:客户 uni-app 不在当前工作区(同 T017/T024)| 能力 | 位置 | 说明 |
|---|---|---|
| 查询 OMG 交易 | OmgPay.queryTrade(baseUrl, cfg, merchantTradeNo) |
返回 Map(TradeStatus/TradeNo/TradeAmt/PaymentDate/PaymentType...),自带验签 |
| 核销成功 | IPosOrderOmgPaymentService.markSuccess(...) |
notify 侧已用,按 trade_no 幂等 |
| 标记失败 | IPosOrderOmgPaymentService.markFail(...) |
同上 |
| 支付成功业务 | OmgPayController.handlePaymentSuccess(order) |
更新订单 state=0/payStatus=1 + 日志 + 推送(私有方法,补单可复用) |
| 查流水 | paymentService.getLatestByDdId(ddId) |
拿最新一条 OMG 流水 |
| TradeStatus 枚举 | contracts/api.md §A4 | 0未付 / 1已付 / 10200095失败 |