package com.talentcard.server.business.payment; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway; import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmCommand; import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmation; import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; /** * 延迟交易的资金确认。 * *

商户开通延迟入账后,用 {@code delay_acct_flag=Y} 下的支付会把钱留在通道的「待确认」状态。业务规则是 * 「核销之后商户才能拿到钱」,所以核销成功时才调用交易确认接口,把这笔钱分配给收款子商户。 * *

状态记在 {@code payment_transaction.funds_confirmation_status}:{@code NULL} 表示这笔支付没有延迟 * (支付即核销的到店消费),{@code pending} 表示钱还在通道冻着,{@code confirmed} 表示已经分配完成。 * 自动提现只认 {@code NULL} 和 {@code confirmed},因此「忘了确认」不会变成「提前把钱提走」。 */ @Service public class PaymentFundConfirmationService { /** * 资金没有走延迟入账,不需要确认。 * *

这些常量是「返回给调用方的结论」,与 {@code payment_transaction.funds_confirmation_status} 里存的 * 状态不同名:库里存小写({@code pending}/{@code confirming}/{@code confirmed}),所以比较一律走 * {@link #isConfirmed(String)},不要直接拿常量去比字段值。 */ public static final String NOT_DELAYED = "NOT_DELAYED"; /** 资金已分配给收款商户。 */ public static final String CONFIRMED = "CONFIRMED"; /** 支付本身还没有成功,稍后再确认。 */ public static final String NOT_SETTLED = "NOT_SETTLED"; /** 已经有一笔确认请求在途,交给定时任务收敛。 */ public static final String PROCESSING = "PROCESSING"; /** 通道明确拒绝,需要人工处理。 */ public static final String FAILED = "FAILED"; /** 找不到对应的支付记录。 */ public static final String UNKNOWN = "UNKNOWN"; private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final Logger log = LoggerFactory.getLogger(PaymentFundConfirmationService.class); private final JdbcTemplate jdbc; private final TransactionTemplate transactions; private final HuifuDelayAccountGateway gateway; public PaymentFundConfirmationService(JdbcTemplate jdbc, TransactionTemplate transactions, HuifuDelayAccountGateway gateway) { this.jdbc = jdbc; this.transactions = transactions; this.gateway = gateway; } /** * 为某个订单确认资金。订单必须已经有成功的支付记录。 * *

幂等:资金已经确认、或这笔支付本来就没有延迟时直接返回,不会重复向通道发确认请求。 */ public String confirmForOrder(long orderId) { List> rows = DatabaseRows.normalize(jdbc.queryForList(""" select p.id, p.payment_no, p.status, p.amount, p.provider_huifu_id, p.provider_transaction_no, p.funds_confirmation_status, p.created_at, o.collect_huifu_id from payment_transaction p join benefit_order o on o.id = p.order_id where p.order_id = ? order by p.id desc limit 1 """, orderId)); if (rows.isEmpty()) return UNKNOWN; Map payment = rows.get(0); String confirmationStatus = text(payment.get("fundsConfirmationStatus")); if (isConfirmed(confirmationStatus)) return CONFIRMED; if (confirmationStatus.isBlank()) return NOT_DELAYED; if (!"SUCCEEDED".equals(text(payment.get("status")))) return NOT_SETTLED; String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId"))); if (huifuId.isBlank()) { log.warn("Payment {} has no collecting sub-merchant, fund confirmation is left pending", payment.get("paymentNo")); return PROCESSING; } long paymentId = number(payment.get("id")); if (!claim(paymentId)) { // 另一个请求已经在确认这笔资金,或者它刚被确认为终态。 return isConfirmed(currentStatus(paymentId)) ? CONFIRMED : PROCESSING; } String confirmNo = BusinessIdGenerator.next("DC"); HuifuDelayConfirmation result; try { result = gateway.confirm(new HuifuDelayConfirmCommand( confirmNo, huifuId, text(payment.get("paymentNo")), text(payment.get("providerTransactionNo")), REQUEST_DATE.format(localDate(payment.get("createdAt"))), "核销后确认资金")); } catch (HuifuPaymentGatewayException exception) { // 请求可能已经被通道受理:退回 pending 交给后续重试,绝不按失败处理,否则钱会永远冻在通道。 log.warn("Fund confirmation for payment {} has an unknown result: {}", payment.get("paymentNo"), exception.getMessage()); recordAttempt(paymentId, confirmNo, "", exception.getMessage()); return PROCESSING; } catch (RuntimeException exception) { log.warn("Fund confirmation for payment {} failed locally: {}", payment.get("paymentNo"), exception.getMessage()); return PROCESSING; } return applyResult(paymentId, confirmNo, result); } /** 认领这笔资金确认:只有仍为 pending 的那一次能拿到,避免重复向通道发确认。 */ private boolean claim(long paymentId) { int claimed = jdbc.update(""" update payment_transaction set funds_confirmation_status = 'confirming', funds_confirm_request_date = ? where id = ? and funds_confirmation_status = 'pending' """, REQUEST_DATE.format(LocalDate.now()), paymentId); return claimed == 1; } /** 数据库里的确认状态是小写,比较必须大小写无关。 */ private static boolean isConfirmed(String status) { return status != null && "confirmed".equalsIgnoreCase(status.trim()); } private String currentStatus(long paymentId) { List> rows = DatabaseRows.normalize(jdbc.queryForList( "select funds_confirmation_status from payment_transaction where id = ?", paymentId)); return rows.isEmpty() ? "" : text(rows.get(0).get("fundsConfirmationStatus")); } private String applyResult(long paymentId, String confirmNo, HuifuDelayConfirmation result) { switch (result.outcome()) { case SUCCEEDED -> { transactions.executeWithoutResult(status -> jdbc.update(""" update payment_transaction set funds_confirmation_status = 'confirmed', funds_confirm_no = ?, funds_confirmed_amount = ?, funds_confirm_code = ?, funds_confirm_message = ?, funds_confirmed_at = now() where id = ? """, confirmNo, result.confirmedAmountInCent(), result.code(), result.describe(), paymentId)); log.info("Payment fund confirmation {} succeeded: confirmed={}", confirmNo, result.confirmedAmount()); return CONFIRMED; } case FAILED -> { // 通道明确拒绝:通常是参数或商户配置问题,保持可重试状态并留下证据。 transactions.executeWithoutResult(status -> jdbc.update(""" update payment_transaction set funds_confirmation_status = 'pending', funds_confirm_no = ?, funds_confirm_code = ?, funds_confirm_message = ? where id = ? """, confirmNo, result.code(), result.describe(), paymentId)); log.warn("Payment fund confirmation {} was rejected: {} ({})", confirmNo, result.describe(), result.code()); return FAILED; } default -> { transactions.executeWithoutResult(status -> jdbc.update(""" update payment_transaction set funds_confirmation_status = 'pending', funds_confirm_no = ?, funds_confirm_code = ?, funds_confirm_message = ? where id = ? """, confirmNo, result.code(), result.describe(), paymentId)); return PROCESSING; } } } /** 只记录一次尝试的请求号与描述,不改状态。 */ private void recordAttempt(long paymentId, String confirmNo, String code, String message) { jdbc.update(""" update payment_transaction set funds_confirm_no = ?, funds_confirm_code = ?, funds_confirm_message = ? where id = ? """, confirmNo, code, message == null ? "" : message, paymentId); } private long number(Object value) { return value instanceof Number number ? number.longValue() : 0L; } private LocalDate localDate(Object value) { if (value instanceof java.sql.Timestamp timestamp) return timestamp.toLocalDateTime().toLocalDate(); if (value instanceof java.time.LocalDateTime dateTime) return dateTime.toLocalDate(); return LocalDate.now(); } private String text(Object value) { return value == null ? "" : String.valueOf(value).trim(); } private String firstNonBlank(String first, String second) { return first == null || first.isBlank() ? (second == null ? "" : second) : first; } }