瀏覽代碼

fix(omg-pay): T053 reconcile 扫描改全量未付 + 读侧查询方法(账本重构数据层)

- selectLeakOrderDdIds:MAX(id) 取一条 → EXISTS(任意 pay_status=0 行),
  多 MTN 订单全部未付行进扫描(避免漏补较早 MTN,正是 id=26 漏补的根因)
- 新增 listUnpaidByDdId(全量 pay_status=0,含历史行,reconcile 遍历用)
- 新增 selectLatestRefundableByDdId(pay_status IN(1,3,4),退款/读侧用)
- 新增 existsPaidByDdId(校验用)
- selectLatestPaidByDdId:pay_status=1 → IN(1,3,4)(修退款中/已退返 null)
- Mapper.java / Service / impl 加对应方法

为 T054(reconcileByQuery 两段式遍历全量未付)铺数据层。is_active 维度
(T056/T057)未引入,listUnpaidByDdId 当前按 pay_status=0 全量,T057 后
仍不限 is_active(遍历全量未付含历史行)。

Co-Authored-By: Claude <noreply@anthropic.com>
qmj 2 周之前
父節點
當前提交
7871eb6688

+ 9 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java

@@ -26,6 +26,15 @@ public interface PosOrderOmgPaymentMapper extends BaseMapper<PosOrderOmgPayment>
 
     PosOrderOmgPayment selectLatestPaidByDdId(@Param("ddId") String ddId);
 
+    /** 全量未付流水(pay_status=0,含已轮换历史行),reconcile 遍历补单用。 */
+    List<PosOrderOmgPayment> listUnpaidByDdId(@Param("ddId") String ddId);
+
+    /** 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退),退款/读侧用。 */
+    PosOrderOmgPayment selectLatestRefundableByDdId(@Param("ddId") String ddId);
+
+    /** 是否存在已付/退款中/已退行(pay_status IN(1,3,4))。 */
+    int existsPaidByDdId(@Param("ddId") String ddId);
+
     /**
      * 扫描补单窗口内未核销的漏单 ddId(定时补单方案B 用)。
      * 每订单取最新一笔 pay_status=0 的流水(older 发起视为放弃),关联 pos_order 排除已取消(state=4)。

+ 9 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgPaymentService.java

@@ -28,6 +28,15 @@ public interface IPosOrderOmgPaymentService {
 
     PosOrderOmgPayment getLatestPaidByDdId(String ddId);
 
+    /** 全量未付流水(pay_status=0),reconcile 遍历补单用。 */
+    List<PosOrderOmgPayment> listUnpaidByDdId(String ddId);
+
+    /** 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退),退款/读侧用。 */
+    PosOrderOmgPayment getLatestRefundableByDdId(String ddId);
+
+    /** 是否存在已付/退款中/已退行。 */
+    boolean existsPaidByDdId(String ddId);
+
     /**
      * 标记支付成功(幂等:已 pay_status=1 返回 0 不重复更新)。
      *

+ 17 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImpl.java

@@ -58,6 +58,23 @@ public class PosOrderOmgPaymentServiceImpl implements IPosOrderOmgPaymentService
         return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectLatestPaidByDdId(ddId);
     }
 
+    @Override
+    public List<PosOrderOmgPayment> listUnpaidByDdId(String ddId) {
+        return ddId == null || ddId.isEmpty()
+                ? java.util.Collections.emptyList()
+                : posOrderOmgPaymentMapper.listUnpaidByDdId(ddId);
+    }
+
+    @Override
+    public PosOrderOmgPayment getLatestRefundableByDdId(String ddId) {
+        return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectLatestRefundableByDdId(ddId);
+    }
+
+    @Override
+    public boolean existsPaidByDdId(String ddId) {
+        return ddId != null && !ddId.isEmpty() && posOrderOmgPaymentMapper.existsPaidByDdId(ddId) > 0;
+    }
+
     @Override
     public int markSuccess(Long paymentId, String tradeNo, String payType, Integer rtnCode,
                            String rtnMsg, String authCode, Date payTime, Date tradeDate, String callbackRaw) {

+ 37 - 12
ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgPaymentMapper.xml

@@ -44,7 +44,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         SELECT <include refid="paymentColumns"/>
         FROM pos_order_omg_payment
         WHERE dd_id = #{ddId}
-          AND pay_status = 1
+          AND pay_status IN (1,3,4)
           AND trade_no IS NOT NULL
           AND trade_no &lt;&gt; ''
         ORDER BY id DESC
@@ -52,23 +52,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
 
     <!-- 扫描补单窗口内未核销漏单(每订单最新一笔 pay_status=0),关联订单排除已取消 state=4 -->
+        <!-- 扫描补单窗口内有任意未核销(pay_status=0)流水的订单 ddId(多 MTN 订单全部未付行都进扫描,
+         不再只取 MAX(id) 一条,避免漏补较早 MTN),关联订单排除已取消 state=4 -->
     <select id="selectLeakOrderDdIds" resultType="java.lang.String">
-        SELECT p.dd_id
+        SELECT DISTINCT p.dd_id
         FROM pos_order_omg_payment p
         INNER JOIN pos_order o ON o.dd_id = p.dd_id
-        INNER JOIN (
-            SELECT MAX(id) AS max_id
-            FROM pos_order_omg_payment
-            WHERE pay_status = 0
-              AND create_time &gt;= #{windowStart}
-              AND create_time &lt;= #{graceCutoff}
-            GROUP BY dd_id
-        ) latest ON latest.max_id = p.id
-        WHERE (o.state IS NULL OR o.state &lt;&gt; 4)
-        ORDER BY p.id ASC
+        WHERE EXISTS (
+            SELECT 1 FROM pos_order_omg_payment
+            WHERE dd_id = p.dd_id AND pay_status = 0
+              AND create_time >= #{windowStart}
+              AND create_time <= #{graceCutoff}
+        )
+        AND (o.state IS NULL OR o.state <> 4)
+        ORDER BY p.dd_id ASC
         LIMIT #{batchSize}
     </select>
 
+    <!-- 全量未付流水(pay_status=0,is_active 不限,含已轮换历史行)供 reconcileByQuery 遍历补单 -->
+    <select id="listUnpaidByDdId" parameterType="String"
+            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
+        SELECT <include refid="paymentColumns"/>
+        FROM pos_order_omg_payment
+        WHERE dd_id = #{ddId} AND pay_status = 0
+        ORDER BY id ASC
+    </select>
+
+    <!-- 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退) -->
+    <select id="selectLatestRefundableByDdId" parameterType="String"
+            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
+        SELECT <include refid="paymentColumns"/>
+        FROM pos_order_omg_payment
+        WHERE dd_id = #{ddId} AND pay_status IN (1,3,4)
+        ORDER BY id DESC
+        LIMIT 1
+    </select>
+
+    <!-- 是否存在已付/退款中/已退行(校验用) -->
+    <select id="existsPaidByDdId" parameterType="String" resultType="int">
+        SELECT COUNT(*) FROM pos_order_omg_payment
+        WHERE dd_id = #{ddId} AND pay_status IN (1,3,4)
+    </select>
+
     <update id="markSuccessIfUnpaid">
         UPDATE pos_order_omg_payment
         SET trade_no = #{tradeNo}, pay_status = 1, pay_type = #{payType},

+ 1 - 1
specs/016-omg-payment/tasks.md

@@ -180,7 +180,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 - [x] T051 [P] **前置:parseKvResponse 重复键放宽**(`ruoyi-admin/.../utils/omg/OmgPay.java`):重复键 last-wins 不抛(用同一份 Map 重算 CheckMacValue 验签,避 kill-shot ⑥ 解析放宽与验签打架);仅整段无 `=` 或缺 CheckMacValue 才抛;`OmgPayTest` 补重复键用例。S 依赖 queryTrade 做换号/补单安全网,重复键仍抛会让安全网失效。
 - [ ] T052 **collation 对齐执行确认**:执行 `updatesql/sql.md` 2026-08-12 节(pos_order_omg_payment/pos_order_omg_refund/pos_store_omg CONVERT 到 utf8mb4_unicode_ci);执行后 `selectLeakOrderDdIds` JOIN 不再报 Illegal mix of collations。
-- [ ] T053 **reconcile 扫描改全量未付**(`PosOrderOmgPaymentMapper.xml` + Service):`selectLeakOrderDdIds` 改 `EXISTS(任意 pay_status=0 行)` 去重 ddId(不再 MAX(id) 取一条);新增 `listUnpaidByDdId`(全量 pay_status=0,is_active 不限);`selectLatestPaidByDdId` 扩 `IN(1,3,4)`(修退款中/已退返 null);新增 `selectLatestRefundableByDdId`(pay_status IN(1,3,4))、`existsPaidByDdId`。
+- [x] T053 **reconcile 扫描改全量未付**(`PosOrderOmgPaymentMapper.xml` + Service):`selectLeakOrderDdIds` 改 `EXISTS(任意 pay_status=0 行)` 去重 ddId(不再 MAX(id) 取一条);新增 `listUnpaidByDdId`(全量 pay_status=0,is_active 不限);`selectLatestPaidByDdId` 扩 `IN(1,3,4)`(修退款中/已退返 null);新增 `selectLatestRefundableByDdId`(pay_status IN(1,3,4))、`existsPaidByDdId`。
 - [ ] T054 **reconcileByQuery 两段式**(`OmgPayController.reconcileByQuery`):① 先 selectLatestPaidByDdId(pay_status=1 且 order.payStatus=0 → handlePaymentSuccess 自愈,L774 不能变死代码);② 否则 listUnpaidByDdId 遍历全量未付行 queryTrade:TradeStatus=1 applyPaidResult break / 0 no-op / 10200095 且 trade_no IS NULL markFail / 10200095 且 trade_no 非空 no-op。
 - [ ] T055 **三道节流闸(防 OMG 403)**(新建 queryTrade 节流组件 `ruoyi-admin/.../utils/omg/OmgQueryThrottle.java` + Redis):首查跳过 create_time<NOW()-INTERVAL 40 MINUTE;per-MerchantID 令牌桶 `omg:qt:token:{merchantId}`(1token/3s,burst1)acquire 失败 skip 本行本轮;per-ddId TTL(`omg:reconcile:dd:{ddId}` 180s / `omg:query:dd:{ddId}` 60s)。reconcileByQuery 与 /query 接入。
 - [ ] T056 **DDL 追加式 S 字段**(写 `updatesql/sql.md`,不直接执行):`pos_order_omg_payment` + `expire_date DATETIME NULL`(仅 paymentInfo 回调写,create 不猜)+ `is_active TINYINT NOT NULL DEFAULT 1` + 生成列 `active_dd_id=IF(is_active=1 AND pay_status=0,dd_id,NULL) VIRTUAL` + `UNIQUE uk_omg_active_dd(active_dd_id)`(需 MySQL 5.7.6+,**生产版本待确认**;不够则退化 `idx_omg_dd_active(dd_id,is_active,pay_status)`+app 锁+CAS);历史回填(每 ddId 最新 pay_status=0 置 is_active=1,其余 0;终态行 0;**绝不回填 expire_date**)。