Selaa lähdekoodia

feat: precreateTrade 落库 + queryTrade 更新状态 + 定时轮询兜底

alphaH 2 viikkoa sitten
vanhempi
sitoutus
a84c0cbfa6

+ 44 - 0
java/src/main/java/com/payment/platform/module/payment/facetoface/scheduler/F2fTradePollScheduler.java

@@ -0,0 +1,44 @@
+package com.payment.platform.module.payment.facetoface.scheduler;
+
+import com.payment.platform.module.payment.facetoface.entity.F2fTradeRecordEntity;
+import com.payment.platform.module.payment.facetoface.service.FacetofaceService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 当面付收款轮询定时任务 — 兜底查询 WAIT_BUYER_PAY 状态的交易
+ * 仿照 queryTrade,每 1 分钟执行,调用 alipay.trade.query 更新状态
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class F2fTradePollScheduler {
+
+    private final FacetofaceService service;
+
+    @Scheduled(fixedDelay = 60 * 1000) // 每 1 分钟
+    public void pollPendingTrades() {
+        log.info("[收款轮询] 定时任务开始执行");
+        List<F2fTradeRecordEntity> records = service.getPendingTrades();
+        if (records.isEmpty()) {
+            log.info("[收款轮询] 无待轮询交易");
+            return;
+        }
+
+        int count = 0;
+        for (F2fTradeRecordEntity record : records) {
+            try {
+                service.doQueryTrade(record);
+                count++;
+            } catch (Exception e) {
+                log.error("[收款轮询] 轮询交易异常: out_trade_no={}, error={}",
+                        record.getOutTradeNo(), e.getMessage());
+            }
+        }
+        log.info("[收款轮询] 定时任务完成: 处理 {}/{} 条", count, records.size());
+    }
+}

+ 138 - 1
java/src/main/java/com/payment/platform/module/payment/facetoface/service/FacetofaceService.java

@@ -16,8 +16,11 @@ import com.payment.platform.common.response.PageResult;
 import com.payment.platform.common.utils.RedisLockUtil;
 import com.payment.platform.core.alipay.AlipayClientFactory;
 import com.payment.platform.module.payment.facetoface.dto.*;
+import com.payment.platform.module.payment.facetoface.entity.F2fTradeRecordEntity;
 import com.payment.platform.module.payment.facetoface.entity.FacetofaceOrderEntity;
+import com.payment.platform.module.payment.facetoface.enums.F2fTradeStatus;
 import com.payment.platform.module.payment.facetoface.enums.FacetofaceOrderStatus;
+import com.payment.platform.module.payment.facetoface.mapper.F2fTradeRecordMapper;
 import com.payment.platform.module.payment.facetoface.mapper.FacetofaceOrderMapper;
 import com.payment.platform.module.payment.serviceprovider.enums.ServiceProviderBizType;
 import lombok.RequiredArgsConstructor;
@@ -39,6 +42,7 @@ import java.util.stream.Collectors;
 public class FacetofaceService {
 
     private final FacetofaceOrderMapper mapper;
+    private final F2fTradeRecordMapper tradeRecordMapper;
     private final AlipayClientFactory alipayClientFactory;
     private final RedisLockUtil redisLockUtil;
 
@@ -403,6 +407,21 @@ public class FacetofaceService {
                 order.getAppAuthToken() != null ? order.getAppAuthToken().length() : 0);
 
         String outTradeNo = "F2F" + System.currentTimeMillis() + String.format("%04d", (int) (Math.random() * 10000));
+
+        // 先保存交易记录(状态 WAIT_BUYER_PAY)
+        F2fTradeRecordEntity tradeRecord = new F2fTradeRecordEntity();
+        tradeRecord.setOutTradeNo(outTradeNo);
+        tradeRecord.setTotalAmount(dto.getTotalAmount());
+        tradeRecord.setSubject(StrUtil.isNotBlank(dto.getSubject()) ? dto.getSubject() : "当面付收款");
+        tradeRecord.setTradeStatus(F2fTradeStatus.WAIT_BUYER_PAY.getValue());
+        tradeRecord.setEnterpriseId(dto.getEnterpriseId());
+        tradeRecord.setGmtCreate(OffsetDateTime.now());
+        tradeRecord.setNextQueryTime(OffsetDateTime.now().plusMinutes(1));
+        tradeRecord.setQueryCount(0);
+        tradeRecord.setStatus("0");
+        tradeRecordMapper.insert(tradeRecord);
+        log.info("[收款] 交易记录已保存: out_trade_no={}, status=WAIT_BUYER_PAY", outTradeNo);
+
         try {
             AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
             model.setOutTradeNo(outTradeNo);
@@ -428,27 +447,51 @@ public class FacetofaceService {
                     response.getCode(), response.getMsg(), response.getSubCode(), response.getSubMsg());
 
             if (!response.isSuccess()) {
+                // 预下单失败 — 标记交易为 CLOSED
+                tradeRecord.setTradeStatus(F2fTradeStatus.TRADE_CLOSED.getValue());
+                tradeRecord.setNextQueryTime(null);
+                tradeRecord.setDescription("precreate失败: " +
+                        (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
+                tradeRecordMapper.updateById(tradeRecord);
                 throw new BusinessException(400, "生成收款码失败: " +
                         (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
             }
 
             log.info("[收款] 收款码生成成功: out_trade_no={}, qr_code={}", outTradeNo, response.getQrCode());
 
+            // 更新收款码URL
+            tradeRecord.setQrCode(response.getQrCode());
+            tradeRecordMapper.updateById(tradeRecord);
+
             F2fTradeVO vo = new F2fTradeVO();
             vo.setOutTradeNo(outTradeNo);
             vo.setQrCode(response.getQrCode());
+            vo.setTradeStatus(F2fTradeStatus.WAIT_BUYER_PAY.getValue());
             return vo;
 
         } catch (AlipayApiException ex) {
+            // 网络异常 — 标记交易为 CLOSED
+            tradeRecord.setTradeStatus(F2fTradeStatus.TRADE_CLOSED.getValue());
+            tradeRecord.setNextQueryTime(null);
+            tradeRecord.setDescription("precreate异常: " + ex.getMessage());
+            tradeRecordMapper.updateById(tradeRecord);
             log.error("[收款] AlipayApiException: {}", ex.getMessage(), ex);
             throw new BusinessException(400, "生成收款码失败: " + ex.getMessage());
         }
     }
 
-    /** 查询交易状态 — 调用 alipay.trade.query */
+    /** 查询交易状态 — 调用 alipay.trade.query,更新本地记录 */
     public F2fTradeVO queryTrade(F2fTradeQueryDTO dto) {
         FacetofaceOrderEntity order = getSuccessOrder(dto.getEnterprise_id());
 
+        // 先查本地记录
+        F2fTradeRecordEntity record = tradeRecordMapper.selectOne(
+                new LambdaQueryWrapper<F2fTradeRecordEntity>()
+                        .eq(F2fTradeRecordEntity::getOutTradeNo, dto.getOut_trade_no()));
+        if (record == null) {
+            throw new BusinessException(404, "未找到该交易记录");
+        }
+
         try {
             AlipayTradeQueryModel model = new AlipayTradeQueryModel();
             model.setOutTradeNo(dto.getOut_trade_no());
@@ -464,6 +507,28 @@ public class FacetofaceService {
                         (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
             }
 
+            // 更新本地记录
+            String alipayTradeStatus = response.getTradeStatus();
+            record.setTradeStatus(alipayTradeStatus != null ? alipayTradeStatus : record.getTradeStatus());
+            record.setTradeNo(response.getTradeNo());
+            record.setBuyerId(response.getBuyerUserId());
+            record.setBuyerLogonId(response.getBuyerLogonId());
+            if (response.getSendPayDate() != null) {
+                record.setGmtPayment(response.getSendPayDate().toInstant().atOffset(java.time.ZoneOffset.ofHours(8)));
+            }
+            // 终态不继续轮询
+            if (alipayTradeStatus != null &&
+                    (alipayTradeStatus.equals(F2fTradeStatus.TRADE_SUCCESS.getValue())
+                     || alipayTradeStatus.equals(F2fTradeStatus.TRADE_FINISHED.getValue())
+                     || alipayTradeStatus.equals(F2fTradeStatus.TRADE_CLOSED.getValue()))) {
+                record.setNextQueryTime(null);
+            } else {
+                record.setNextQueryTime(OffsetDateTime.now().plusMinutes(1));
+            }
+            record.setLastQueryTime(OffsetDateTime.now());
+            record.setQueryCount(record.getQueryCount() != null ? record.getQueryCount() + 1 : 1);
+            tradeRecordMapper.updateById(record);
+
             F2fTradeVO vo = new F2fTradeVO();
             vo.setOutTradeNo(dto.getOut_trade_no());
             vo.setTradeNo(response.getTradeNo());
@@ -498,6 +563,78 @@ public class FacetofaceService {
         return order;
     }
 
+    // ========== 定时轮询 ==========
+
+    /** 获取待轮询的交易记录 */
+    public List<F2fTradeRecordEntity> getPendingTrades() {
+        return tradeRecordMapper.selectList(
+                new LambdaQueryWrapper<F2fTradeRecordEntity>()
+                        .le(F2fTradeRecordEntity::getNextQueryTime, OffsetDateTime.now())
+                        .eq(F2fTradeRecordEntity::getTradeStatus, F2fTradeStatus.WAIT_BUYER_PAY.getValue())
+                        .last("LIMIT 100"));
+    }
+
+    /** 内部轮询方法 — 仿照 queryTrade 逻辑,不抛异常 */
+    public void doQueryTrade(F2fTradeRecordEntity record) {
+        FacetofaceOrderEntity order;
+        try {
+            order = getSuccessOrder(record.getEnterpriseId());
+        } catch (Exception e) {
+            log.warn("[收款轮询] 获取商户授权失败: out_trade_no={}, error={}", record.getOutTradeNo(), e.getMessage());
+            return;
+        }
+
+        try {
+            AlipayTradeQueryModel model = new AlipayTradeQueryModel();
+            model.setOutTradeNo(record.getOutTradeNo());
+
+            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+            request.setBizModel(model);
+            request.putOtherTextParam("app_auth_token", order.getAppAuthToken());
+
+            AlipayTradeQueryResponse response = alipayClientFactory.getClient(
+                    record.getEnterpriseId(), ServiceProviderBizType.FACETOFACE_NOTIFY.getValue()).execute(request);
+
+            if (!response.isSuccess()) {
+                log.warn("[收款轮询] 查询失败: out_trade_no={}, code={}, msg={}",
+                        record.getOutTradeNo(), response.getCode(), response.getSubMsg());
+                record.setNextQueryTime(OffsetDateTime.now().plusMinutes(5));
+                record.setLastQueryTime(OffsetDateTime.now());
+                record.setQueryCount(record.getQueryCount() != null ? record.getQueryCount() + 1 : 1);
+                tradeRecordMapper.updateById(record);
+                return;
+            }
+
+            String alipayStatus = response.getTradeStatus();
+            log.info("[收款轮询] 查询成功: out_trade_no={}, trade_status={}, trade_no={}",
+                    record.getOutTradeNo(), alipayStatus, response.getTradeNo());
+
+            record.setTradeStatus(alipayStatus != null ? alipayStatus : record.getTradeStatus());
+            record.setTradeNo(response.getTradeNo());
+            record.setBuyerId(response.getBuyerUserId());
+            record.setBuyerLogonId(response.getBuyerLogonId());
+            if (response.getSendPayDate() != null) {
+                record.setGmtPayment(response.getSendPayDate().toInstant().atOffset(java.time.ZoneOffset.ofHours(8)));
+            }
+
+            // 终态停止轮询
+            if (alipayStatus != null &&
+                    (alipayStatus.equals(F2fTradeStatus.TRADE_SUCCESS.getValue())
+                     || alipayStatus.equals(F2fTradeStatus.TRADE_FINISHED.getValue())
+                     || alipayStatus.equals(F2fTradeStatus.TRADE_CLOSED.getValue()))) {
+                record.setNextQueryTime(null);
+            } else {
+                record.setNextQueryTime(OffsetDateTime.now().plusMinutes(1));
+            }
+            record.setLastQueryTime(OffsetDateTime.now());
+            record.setQueryCount(record.getQueryCount() != null ? record.getQueryCount() + 1 : 1);
+            tradeRecordMapper.updateById(record);
+
+        } catch (AlipayApiException ex) {
+            log.error("[收款轮询] 异常: out_trade_no={}, error={}", record.getOutTradeNo(), ex.getMessage());
+        }
+    }
+
     // ========== private ==========
 
     private FacetofaceOrderEntity requireOrder(Long id) {