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.HuifuDelayConfirmRefundCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmation;
|
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayRevert;
|
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.TransactionDefinition;
|
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},因此「忘了确认」不会变成「提前把钱提走」。
|
*
|
* <p>撤销核销走反方向({@link #revertForOrder}):调用交易确认退款把已确认的钱退回延迟户,成功后状态
|
* 退回 {@code pending},商户重新核销时会再确认一次。回退期间用 {@code reverting} 占位。
|
*/
|
@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";
|
/**
|
* 订单此刻不再是「已核销」,不能把钱放给商户。
|
*
|
* <p>撤销核销会把订单翻回待核销,此时钱必须留在延迟户:确认在放款前复查这个前提,
|
* 避免「订单已回到待核销、钱却已经打给商户」。
|
*/
|
public static final String NOT_VERIFIED = "NOT_VERIFIED";
|
|
/** 钱已退回(或本来就在)通道延迟户,撤销核销可以继续。 */
|
public static final String REVERTED = "REVERTED";
|
/** 这笔资金不需要回退:没有支付记录、支付没有走延迟入账,或钱本来就在延迟户。 */
|
public static final String REVERT_NOT_NEEDED = "REVERT_NOT_NEEDED";
|
/** 通道明确拒绝回退(例如资金已结算到银行卡、基本户余额不足),钱没有动。 */
|
public static final String REVERT_REJECTED = "REVERT_REJECTED";
|
/** 回退结果未定或仍在处理中:钱的位置不确定,撤销核销必须停在「已核销」。 */
|
public static final String REVERT_IN_FLIGHT = "REVERT_IN_FLIGHT";
|
/**
|
* 返回码在这条路径上语义未定:通道给了话,但我们不知道它是什么意思,需要人工核对。
|
*
|
* <p>和 {@link #REVERT_IN_FLIGHT} 分开是因为给商户的话不同:在途可以说「稍后重试」,
|
* 语义未定只能请他联系平台——它也可能是「已结算」,让商户反复重试是没有意义的。
|
*/
|
public static final String REVERT_NEEDS_REVIEW = "REVERT_NEEDS_REVIEW";
|
/** 回退请求根本没发出去(本地配置或 SDK 出错):钱没动,可以重试。 */
|
public static final String REVERT_NOT_SENT = "REVERT_NOT_SENT";
|
/** 找不到支付记录,或缺少定位原确认请求的键。 */
|
public static final String REVERT_UNKNOWN = "REVERT_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;
|
|
/**
|
* 资金侧的写入用独立事务提交。
|
*
|
* <p>撤销核销会在自己的事务里调用本服务,而通道调用不是事务性的:认领和结果如果跟着外层事务一起
|
* 回滚,通道那边已经退回延迟户的钱在本地就再也看不到了——状态还写着 {@code confirmed},钱其实已经
|
* 不在商户基本户。用独立事务提交,判断依据就永远留在库里。
|
*/
|
private final TransactionTemplate moneyTransactions;
|
|
public PaymentFundConfirmationService(JdbcTemplate jdbc, TransactionTemplate transactions,
|
HuifuDelayAccountGateway gateway) {
|
this.jdbc = jdbc;
|
this.transactions = transactions;
|
this.gateway = gateway;
|
// 注入的 TransactionTemplate 是容器里的单例,支付、退款和自动提现共用它,不能改它的传播行为,
|
// 否则那些服务会跟着变成 REQUIRES_NEW、外层事务被静默挂起。这里自己新建一个。
|
this.moneyTransactions = new TransactionTemplate(transactions.getTransactionManager());
|
this.moneyTransactions.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
}
|
|
/**
|
* 为某个订单确认资金。订单必须已经有成功的支付记录。
|
*
|
* <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"));
|
ClaimOutcome claimOutcome = transactions.execute(status -> claimConfirmation(orderId, paymentId));
|
if (claimOutcome == ClaimOutcome.ORDER_NOT_VERIFIED) return NOT_VERIFIED;
|
if (claimOutcome != ClaimOutcome.CLAIMED) {
|
// 另一个请求已经在确认这笔资金,或者它刚被确认为终态。
|
if (isConfirmed(currentStatus(paymentId))) return CONFIRMED;
|
// 当天认领出来的「确认中」不能重发确认(claim 只在次日再认领),但「那次确认到底怎么样了」
|
// 可以查:确认查询接口从写下那天起就是死代码,这里把它接上,让同一天的确认中也能收敛,
|
// 而不是整整等一天。
|
String queried = queryInFlightConfirmation(paymentId);
|
return queried != null ? queried : 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);
|
}
|
|
/**
|
* 查一次「确认中」那笔确认的结果。
|
*
|
* <p>认领规则刻意不允许当天重发确认(避免重复分账),于是当天认领出来的确认中只能等到次日才有人再看
|
* ——这期间商户的钱明明已经有了结论却没人知道。确认查询接口({@code delaytrans.confirmquery})
|
* 从写下那天起就是死代码,这里把它接上:查得到就按结果落地(成功 → confirmed,失败/在途 → pending,
|
* 下次还能重发),查不到就保持原样交给后续轮次。
|
*
|
* @return 应用了查询结果后的结论;拿不到定位键或查询失败时返回 {@code null},表示「没查出来」。
|
*/
|
private String queryInFlightConfirmation(long paymentId) {
|
List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
|
select p.funds_confirm_no, p.provider_huifu_id, o.collect_huifu_id
|
from payment_transaction p
|
join benefit_order o on o.id = p.order_id
|
where p.id = ?
|
limit 1
|
""", paymentId));
|
if (rows.isEmpty()) return null;
|
Map<String, Object> payment = rows.get(0);
|
String confirmNo = text(payment.get("fundsConfirmNo"));
|
String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
|
if (confirmNo.isBlank() || huifuId.isBlank()) return null;
|
try {
|
return applyResult(paymentId, confirmNo, gateway.queryConfirm(confirmNo, huifuId));
|
} catch (HuifuPaymentGatewayException exception) {
|
log.warn("Fund confirmation query for {} has no usable answer: {}", confirmNo, exception.getMessage());
|
return null;
|
} catch (RuntimeException exception) {
|
log.warn("Fund confirmation query for {} failed locally: {}", confirmNo, exception.getMessage());
|
return null;
|
}
|
}
|
|
/**
|
* 重发一次交易确认退款(撤销核销的回退)。
|
*
|
* <p>回退没有查询接口,而补偿任务原先只扫「待确认/确认中」——停在 {@code reverting} 的记录永远
|
* 没人再看,撤销核销就会被永久卡住。回退以 {@code funds_revert_no} 为幂等键,通道对同一个请求号
|
* 只会执行一次并回答它当前的状态,所以**幂等的重发就是它的查询对等物**:明确成功/失败就落地,
|
* 仍然在途就继续停在 reverting 等下一轮。
|
*
|
* @return 与 {@link #revertForOrder(long)} 同一套结论常量。
|
*/
|
public String retryRevertForOrder(long orderId) {
|
List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
|
select p.id, p.payment_no, p.funds_confirmation_status, p.funds_revert_no,
|
p.funds_revert_request_date, p.funds_confirmed_amount, p.funds_confirm_no,
|
p.funds_confirm_request_date, p.provider_huifu_id,
|
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 REVERT_UNKNOWN;
|
Map<String, Object> payment = rows.get(0);
|
if (!statusIs(text(payment.get("fundsConfirmationStatus")), "reverting")) return REVERT_NOT_NEEDED;
|
String revertNo = text(payment.get("fundsRevertNo"));
|
String requestDate = text(payment.get("fundsRevertRequestDate"));
|
String confirmNo = text(payment.get("fundsConfirmNo"));
|
String confirmRequestDate = text(payment.get("fundsConfirmRequestDate"));
|
String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
|
if (revertNo.isBlank() || requestDate.isBlank() || confirmNo.isBlank() || confirmRequestDate.isBlank() || huifuId.isBlank()) {
|
// 缺定位键就重发不了,钱的位置仍然未定:留在 reverting 等人工,绝不猜。
|
log.warn("Payment {} has an in-flight revert with no usable reference, it needs a human", payment.get("paymentNo"));
|
return REVERT_UNKNOWN;
|
}
|
long paymentId = number(payment.get("id"));
|
HuifuDelayRevert result;
|
try {
|
result = gateway.refund(new HuifuDelayConfirmRefundCommand(
|
revertNo, huifuId, confirmNo, confirmRequestDate, "撤销核销退回延迟户(补偿重发)"));
|
} catch (HuifuPaymentGatewayException exception) {
|
recordRevertAttempt(paymentId, revertNo, "", exception.getMessage());
|
return REVERT_IN_FLIGHT;
|
} catch (RuntimeException exception) {
|
log.warn("Fund revert retry for payment {} failed before reaching the provider: {}",
|
payment.get("paymentNo"), exception.getMessage());
|
return REVERT_IN_FLIGHT;
|
}
|
return applyRevertResult(paymentId, revertNo, result, payment);
|
}
|
|
/**
|
* 撤销核销:把这笔已经确认给商户的资金退回通道延迟户。
|
*
|
* <p>调用方只有在拿到 {@link #REVERTED} 或 {@link #REVERT_NOT_NEEDED} 时才可以撤销核销。其余结果都
|
* 表示钱的位置不确定、或仍在商户手里:此时翻单会造成「订单显示待核销、钱已经结算给商户」。
|
*
|
* <p>方法自身不开事务,认领与落结果各走独立事务(见 {@link #moneyTransactions}):通道调用不是
|
* 事务性的,让这些写入跟着外层事务回滚,会把通道的真实结果丢掉。
|
*/
|
public String revertForOrder(long orderId) {
|
List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
|
select p.id, p.payment_no, p.funds_confirmation_status, p.funds_confirm_no,
|
p.funds_confirm_request_date, p.funds_confirmed_amount, p.provider_huifu_id,
|
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 REVERT_NOT_NEEDED;
|
|
Map<String, Object> payment = rows.get(0);
|
String fundsStatus = text(payment.get("fundsConfirmationStatus"));
|
// 没有走延迟入账(支付即核销的到店消费),或钱本来就在延迟户:同样不需要回退。
|
if (fundsStatus.isBlank() || statusIs(fundsStatus, "pending")) return REVERT_NOT_NEEDED;
|
// 回退在途或确认在途:钱在哪一边未定,等它收敛后再撤销。
|
if (statusIs(fundsStatus, "reverting") || statusIs(fundsStatus, "confirming")) return REVERT_IN_FLIGHT;
|
if (!statusIs(fundsStatus, "confirmed")) return REVERT_UNKNOWN;
|
|
String confirmNo = text(payment.get("fundsConfirmNo"));
|
String confirmRequestDate = text(payment.get("fundsConfirmRequestDate"));
|
if (confirmNo.isBlank() || confirmRequestDate.isBlank()) {
|
// 没有原确认的定位键就撤销不了确认;钱确实在商户手里,不能翻单,只能人工处理。
|
log.warn("Payment {} is confirmed but has no confirmation reference, the revert needs a human",
|
payment.get("paymentNo"));
|
return REVERT_UNKNOWN;
|
}
|
String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
|
if (huifuId.isBlank()) {
|
log.warn("Payment {} has no collecting sub-merchant, the revert needs a human", payment.get("paymentNo"));
|
return REVERT_UNKNOWN;
|
}
|
|
long paymentId = number(payment.get("id"));
|
String revertNo = BusinessIdGenerator.next("DR");
|
String requestDate = REQUEST_DATE.format(LocalDate.now());
|
if (!claimRevert(paymentId, revertNo, requestDate)) {
|
// 另一个请求已经在回退这笔钱。
|
return REVERT_IN_FLIGHT;
|
}
|
|
HuifuDelayRevert result;
|
try {
|
result = gateway.refund(new HuifuDelayConfirmRefundCommand(
|
revertNo, huifuId, confirmNo, confirmRequestDate, "撤销核销退回延迟户"));
|
} catch (HuifuPaymentGatewayException exception) {
|
// 请求可能已经被通道受理:停在 reverting 交给人工核对,绝不按失败处理,也绝不放行撤销核销。
|
log.warn("Fund revert for payment {} has an unknown result: {}",
|
payment.get("paymentNo"), exception.getMessage());
|
recordRevertAttempt(paymentId, revertNo, "", exception.getMessage());
|
return REVERT_IN_FLIGHT;
|
} catch (RuntimeException exception) {
|
// 通道根本没被调到(配置不完整、SDK 初始化失败之类):钱没动,可以重试。不能说成「已结算」,
|
// 商户照着这句话去找平台就白跑了。
|
log.warn("Fund revert for payment {} failed before reaching the provider: {}",
|
payment.get("paymentNo"), exception.getMessage());
|
abandonRevertClaim(paymentId, exception.getMessage());
|
return REVERT_NOT_SENT;
|
}
|
|
return applyRevertResult(paymentId, revertNo, result, payment);
|
}
|
|
/**
|
* 认领这笔资金确认:只有仍为 pending 的那一次能拿到,避免重复向通道发确认。
|
*
|
* <p>同时收掉「认领之后进程就没了」留下的孤儿:状态停在 {@code confirming}、认领日期又不是今天的
|
* 那些请求,说明那次确认没走到落结果就中断了(部署、崩溃、超时被杀)。补偿任务只挑得到这种行而
|
* 不会重发,没有这一步这笔钱就永远冻在延迟户里。重新确认是幂等的——通道会按「已无未确认金额」
|
* 返回,{@link HuifuDelayConfirmation} 把那个码当成功,所以隔天补发一次是安全的。
|
*
|
* <p>认领日期为空的行不动:那说明它不是我们认领出来的(手工数据),宁可留着给人工看。
|
*/
|
private boolean claim(long paymentId) {
|
String today = REQUEST_DATE.format(LocalDate.now());
|
int claimed = jdbc.update("""
|
update payment_transaction
|
set funds_confirmation_status = 'confirming', funds_confirm_request_date = ?
|
where id = ?
|
and (funds_confirmation_status = 'pending'
|
or (funds_confirmation_status = 'confirming'
|
and funds_confirm_request_date is not null
|
and funds_confirm_request_date < ?))
|
""", today, paymentId, today);
|
return claimed == 1;
|
}
|
|
/**
|
* 在订单行锁里复查「订单此刻仍然已核销」,再认领资金确认。
|
*
|
* <p>撤销核销锁的是同一行,因此这个判断在整个认领期间是稳定的:撤销核销提交在前,这里会看到
|
* {@code verify_status} 已经为 0 而拒绝认领;认领在前,撤销核销会看到 {@code confirming} 而拒绝翻单。
|
* 两个方向都不会出现「订单回到待核销、钱却打给了商户」。
|
*/
|
private ClaimOutcome claimConfirmation(long orderId, long paymentId) {
|
List<Map<String, Object>> orders = DatabaseRows.normalize(jdbc.queryForList("""
|
select verify_status, order_status, refund_status
|
from benefit_order
|
where id = ? and deleted = 0
|
limit 1 for update
|
""", orderId));
|
if (orders.isEmpty()) return ClaimOutcome.ORDER_NOT_VERIFIED;
|
Map<String, Object> order = orders.get(0);
|
if (number(order.get("verifyStatus")) != 1 || number(order.get("orderStatus")) != 1
|
|| number(order.get("refundStatus")) != 0) {
|
return ClaimOutcome.ORDER_NOT_VERIFIED;
|
}
|
return claim(paymentId) ? ClaimOutcome.CLAIMED : ClaimOutcome.NOT_CLAIMED;
|
}
|
|
private enum ClaimOutcome { CLAIMED, NOT_CLAIMED, ORDER_NOT_VERIFIED }
|
|
/** 认领这笔资金回退:只有仍为 confirmed 的那一次能拿到,避免重复向通道发回退请求。 */
|
private boolean claimRevert(long paymentId, String revertNo, String requestDate) {
|
int claimed = moneyTransactions.execute(status -> jdbc.update("""
|
update payment_transaction
|
set funds_confirmation_status = 'reverting', funds_revert_no = ?,
|
funds_revert_request_date = ?, funds_revert_code = null, funds_revert_message = null
|
where id = ? and funds_confirmation_status = 'confirmed'
|
""", revertNo, requestDate, paymentId));
|
return claimed == 1;
|
}
|
|
/**
|
* 落回退结果。
|
*
|
* <p>只有通道明确说成功才把状态退回 {@code pending}(等于「钱在延迟户」)。明确拒绝退回
|
* {@code confirmed},其余一律停在 {@code reverting}:这条路径上没有真实回执证明码的含义之前不猜,
|
* 因为把失败当成功会让订单翻回待核销而钱还在商户基本户。
|
*/
|
private String applyRevertResult(long paymentId, String revertNo, HuifuDelayRevert result,
|
Map<String, Object> payment) {
|
switch (result.outcome()) {
|
case SUCCEEDED -> {
|
// 上一轮的确认回执(funds_confirm_*)保留下来作为「曾经确认过」的证据;商户重新核销时
|
// 会走一次全新的交易确认。
|
Long revertedAmount = cents(payment.get("fundsConfirmedAmount"));
|
moneyTransactions.executeWithoutResult(status -> jdbc.update("""
|
update payment_transaction
|
set funds_confirmation_status = 'pending', funds_revert_no = ?,
|
funds_revert_code = ?, funds_revert_message = ?,
|
funds_reverted_amount = ?, funds_reverted_at = now()
|
where id = ? and funds_confirmation_status = 'reverting'
|
""", revertNo, result.code(), result.describe(), revertedAmount, paymentId));
|
log.info("Fund revert {} succeeded: the money is back in the delay account", revertNo);
|
return REVERTED;
|
}
|
case FAILED -> {
|
// 通道明确拒绝:钱没有动,状态退回可重试的 confirmed,并把回执留作证据。
|
moneyTransactions.executeWithoutResult(status -> jdbc.update("""
|
update payment_transaction
|
set funds_confirmation_status = 'confirmed',
|
funds_revert_code = ?, funds_revert_message = ?
|
where id = ? and funds_confirmation_status = 'reverting'
|
""", result.code(), result.describe(), paymentId));
|
log.warn("Fund revert {} was rejected: {} ({})", revertNo, result.describe(), result.code());
|
return REVERT_REJECTED;
|
}
|
case NEEDS_REVIEW -> {
|
// 通道给了话但语义未定(例如 23000003):不能假定钱回到了延迟户,留在 reverting 等人工。
|
// 通道流水号要进日志:人工去控台核对时靠它定位这笔回退。
|
recordRevertAttempt(paymentId, revertNo, result.code(), result.describe());
|
log.warn("Fund revert {} has an undetermined result, the order stays verified:"
|
+ " provider={} code={} desc={}",
|
revertNo, result.providerTransactionNo(), result.code(), result.describe());
|
return REVERT_NEEDS_REVIEW;
|
}
|
default -> {
|
// 通道还在处理:同样是钱的位置未定,但它是正常的在途,商户稍后重试就能拿到结果。
|
recordRevertAttempt(paymentId, revertNo, result.code(), result.describe());
|
return REVERT_IN_FLIGHT;
|
}
|
}
|
}
|
|
/** 只记录一次回退尝试的请求号与描述,不改状态。 */
|
private void recordRevertAttempt(long paymentId, String revertNo, String code, String message) {
|
moneyTransactions.executeWithoutResult(status -> jdbc.update("""
|
update payment_transaction
|
set funds_revert_no = ?, funds_revert_code = ?, funds_revert_message = ?
|
where id = ?
|
""", revertNo, code, message == null ? "" : message, paymentId));
|
}
|
|
/**
|
* 放弃这次回退认领:请求根本没发出去(本地配置或 SDK 出错),钱没有动,退回 confirmed 让商户重试。
|
*
|
* <p>请求号与请求日期要清掉:它们是给通道侧定位用的,这次通道根本没收到。失败原因留在
|
* {@code funds_revert_code/message} 里,免得排查时只看到一条干干净净的 confirmed。
|
*/
|
private void abandonRevertClaim(long paymentId, String message) {
|
moneyTransactions.executeWithoutResult(status -> jdbc.update("""
|
update payment_transaction
|
set funds_confirmation_status = 'confirmed', funds_revert_no = null,
|
funds_revert_request_date = null, funds_revert_code = 'LOCAL_ERROR',
|
funds_revert_message = ?
|
where id = ? and funds_confirmation_status = 'reverting'
|
""", message == null ? "" : message, paymentId));
|
}
|
|
/** 这笔资金是否确实在通道延迟户——撤销核销只有在这个前提下才允许翻单。 */
|
public static boolean moneyBackInDelayAccount(String outcome) {
|
return REVERTED.equals(outcome) || REVERT_NOT_NEEDED.equals(outcome);
|
}
|
|
/**
|
* 回退没有成功时给商户的话。
|
*
|
* <p>不能只说「撤销失败」:商户需要分清是钱已经结算到银行卡(要联系平台),还是通道还在处理
|
* (稍后重试就行),否则只会反复点。文案跟结论常量放在一起,免得将来新增一种结论时漏配。
|
*/
|
public static String revertRefusalMessage(String outcome) {
|
if (REVERT_REJECTED.equals(outcome)) return "这笔订单的资金已结算到商户银行卡,无法撤销核销";
|
if (REVERT_IN_FLIGHT.equals(outcome)) return "这笔订单的资金正在退回中,请稍后重试";
|
if (REVERT_NOT_SENT.equals(outcome)) return "撤销核销暂时无法发起,请稍后重试";
|
return "这笔订单的资金状态无法核对,请联系平台处理";
|
}
|
|
/** 数据库里的状态是小写,比较必须大小写无关。 */
|
private static boolean statusIs(String status, String expected) {
|
return status != null && expected.equalsIgnoreCase(status.trim());
|
}
|
|
private static boolean isConfirmed(String status) {
|
return statusIs(status, "confirmed");
|
}
|
|
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;
|
}
|
|
/** 库里的金额字段(分);空值保持 null,好和「没记过金额」区分开。 */
|
private static Long cents(Object value) {
|
return value instanceof Number number ? number.longValue() : null;
|
}
|
|
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;
|
}
|
}
|