package com.talentcard.server.business.payment; import java.nio.charset.StandardCharsets; 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.order.OrderFundBoundaryService; import com.talentcard.server.business.payment.PaymentSettlementService.ProviderPaymentFact; import com.talentcard.server.business.payment.PaymentSettlementService.SettlementOutcome; import com.talentcard.server.business.payment.huifu.HuifuPayload; import com.talentcard.server.business.payment.huifu.HuifuPayload.TransStatus; import com.talentcard.server.business.payment.huifu.HuifuSignatureService; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * Handles Huifu asynchronous notifications. * *

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 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; private final OrderFundBoundaryService orderFundBoundary; public HuifuPaymentCallbackService(JdbcTemplate jdbc, HuifuSignatureService signatureService, PaymentSettlementService settlementService, OrderFundBoundaryService orderFundBoundary) { this.jdbc = jdbc; this.signatureService = signatureService; this.settlementService = settlementService; this.orderFundBoundary = orderFundBoundary; } @Transactional public String handle(String responseData, String signature) { if (!signatureService.verifyCallbackPayload(responseData, signature)) { throw new BusinessException(ErrorCode.FORBIDDEN, "汇付回调验签失败"); } Map envelope = parse(responseData); Map data = HuifuPayload.businessData(envelope); String requestNo = HuifuPayload.firstNonBlank(HuifuPayload.value(data, "req_seq_id"), HuifuPayload.value(envelope, "req_seq_id")); if (requestNo.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付回调缺少请求流水号"); 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); } String transStatus = HuifuPayload.value(data, "trans_stat", "transStat"); if (HuifuPayload.status(transStatus) == TransStatus.UNKNOWN) { updateEvent(requestNo, "UNKNOWN_STATUS"); return acknowledgement(requestNo); } if (!transactionExists(requestNo)) { updateEvent(requestNo, "UNMATCHED"); return acknowledgement(requestNo); } if (!HuifuPayload.paid(transStatus, HuifuPayload.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 provider = ? and payment_no = ? and status in ('CREATED', 'CREATE_FAILED', 'PREPAY_CREATED', 'CREATE_UNKNOWN') """, HuifuPayload.code(data), HuifuPayload.message(data), PROVIDER, requestNo); updateEvent(requestNo, "PENDING_QUERY"); return acknowledgement(requestNo); } SettlementOutcome outcome = settlementService.settle(new ProviderPaymentFact( requestNo, HuifuPayload.value(data, "huifu_id", "huifuId"), HuifuPayload.amountInCent(HuifuPayload.value(data, "trans_amt", "transAmt")), providerTransactionNo(data), HuifuPayload.code(data), HuifuPayload.message(data), "CALLBACK")); updateEvent(requestNo, eventStatus(outcome)); return acknowledgement(requestNo); } private boolean handleRefund(String refundNo, Map data) { List> refunds = rows(""" select id, order_id, refund_status from order_refund where deleted = 0 and refund_no = ? limit 1 for update """, refundNo); if (refunds.isEmpty()) return false; Map refund = refunds.get(0); long orderId = ((Number) refund.get("orderId")).longValue(); String transStatus = HuifuPayload.value(data, "trans_stat", "transStat"); String code = HuifuPayload.code(data); if (HuifuPayload.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), code, HuifuPayload.message(data), refund.get("id")); // 通道确认退款成功是最终事实:订单级退款状态无条件收敛为已退款。 orderFundBoundary.markRefunded(orderId); jdbc.update(""" update benefit_order o set o.order_status = 2 where o.id = ? and o.order_status = 1 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 (HuifuPayload.terminalFailure(transStatus, code)) { int failed = jdbc.update(""" update order_refund set provider_response_code = ?, provider_response_message = ?, refund_status = 3 where id = ? and refund_status = 1 """, code, HuifuPayload.message(data), refund.get("id")); // 只有本事务真的把退款置为失败,才把订单退回未退款状态;否则会覆盖并发的退款成功。 if (failed == 1) orderFundBoundary.markRefundFailed(orderId); updateEvent(refundNo, "REFUND_FAILED"); } else { jdbc.update(""" update order_refund set provider_response_code = ?, provider_response_message = ? where id = ? and refund_status = 1 """, code, HuifuPayload.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> 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 event = rows.get(0); return payloadHash.equals(HuifuPayload.text(event.get("payloadSha256"))) && TERMINAL_EVENT_STATUSES.contains(HuifuPayload.text(event.get("processStatus"))); } private void upsertEvent(String requestNo, String payloadHash, String status) { jdbc.update(""" insert into payment_callback_event(provider, provider_request_no, payload_sha256, signature_valid, process_status) values (?, ?, ?, 1, ?) on duplicate key update payload_sha256 = values(payload_sha256), signature_valid = 1, received_at = now() """, PROVIDER, requestNo, payloadHash, status); } private void updateEvent(String requestNo, String status) { jdbc.update(""" update payment_callback_event set process_status = ?, processed_at = now() where provider = ? and provider_request_no = ? """, status, PROVIDER, requestNo); } private String providerTransactionNo(Map data) { return HuifuPayload.value(data, "hf_seq_id", "bank_order_no", "hfSeqId"); } private String acknowledgement(String requestNo) { return "RECV_ORD_ID_" + requestNo; } @SuppressWarnings("unchecked") private Map parse(String payload) { try { return JSON.parseObject(payload, Map.class); } catch (Exception exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付回调数据格式错误"); } } private String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder builder = new StringBuilder(); for (byte current : digest) builder.append(String.format("%02x", current)); return builder.toString(); } catch (Exception exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付回调摘要失败"); } } private List> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); } }