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;
|
|
/**
|
* 延迟交易的资金确认。
|
*
|
* <p>商户开通延迟入账后,用 {@code delay_acct_flag=Y} 下的支付会把钱留在通道的「待确认」状态。业务规则是
|
* 「核销之后商户才能拿到钱」,所以核销成功时才调用交易确认接口,把这笔钱分配给收款子商户。
|
*
|
* <p>状态记在 {@code payment_transaction.funds_confirmation_status}:{@code NULL} 表示这笔支付没有延迟
|
* (支付即核销的到店消费),{@code pending} 表示钱还在通道冻着,{@code confirmed} 表示已经分配完成。
|
* 自动提现只认 {@code NULL} 和 {@code confirmed},因此「忘了确认」不会变成「提前把钱提走」。
|
*/
|
@Service
|
public class PaymentFundConfirmationService {
|
|
/**
|
* 资金没有走延迟入账,不需要确认。
|
*
|
* <p>这些常量是「返回给调用方的结论」,与 {@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;
|
}
|
|
/**
|
* 为某个订单确认资金。订单必须已经有成功的支付记录。
|
*
|
* <p>幂等:资金已经确认、或这笔支付本来就没有延迟时直接返回,不会重复向通道发确认请求。
|
*/
|
public String confirmForOrder(long orderId) {
|
List<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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;
|
}
|
}
|