From d879291f56eb64adffa44f763238ea736b4be99b Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 12 九月 2026 16:16:46 +0800
Subject: [PATCH] feat(payment): 订单固定收款子商户并校验支付回调

---
 talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentCallbackService.java |  164 ++++++++++++++++++++++++++++++------------------------
 1 files changed, 90 insertions(+), 74 deletions(-)

diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentCallbackService.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentCallbackService.java
index 44827ec..7986249 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentCallbackService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentCallbackService.java
@@ -4,9 +4,13 @@
 import java.security.MessageDigest;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.alibaba.fastjson.JSON;
 import com.talentcard.server.business.DatabaseRows;
+import com.talentcard.server.business.payment.HuifuPaymentPayload.TransStatus;
+import com.talentcard.server.business.payment.PaymentSettlementService.ProviderPaymentFact;
+import com.talentcard.server.business.payment.PaymentSettlementService.SettlementOutcome;
 import com.talentcard.server.business.payment.huifu.HuifuSignatureService;
 import com.talentcard.server.common.BusinessException;
 import com.talentcard.server.common.ErrorCode;
@@ -14,17 +18,33 @@
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+/**
+ * Handles Huifu asynchronous notifications.
+ *
+ * <p>The signature is verified first; only then is the payload interpreted. A successful payment
+ * notification is handed to {@link PaymentSettlementService}, which compares the request number,
+ * merchant number and amount with the order's fixed collecting sub-merchant before any state
+ * changes. Duplicated notifications for an already processed request are acknowledged without
+ * touching business state.
+ */
 @Service
 public class HuifuPaymentCallbackService {
 
     private static final String PROVIDER = "HUIFU";
 
+    /** Event states that already describe a final decision for this exact payload. */
+    private static final Set<String> TERMINAL_EVENT_STATUSES = Set.of(
+            "PROCESSED", "PAYMENT_CLOSED", "UNKNOWN_STATUS",
+            "MERCHANT_MISMATCH", "MERCHANT_UNVERIFIED", "AMOUNT_MISMATCH", "COLLECTOR_UNKNOWN");
+
     private final JdbcTemplate jdbc;
     private final HuifuSignatureService signatureService;
+    private final PaymentSettlementService settlementService;
 
-    public HuifuPaymentCallbackService(JdbcTemplate jdbc, HuifuSignatureService signatureService) {
+    public HuifuPaymentCallbackService(JdbcTemplate jdbc, HuifuSignatureService signatureService, PaymentSettlementService settlementService) {
         this.jdbc = jdbc;
         this.signatureService = signatureService;
+        this.settlementService = settlementService;
     }
 
     @Transactional
@@ -32,57 +52,51 @@
         if (!signatureService.verifyCallbackPayload(responseData, signature)) {
             throw new BusinessException(ErrorCode.FORBIDDEN, "汇付回调验签失败");
         }
-        Map<String, Object> data = parse(responseData);
-        String requestNo = text(data.get("req_seq_id"));
+        Map<String, Object> envelope = parse(responseData);
+        Map<String, Object> data = HuifuPaymentPayload.businessData(envelope);
+        String requestNo = HuifuPaymentPayload.firstNonBlank(HuifuPaymentPayload.value(data, "req_seq_id"), HuifuPaymentPayload.value(envelope, "req_seq_id"));
         if (requestNo.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付回调缺少请求流水号");
-        upsertEvent(requestNo, sha256(responseData), "RECEIVED");
+
+        String payloadHash = sha256(responseData);
+        boolean alreadyHandled = alreadyHandled(requestNo, payloadHash);
+        upsertEvent(requestNo, payloadHash, "RECEIVED");
+        if (alreadyHandled) return acknowledgement(requestNo);
 
         if (handleRefund(requestNo, data)) {
             return acknowledgement(requestNo);
         }
 
-        List<Map<String, Object>> transactions = rows("""
-                select id, order_id, status
-                from payment_transaction
-                where provider = ? and payment_no = ?
-                limit 1 for update
-                """, PROVIDER, requestNo);
-        if (transactions.isEmpty()) {
+        String transStatus = HuifuPaymentPayload.value(data, "trans_stat", "transStat");
+        if (HuifuPaymentPayload.status(transStatus) == TransStatus.UNKNOWN) {
+            updateEvent(requestNo, "UNKNOWN_STATUS");
+            return acknowledgement(requestNo);
+        }
+
+        if (!transactionExists(requestNo)) {
             updateEvent(requestNo, "UNMATCHED");
             return acknowledgement(requestNo);
         }
 
-        Map<String, Object> transaction = transactions.get(0);
-        if (paid(data)) {
-            int paymentUpdated = jdbc.update("""
-                    update payment_transaction
-                    set status = 'SUCCEEDED', provider_transaction_no = ?, provider_response_code = ?,
-                        provider_response_message = ?, paid_at = coalesce(paid_at, now())
-                    where id = ? and status in ('CREATED', 'CREATE_FAILED', 'PREPAY_CREATED', 'CREATE_UNKNOWN')
-                    """, providerTransactionNo(data), resultCode(data), resultMessage(data), transaction.get("id"));
-            if (paymentUpdated > 0) {
-                jdbc.update("""
-                        update benefit_order
-                        set order_status = 1,
-                            verify_status = case when order_type = 'STORE_CONSUME' then 1 else verify_status end
-                        where id = ? and order_status = 0
-                        """, transaction.get("orderId"));
-                updateEvent(requestNo, "PROCESSED");
-            } else if ("SUCCEEDED".equals(String.valueOf(transaction.get("status")))) {
-                updateEvent(requestNo, "PROCESSED");
-            } else {
-                // This is a late callback for a payment that the talent user
-                // cancelled before it succeeded.  Keep the order cancelled.
-                updateEvent(requestNo, "PAYMENT_CLOSED");
-            }
-        } else {
+        if (!HuifuPaymentPayload.paid(transStatus, HuifuPaymentPayload.code(data))) {
+            // The provider did not confirm money: keep the payment pending for the query path.
             jdbc.update("""
                     update payment_transaction
                     set provider_response_code = ?, provider_response_message = ?
-                    where id = ? and status in ('CREATED', 'CREATE_FAILED', 'PREPAY_CREATED')
-                    """, resultCode(data), resultMessage(data), transaction.get("id"));
+                    where provider = ? and payment_no = ? and status in ('CREATED', 'CREATE_FAILED', 'PREPAY_CREATED', 'CREATE_UNKNOWN')
+                    """, HuifuPaymentPayload.code(data), HuifuPaymentPayload.message(data), PROVIDER, requestNo);
             updateEvent(requestNo, "PENDING_QUERY");
+            return acknowledgement(requestNo);
         }
+
+        SettlementOutcome outcome = settlementService.settle(new ProviderPaymentFact(
+                requestNo,
+                HuifuPaymentPayload.value(data, "huifu_id", "huifuId"),
+                HuifuPaymentPayload.amountInCent(HuifuPaymentPayload.value(data, "trans_amt", "transAmt")),
+                providerTransactionNo(data),
+                HuifuPaymentPayload.code(data),
+                HuifuPaymentPayload.message(data),
+                "CALLBACK"));
+        updateEvent(requestNo, eventStatus(outcome));
         return acknowledgement(requestNo);
     }
 
@@ -96,13 +110,15 @@
         if (refunds.isEmpty()) return false;
 
         Map<String, Object> refund = refunds.get(0);
-        if (paid(data)) {
+        String transStatus = HuifuPaymentPayload.value(data, "trans_stat", "transStat");
+        String code = HuifuPaymentPayload.code(data);
+        if (HuifuPaymentPayload.paid(transStatus, code)) {
             jdbc.update("""
                     update order_refund
                     set refund_status = 2, provider = 'HUIFU', provider_refund_no = ?,
                         provider_response_code = ?, provider_response_message = ?, refund_time = coalesce(refund_time, now())
                     where id = ? and refund_status = 1
-                    """, providerTransactionNo(data), resultCode(data), resultMessage(data), refund.get("id"));
+                    """, providerTransactionNo(data), code, HuifuPaymentPayload.message(data), refund.get("id"));
             jdbc.update("""
                     update benefit_order o
                     set o.order_status = 2
@@ -110,22 +126,53 @@
                       and o.pay_amount <= (select coalesce(sum(r.refund_amount), 0) from order_refund r where r.order_id = o.id and r.refund_status = 2 and r.deleted = 0)
                     """, refund.get("orderId"));
             updateEvent(refundNo, "PROCESSED");
-        } else if (terminalFailure(data)) {
+        } else if (HuifuPaymentPayload.terminalFailure(transStatus, code)) {
             jdbc.update("""
                     update order_refund
                     set provider_response_code = ?, provider_response_message = ?, refund_status = 3
                     where id = ? and refund_status = 1
-                    """, resultCode(data), resultMessage(data), refund.get("id"));
+                    """, code, HuifuPaymentPayload.message(data), refund.get("id"));
             updateEvent(refundNo, "REFUND_FAILED");
         } else {
             jdbc.update("""
                     update order_refund
                     set provider_response_code = ?, provider_response_message = ?
                     where id = ? and refund_status = 1
-                    """, resultCode(data), resultMessage(data), refund.get("id"));
+                    """, code, HuifuPaymentPayload.message(data), refund.get("id"));
             updateEvent(refundNo, "PENDING_QUERY");
         }
         return true;
+    }
+
+    private String eventStatus(SettlementOutcome outcome) {
+        return switch (outcome) {
+            case SETTLED, ALREADY_SETTLED -> "PROCESSED";
+            case CLOSED_LATE_SUCCESS -> "PAYMENT_CLOSED";
+            case MERCHANT_MISMATCH -> "MERCHANT_MISMATCH";
+            case MERCHANT_UNVERIFIED -> "MERCHANT_UNVERIFIED";
+            case AMOUNT_MISMATCH -> "AMOUNT_MISMATCH";
+            case COLLECTOR_UNKNOWN -> "COLLECTOR_UNKNOWN";
+            case UNMATCHED -> "UNMATCHED";
+            case NOT_SETTLEABLE -> "PENDING_QUERY";
+        };
+    }
+
+    private boolean transactionExists(String paymentNo) {
+        Long count = jdbc.queryForObject("select count(*) from payment_transaction where provider = ? and payment_no = ?", Long.class, PROVIDER, paymentNo);
+        return count != null && count > 0;
+    }
+
+    private boolean alreadyHandled(String requestNo, String payloadHash) {
+        List<Map<String, Object>> rows = rows("""
+                select process_status, payload_sha256
+                from payment_callback_event
+                where provider = ? and provider_request_no = ?
+                limit 1
+                """, PROVIDER, requestNo);
+        if (rows.isEmpty()) return false;
+        Map<String, Object> event = rows.get(0);
+        return payloadHash.equals(HuifuPaymentPayload.text(event.get("payloadSha256")))
+                && TERMINAL_EVENT_STATUSES.contains(HuifuPaymentPayload.text(event.get("processStatus")));
     }
 
     private void upsertEvent(String requestNo, String payloadHash, String status) {
@@ -143,35 +190,8 @@
                 """, status, PROVIDER, requestNo);
     }
 
-    private boolean paid(Map<String, Object> data) {
-        String status = text(data.get("trans_stat"));
-        String code = resultCode(data);
-        return ("S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status) || "PAID".equalsIgnoreCase(status))
-                && ("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code));
-    }
-
-    private boolean terminalFailure(Map<String, Object> data) {
-        String status = text(data.get("trans_stat"));
-        String code = resultCode(data);
-        boolean processing = "P".equalsIgnoreCase(status) || "PROCESSING".equalsIgnoreCase(status) || "PENDING".equalsIgnoreCase(status);
-        boolean successCode = "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code);
-        return !processing && (!successCode || "F".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status));
-    }
-
     private String providerTransactionNo(Map<String, Object> data) {
-        String value = text(data.get("hf_seq_id"));
-        if (value.isBlank()) value = text(data.get("bank_order_no"));
-        return value;
-    }
-
-    private String resultCode(Map<String, Object> data) {
-        String code = text(data.get("sub_resp_code"));
-        return code.isBlank() ? text(data.get("resp_code")) : code;
-    }
-
-    private String resultMessage(Map<String, Object> data) {
-        String message = text(data.get("sub_resp_desc"));
-        return message.isBlank() ? text(data.get("resp_desc")) : message;
+        return HuifuPaymentPayload.value(data, "hf_seq_id", "bank_order_no", "hfSeqId");
     }
 
     private String acknowledgement(String requestNo) {
@@ -196,10 +216,6 @@
         } catch (Exception exception) {
             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付回调摘要失败");
         }
-    }
-
-    private String text(Object value) {
-        return value == null ? "" : String.valueOf(value).trim();
     }
 
     private List<Map<String, Object>> rows(String sql, Object... args) {

--
Gitblit v1.7.1