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

fix: 支付分退款改用通用退款接口/v3/refund/domestic/refunds

之前错误使用 /v3/payscore/serviceorder/.../pay/refund(404),
微信支付分退款实际应走通用境内退款接口,用transaction_id关联。

改动:
- PayScoreCallbackResult: 新增transactionId字段
- parseCallback: 从collection.details提取transaction_id
- handlePayScoreCallback: 存储transactionId到订单
- PayScoreRefundRequest: 新增transactionId/totalAmount字段
- refundServiceOrder: 改用通用退款API + transaction_id

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 недель назад
Родитель
Сommit
ef18b2b8fc

+ 5 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCallbackResult.java

@@ -54,6 +54,11 @@ public class PayScoreCallbackResult {
      * 取消原因(PAYSCORE.USER_CANCEL 回调时填充)
      */
     private String cancelReason;
+
+    /**
+     * 微信支付订单号(PAYSCORE.USER_PAID 回调时从 collection.details 提取,用于退款)
+     */
+    private String transactionId;
     
     public static PayScoreCallbackResult success(String eventType, String outOrderNo, String state) {
         PayScoreCallbackResult result = new PayScoreCallbackResult();

+ 7 - 1
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreRefundRequest.java

@@ -6,7 +6,7 @@ import java.math.BigDecimal;
 
 /**
  * 支付分退款请求
- * 使用微信支付分独立退款接口
+ * 使用微信支付通用退款接口 /v3/refund/domestic/refunds
  */
 @Data
 public class PayScoreRefundRequest {
@@ -14,9 +14,15 @@ public class PayScoreRefundRequest {
     /** 商户服务订单号 */
     private String outOrderNo;
 
+    /** 微信支付订单号(支付分扣款成功后回调返回的 transaction_id) */
+    private String transactionId;
+
     /** 退款金额(元) */
     private BigDecimal refundAmount;
 
+    /** 原订单总金额(元) */
+    private BigDecimal totalAmount;
+
     /** 退款原因 */
     private String reason;
 

+ 12 - 29
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -303,8 +303,11 @@ public class PayScoreServiceImpl implements PayScoreService {
             if (result.getTotalAmount() != null) {
                 order.setTotalAmount(result.getTotalAmount());
             }
-            log.info("[支付分服务] 用户完成支付 - orderId: {}, amount: {}元",
-                    order.getId(), result.getTotalAmount());
+            if (result.getTransactionId() != null) {
+                order.setTransactionId(result.getTransactionId());
+            }
+            log.info("[支付分服务] 用户完成支付 - orderId: {}, amount: {}元, transactionId: {}",
+                    order.getId(), result.getTotalAmount(), result.getTransactionId());
         } else if ("PAYSCORE.REFUND_PAID".equals(eventType)) {
             order.setRefundStatus("REFUNDED");
             order.setPayStatus(OrderConstants.PAY_STATUS_REFUND);
@@ -536,40 +539,20 @@ public class PayScoreServiceImpl implements PayScoreService {
         BigDecimal actualRefundAmount = refundAmount != null ? refundAmount
                 : (order.getPaidAmount() != null ? order.getPaidAmount() : order.getTotalAmount());
 
-        // 先调用 /pay 接口确保支付资源存在,否则 /pay/refund 会返回 404
-        log.info("[支付分服务] 退款前先确保支付已发起 - orderId: {}", orderId);
-        PayScoreResult payResult = payScoreStrategy.payServiceOrder(order.getPayScoreOrderId());
-        if (!payResult.isSuccess()) {
-            log.warn("[支付分服务] /pay 调用未成功(可能已自动扣款),继续尝试退款: code={}, msg={}",
-                    payResult.getErrorCode(), payResult.getErrorMsg());
+        // 校验 transaction_id(退款必须,来自支付回调 PAYSCORE.USER_PAID)
+        if (order.getTransactionId() == null || order.getTransactionId().isEmpty()) {
+            return PayScoreResult.fail("MISSING_TRANSACTION_ID",
+                    "未收到微信支付回调,缺少 transaction_id,请稍后重试");
         }
 
         PayScoreRefundRequest request = new PayScoreRefundRequest();
         request.setOutOrderNo(order.getPayScoreOrderId());
+        request.setTransactionId(order.getTransactionId());
         request.setRefundAmount(actualRefundAmount);
+        request.setTotalAmount(order.getTotalAmount());
         request.setReason(reason != null ? reason : "用户申请退款");
 
-        // 重试退款:/pay 后支付资源创建可能有延迟,重试最多3次
-        PayScoreResult result = null;
-        for (int i = 0; i < 3; i++) {
-            if (i > 0) {
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException ignored) {
-                }
-                log.info("[支付分服务] 退款重试 {}/3 - orderId: {}", i + 1, orderId);
-            }
-            result = payScoreStrategy.refundServiceOrder(request);
-            if (result.isSuccess()) {
-                break;
-            }
-            // 404 空body说明支付资源尚未就绪,继续重试
-            if (result.getErrorCode() == null && result.getErrorMsg() == null) {
-                log.warn("[支付分服务] 退款返回空错误(可能支付资源未就绪),准备重试");
-                continue;
-            }
-            break;
-        }
+        PayScoreResult result = payScoreStrategy.refundServiceOrder(request);
 
         if (result.isSuccess()) {
             order.setRefundStatus("REFUNDED");

+ 34 - 7
haha-service/src/main/java/com/haha/service/payment/payscore/impl/WxPayScoreStrategy.java

@@ -596,22 +596,36 @@ public class WxPayScoreStrategy implements PayScoreStrategy {
 
     @Override
     public PayScoreResult refundServiceOrder(PayScoreRefundRequest request) {
-        log.info("[微信支付分] 申请退款 - 订单号: {}, 金额: {}元", request.getOutOrderNo(), request.getRefundAmount());
+        log.info("[微信支付分] 申请退款 - 订单号: {}, 金额: {}元, transactionId: {}",
+                request.getOutOrderNo(), request.getRefundAmount(), request.getTransactionId());
 
         PayScoreResult checkResult = checkServiceAvailable();
         if (checkResult != null) {
             return checkResult;
         }
 
+        if (request.getTransactionId() == null || request.getTransactionId().isEmpty()) {
+            log.error("[微信支付分] 退款失败: transaction_id 为空,可能回调尚未处理完成");
+            return PayScoreResult.fail("MISSING_TRANSACTION_ID", "缺少微信支付订单号,请等待支付回调完成后重试");
+        }
+
         try {
+            // 支付分退款使用通用退款接口 /v3/refund/domestic/refunds
             ObjectNode body = objectMapper.createObjectNode();
-            body.put("appid", wxPayConfig.getAppId());
-            body.put("service_id", wxPayConfig.getServiceId());
+            body.put("transaction_id", request.getTransactionId());
+            body.put("out_refund_no", request.getOutRefundNo() != null
+                    ? request.getOutRefundNo() : "RF" + request.getOutOrderNo());
             body.put("reason", request.getReason() != null ? request.getReason() : "用户申请退款");
-            body.put("amount", yuanToFen(request.getRefundAmount()));
+
+            ObjectNode amount = objectMapper.createObjectNode();
+            amount.put("refund", yuanToFen(request.getRefundAmount()));
+            amount.put("total", request.getTotalAmount() != null
+                    ? yuanToFen(request.getTotalAmount()) : yuanToFen(request.getRefundAmount()));
+            amount.put("currency", "CNY");
+            body.set("amount", amount);
 
             String requestBody = objectMapper.writeValueAsString(body);
-            String url = PAY_SCORE_BASE_URL + "/" + request.getOutOrderNo() + "/pay/refund";
+            String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
             log.info("[微信支付分] 退款请求: url={}, body={}", url, requestBody);
 
             HttpRequest httpRequest = new HttpRequest.Builder()
@@ -626,11 +640,12 @@ public class WxPayScoreStrategy implements PayScoreStrategy {
             Map<String, Object> responseMap = response.getServiceResponse();
             JsonNode respJson = objectMapper.valueToTree(responseMap);
 
-            String outOrderNo = getJsonString(respJson, "out_order_no");
             String refundId = getJsonString(respJson, "refund_id");
+            String status = getJsonString(respJson, "status");
 
-            PayScoreResult result = PayScoreResult.success(outOrderNo, null);
+            PayScoreResult result = PayScoreResult.success(request.getOutOrderNo(), status);
             result.setPackageStr(refundId);
+            log.info("[微信支付分] 退款申请成功 - refundId: {}, status: {}", refundId, status);
             return result;
 
         } catch (HttpException e) {
@@ -766,6 +781,18 @@ public class WxPayScoreStrategy implements PayScoreStrategy {
                 result.setTotalAmount(fenToYuan(totalAmountFen));
             }
 
+            // 提取微信支付订单号(用于退款)
+            if (dataJson.has("collection")) {
+                JsonNode collection = dataJson.get("collection");
+                if (collection.has("details") && collection.get("details").isArray()) {
+                    JsonNode details = collection.get("details");
+                    if (details.size() > 0) {
+                        String transactionId = getJsonString(details.get(0), "transaction_id");
+                        result.setTransactionId(transactionId);
+                    }
+                }
+            }
+
             // 解析支付时间
             if (dataJson.has("pay_succ_time") && !dataJson.get("pay_succ_time").isNull()) {
                 String paySuccTime = dataJson.get("pay_succ_time").asText();