| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | """, 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) { |
| | |
| | | } 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) { |