Просмотр исходного кода

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 недель назад
Родитель
Сommit
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);
     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 用)。
      * 扫描补单窗口内未核销的漏单 ddId(定时补单方案B 用)。
      * 每订单取最新一笔 pay_status=0 的流水(older 发起视为放弃),关联 pos_order 排除已取消(state=4)。
      * 每订单取最新一笔 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);
     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 不重复更新)。
      * 标记支付成功(幂等:已 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);
         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
     @Override
     public int markSuccess(Long paymentId, String tradeNo, String payType, Integer rtnCode,
     public int markSuccess(Long paymentId, String tradeNo, String payType, Integer rtnCode,
                            String rtnMsg, String authCode, Date payTime, Date tradeDate, String callbackRaw) {
                            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"/>
         SELECT <include refid="paymentColumns"/>
         FROM pos_order_omg_payment
         FROM pos_order_omg_payment
         WHERE dd_id = #{ddId}
         WHERE dd_id = #{ddId}
-          AND pay_status = 1
+          AND pay_status IN (1,3,4)
           AND trade_no IS NOT NULL
           AND trade_no IS NOT NULL
           AND trade_no &lt;&gt; ''
           AND trade_no &lt;&gt; ''
         ORDER BY id DESC
         ORDER BY id DESC
@@ -52,23 +52,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
     </select>
 
 
     <!-- 扫描补单窗口内未核销漏单(每订单最新一笔 pay_status=0),关联订单排除已取消 state=4 -->
     <!-- 扫描补单窗口内未核销漏单(每订单最新一笔 pay_status=0),关联订单排除已取消 state=4 -->
+        <!-- 扫描补单窗口内有任意未核销(pay_status=0)流水的订单 ddId(多 MTN 订单全部未付行都进扫描,
+         不再只取 MAX(id) 一条,避免漏补较早 MTN),关联订单排除已取消 state=4 -->
     <select id="selectLeakOrderDdIds" resultType="java.lang.String">
     <select id="selectLeakOrderDdIds" resultType="java.lang.String">
-        SELECT p.dd_id
+        SELECT DISTINCT p.dd_id
         FROM pos_order_omg_payment p
         FROM pos_order_omg_payment p
         INNER JOIN pos_order o ON o.dd_id = p.dd_id
         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}
         LIMIT #{batchSize}
     </select>
     </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 id="markSuccessIfUnpaid">
         UPDATE pos_order_omg_payment
         UPDATE pos_order_omg_payment
         SET trade_no = #{tradeNo}, pay_status = 1, pay_type = #{payType},
         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 做换号/补单安全网,重复键仍抛会让安全网失效。
 - [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。
 - [ ] 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。
 - [ ] 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 接入。
 - [ ] 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**)。
 - [ ] 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**)。