Răsfoiți Sursa

@
refactor: saveBillBase 改用 INSERT ON CONFLICT 原子 upsert

替代 select-then-insert-or-update + DuplicateKeyException catch 方案。
PostgreSQL 原生 ON CONFLICT (pay_no) DO UPDATE,数据库层面保证:
- 无论 COLLECT/ASSETS_UPDATE 到达顺序,最后一个写入的状态为最终状态
- 零竞态窗口,消除异常时序下的数据正确性风险
@

alphaH 9 ore în urmă
părinte
comite
5a8b63caf5

+ 3 - 21
java/src/main/java/com/payment/platform/module/payment/notification/handler/BillHandler.java

@@ -28,7 +28,6 @@ import com.payment.platform.module.payment.notification.mapper.PayBillVoucherMap
 import com.payment.platform.module.payment.openapi.service.OpenapiService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
@@ -199,6 +198,7 @@ public class BillHandler extends BaseNotifyHandler {
                                String expenseRuleGroupId, String expenseSceneCode, String expenseType) {
         if (StrUtil.isBlank(payNo)) return;
 
+        // 先查现有记录(用于保留某些字段),不存在则 new
         PayBillEntity existing = payBillMapper.selectOne(
                 new LambdaQueryWrapper<PayBillEntity>().eq(PayBillEntity::getPayNo, payNo));
         PayBillEntity bill = (existing != null) ? existing : new PayBillEntity();
@@ -214,12 +214,10 @@ public class BillHandler extends BaseNotifyHandler {
             if (parsed != null) {
                 bill.setGmtBizCreate(parsed);
             } else if (existing == null) {
-                // 通知中 gmt_biz_create 格式无法解析时,新记录使用当前时间兜底
                 bill.setGmtBizCreate(OffsetDateTime.now());
                 log.warn("账单 gmt_biz_create 解析失败,使用当前时间兜底: pay_no={}, raw={}", payNo, gmtBizCreate);
             }
         } else if (existing == null) {
-            // 通知中未提供 gmt_biz_create 时,新记录使用当前时间兜底
             bill.setGmtBizCreate(OffsetDateTime.now());
         }
         if (StrUtil.isNotBlank(gmtRecievePay)) bill.setGmtRecievePay(toOffsetDateTime(gmtRecievePay));
@@ -232,28 +230,12 @@ public class BillHandler extends BaseNotifyHandler {
         bill.setExpenseType(expenseType);
         bill.setStatus("NEW");
 
-        // 支付宝通知回调无认证上下文,需根据 enterprise_id 查 tenant_id
         if (existing == null && bill.getTenantId() == null) {
             bill.setTenantId(resolveTenantId(enterpriseId));
         }
 
-        if (existing != null) {
-            payBillMapper.updateById(bill);
-        } else {
-            try {
-                payBillMapper.insert(bill);
-            } catch (DuplicateKeyException e) {
-                // 并发:另一条通知(同一pay_no)抢先插入 → 重新查出已存在记录并更新
-                PayBillEntity dup = payBillMapper.selectOne(
-                        new LambdaQueryWrapper<PayBillEntity>().eq(PayBillEntity::getPayNo, payNo));
-                if (dup != null) {
-                    bill.setId(dup.getId());
-                    payBillMapper.updateById(bill);
-                } else {
-                    throw e;
-                }
-            }
-        }
+        // 原子 upsert:INSERT ON CONFLICT DO UPDATE,并发安全
+        payBillMapper.upsert(bill);
         log.info("保存账单基础数据: pay_no={}, type={}, amount={}, employee_id={}",
                 payNo, consumeType, consumeAmount, employeeId);
     }

+ 39 - 0
java/src/main/java/com/payment/platform/module/payment/notification/mapper/PayBillMapper.java

@@ -1,7 +1,9 @@
 package com.payment.platform.module.payment.notification.mapper;
 
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillEntity;
+import org.apache.ibatis.annotations.Insert;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -33,5 +35,42 @@ public interface PayBillMapper extends BaseMapper<PayBillEntity> {
     Map<String, BigDecimal> statConsumeAmount(@Param("tenantId") Long tenantId,
                                                @Param("enterpriseId") String enterpriseId,
                                                @Param("payeeType") String payeeType);
+
+    /**
+     * 原子 upsert(通知处理用,跳过租户过滤)
+     * 并发安全:同一 pay_no 的 COLLECT → ASSETS_UPDATE 无论到达顺序,
+     * 数据库层面保证最后一个写入的状态为最终状态。
+     */
+    @InterceptorIgnore(tenantLine = "true")
+    @Insert("""
+            INSERT INTO pay_bill (id, pay_no, account_id, employee_id, consume_type,
+              consume_amount, gmt_biz_create, gmt_recieve_pay, peer_pay_amount,
+              notify_reason, notify_msg, related_pay_no, expense_rule_group_id,
+              expense_scene_code, expense_type, status, ext_infos,
+              enterprise_id, tenant_id, created_time, updated_time)
+            VALUES (#{id}, #{payNo}, #{accountId}, #{employeeId}, #{consumeType},
+              #{consumeAmount}, #{gmtBizCreate}, #{gmtRecievePay}, #{peerPayAmount},
+              #{notifyReason}, #{notifyMsg}, #{relatedPayNo}, #{expenseRuleGroupId},
+              #{expenseSceneCode}, #{expenseType}, #{status}, #{extInfos}::jsonb,
+              #{enterpriseId}, #{tenantId}, #{createdTime}, #{updatedTime})
+            ON CONFLICT (pay_no) DO UPDATE SET
+              account_id = EXCLUDED.account_id,
+              employee_id = EXCLUDED.employee_id,
+              consume_type = EXCLUDED.consume_type,
+              consume_amount = EXCLUDED.consume_amount,
+              gmt_biz_create = EXCLUDED.gmt_biz_create,
+              gmt_recieve_pay = EXCLUDED.gmt_recieve_pay,
+              peer_pay_amount = EXCLUDED.peer_pay_amount,
+              notify_reason = EXCLUDED.notify_reason,
+              notify_msg = EXCLUDED.notify_msg,
+              related_pay_no = EXCLUDED.related_pay_no,
+              expense_rule_group_id = EXCLUDED.expense_rule_group_id,
+              expense_scene_code = EXCLUDED.expense_scene_code,
+              expense_type = EXCLUDED.expense_type,
+              status = EXCLUDED.status,
+              ext_infos = EXCLUDED.ext_infos,
+              updated_time = EXCLUDED.updated_time
+            """)
+    void upsert(PayBillEntity bill);
 }