From 5c33cf1f2622d4470b861cb041024ead1775c828 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 16 九月 2026 18:05:27 +0800
Subject: [PATCH] feat(payment): 撤销核销把已确认的资金退回延迟户

---
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuDelayAccountGateway.java |   82 ++++++++++++++++++++++++++++++++++++++--
 1 files changed, 77 insertions(+), 5 deletions(-)

diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuDelayAccountGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuDelayAccountGateway.java
index 83730d6..496ec68 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuDelayAccountGateway.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuDelayAccountGateway.java
@@ -24,6 +24,9 @@
  *
  * <p>业务上是否延迟由订单类型决定:先付钱后核销的订单延迟,核销成功才确认;支付即核销的订单不延迟,
  * 因此不需要确认。
+ *
+ * <p>核销被撤销时走反方向:交易确认退款把已确认给商户的钱退回延迟户,重新变成不可结算、不可取现,
+ * 等商户重新核销时再确认一次。这条路径的结果分类与确认分开,见 {@link HuifuDelayRevert}。
  */
 @Service
 public class HuifuDelayAccountGateway {
@@ -81,8 +84,8 @@
         return execute(request, CONFIRM_QUERY_FUNCTION, "汇付交易确认查询失败");
     }
 
-    /** 把已确认的金额退回原商户账户。 */
-    public HuifuDelayConfirmation refund(HuifuDelayConfirmRefundCommand command) {
+    /** 把已确认的金额退回原商户账户(延迟户),撤销核销用它把钱要回来。 */
+    public HuifuDelayRevert refund(HuifuDelayConfirmRefundCommand command) {
         ensureConfigured();
         initializeSdk();
         Map<String, Object> request = new LinkedHashMap<>();
@@ -96,10 +99,10 @@
                 "req_seq_id", command.refundNo(),
                 "huifu_id", command.huifuId().trim(),
                 "org_req_seq_id", command.confirmNo())));
-        return execute(request, CONFIRM_REFUND_FUNCTION, "汇付交易确认退款失败");
+        return executeRevert(request, CONFIRM_REFUND_FUNCTION, "汇付交易确认退款失败");
     }
 
-    private HuifuDelayConfirmation execute(Map<String, Object> request, String function, String failureMessage) {
+    private Map<String, Object> call(Map<String, Object> request, String function, String failureMessage) {
         Map<String, Object> response;
         try {
             response = BasePayClient.request(request, function, false);
@@ -109,7 +112,15 @@
             throw new HuifuPaymentGatewayException(failureMessage, isOutcomeUnknown(exception), exception);
         }
         if (response == null) throw new HuifuPaymentGatewayException(failureMessage, true, null);
-        return HuifuDelayConfirmation.from(response);
+        return response;
+    }
+
+    private HuifuDelayConfirmation execute(Map<String, Object> request, String function, String failureMessage) {
+        return HuifuDelayConfirmation.from(call(request, function, failureMessage));
+    }
+
+    private HuifuDelayRevert executeRevert(Map<String, Object> request, String function, String failureMessage) {
+        return HuifuDelayRevert.from(call(request, function, failureMessage));
     }
 
     private void ensureConfigured() {
@@ -239,4 +250,65 @@
             return HuifuPayload.amountInCent(confirmedAmount);
         }
     }
+
+    /**
+     * 交易确认退款(撤销核销回退资金)的结果。
+     *
+     * <p>刻意不复用 {@link HuifuDelayConfirmation} 的分类:同一个业务码在确认和回退两条路径上语义
+     * 不同。{@code 23000003} 在确认路径上是「已无未确认金额」——那是既有真实回执复验过的幂等成功,
+     * 而在回退路径上它既可能是「已无已确认金额」(同样是幂等),也可能是被拒的原因(例如基本户
+     * 余额不足)。这里一律不猜:判成成功会让订单翻回「待核销」而钱还在商户基本户,是资金事故;
+     * 判成失败又没有回执依据。所以语义未定的码单独归到 {@link Outcome#NEEDS_REVIEW},停在
+     * 「已核销」等人工核对。
+     */
+    public record HuifuDelayRevert(
+            Outcome outcome,
+            String code,
+            String message,
+            String providerTransactionNo
+    ) {
+
+        public enum Outcome {
+            /** 钱已退回商户延迟户。 */
+            SUCCEEDED,
+            /** 通道还在处理,结果未定。 */
+            PROCESSING,
+            /** 通道明确拒绝(例如资金已结算到银行卡、余额不足),钱没有动。 */
+            FAILED,
+            /** 返回码在这条路径上语义未定,不能当作成功或失败,需要人工核对。 */
+            NEEDS_REVIEW
+        }
+
+        static HuifuDelayRevert from(Map<String, Object> response) {
+            Map<String, Object> data = HuifuPayload.businessData(response);
+            String code = HuifuPayload.code(data);
+            String message = HuifuPayload.firstNonBlank(
+                    HuifuPayload.value(data, "sub_resp_desc", "trans_desc"), HuifuPayload.message(data));
+            String status = HuifuPayload.value(data, "trans_stat", "trans_status", "transStat", "transStatus");
+            String providerNo = HuifuPayload.value(data, "hf_seq_id", "org_hf_seq_id", "hfSeqId", "orgHfSeqId");
+            return new HuifuDelayRevert(classify(status, code), code, message, providerNo);
+        }
+
+        /**
+         * 在回退路径上语义未定的业务码见 {@link HuifuPayload#undeterminedCode(String)}。
+         *
+         * <p>它们必须排在交易状态之前判断:通道给这些码时会同时给 {@code trans_stat=F},
+         * 先看状态就会把它们当成明确失败(交易确认那条路上正是这样踩过一次坑)。
+         */
+        private static Outcome classify(String status, String code) {
+            String normalizedCode = code == null ? "" : code.trim();
+            if (HuifuPayload.undeterminedCode(normalizedCode)) return Outcome.NEEDS_REVIEW;
+            if (HuifuPayload.successCode(normalizedCode)) return Outcome.SUCCEEDED;
+            if (HuifuPayload.status(status) == HuifuPayload.TransStatus.FAILED) return Outcome.FAILED;
+            if ("00000100".equals(normalizedCode)) return Outcome.PROCESSING;
+            return normalizedCode.isBlank() ? Outcome.PROCESSING : Outcome.FAILED;
+        }
+
+        /** 供日志与故障定位使用的一句话结论。 */
+        public String describe() {
+            if (!message.isBlank()) return message;
+            if (!code.isBlank()) return "Huifu delay confirmation refund returned code " + code;
+            return "The provider returned no description";
+        }
+    }
 }

--
Gitblit v1.7.1