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