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.
|
*
|
* <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;
|
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<String, Object> envelope = parse(responseData);
|
Map<String, Object> 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<String, Object> data) {
|
List<Map<String, Object>> 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<String, Object> 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<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(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<String, Object> 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<String, Object> 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<Map<String, Object>> rows(String sql, Object... args) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, args));
|
}
|
}
|