package com.talentcard.server.business.payment.huifu;
|
|
import com.alibaba.fastjson.JSON;
|
import com.huifu.bspay.sdk.opps.client.BasePayClient;
|
import com.huifu.bspay.sdk.opps.core.BasePay;
|
import com.huifu.bspay.sdk.opps.core.config.MerConfig;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.stereotype.Service;
|
|
import java.time.LocalDate;
|
import java.time.format.DateTimeFormatter;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
|
/**
|
* 斗拱延迟交易的确认网关。
|
*
|
* <p>商户开通延迟入账({@code delay_flag=Y})后,用 {@code delay_acct_flag=Y} 下的支付会停在
|
* 「待确认」({@code unconfirm_amt})状态:钱已经在汇付,但没有分配给任何一方。只有调用交易确认
|
* 接口,这笔钱才会按分账串分配给收款子商户。
|
*
|
* <p>业务上是否延迟由订单类型决定:先付钱后核销的订单延迟,核销成功才确认;支付即核销的订单不延迟,
|
* 因此不需要确认。
|
*/
|
@Service
|
public class HuifuDelayAccountGateway {
|
|
private static final Object SDK_INIT_LOCK = new Object();
|
private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
private static final Logger log = LoggerFactory.getLogger(HuifuDelayAccountGateway.class);
|
|
/** 交易确认:把待确认金额分配给分账接收方。 */
|
private static final String CONFIRM_FUNCTION = "v2.trade.payment.delaytrans.confirm";
|
/** 交易确认查询:按原确认请求定位一笔确认。 */
|
private static final String CONFIRM_QUERY_FUNCTION = "v2.trade.payment.delaytrans.confirmquery";
|
/** 交易确认退款:把已确认的金额退回原商户账户。 */
|
private static final String CONFIRM_REFUND_FUNCTION = "v2.trade.payment.delaytrans.confirmrefund";
|
|
private final HuifuPaymentProperties properties;
|
|
public HuifuDelayAccountGateway(HuifuPaymentProperties properties) {
|
this.properties = properties;
|
}
|
|
/**
|
* 确认一笔延迟交易,把钱分配给该子商户。
|
*
|
* <p>{@code acctSplitBunch} 留空表示「按交易时的分账串处理分账资金」,也就是仍然分给原收款商户,
|
* 这正是我们的场景:钱本来就该给收款商户,只是要等核销后才给。
|
*/
|
public HuifuDelayConfirmation confirm(HuifuDelayConfirmCommand command) {
|
ensureConfigured();
|
initializeSdk();
|
Map<String, Object> request = new LinkedHashMap<>();
|
request.put("req_date", REQUEST_DATE.format(LocalDate.now()));
|
request.put("req_seq_id", command.confirmNo());
|
request.put("huifu_id", command.huifuId().trim());
|
putIfPresent(request, "org_hf_seq_id", command.originalProviderNo());
|
putIfPresent(request, "org_req_date", command.originalRequestDate());
|
putIfPresent(request, "org_req_seq_id", command.paymentNo());
|
putIfPresent(request, "remark", command.remark());
|
log.info("Huifu delay confirmation request: {}", JSON.toJSONString(Map.of(
|
"req_seq_id", command.confirmNo(),
|
"huifu_id", command.huifuId().trim(),
|
"org_req_seq_id", command.paymentNo(),
|
"org_hf_seq_id", command.originalProviderNo() == null ? "" : command.originalProviderNo())));
|
return execute(request, CONFIRM_FUNCTION, "汇付交易确认失败");
|
}
|
|
/** 查询一次交易确认的结果。 */
|
public HuifuDelayConfirmation queryConfirm(String confirmNo, String huifuId) {
|
ensureConfigured();
|
initializeSdk();
|
Map<String, Object> request = new LinkedHashMap<>();
|
request.put("req_date", REQUEST_DATE.format(LocalDate.now()));
|
request.put("req_seq_id", confirmNo);
|
request.put("huifu_id", huifuId.trim());
|
return execute(request, CONFIRM_QUERY_FUNCTION, "汇付交易确认查询失败");
|
}
|
|
/** 把已确认的金额退回原商户账户。 */
|
public HuifuDelayConfirmation refund(HuifuDelayConfirmRefundCommand command) {
|
ensureConfigured();
|
initializeSdk();
|
Map<String, Object> request = new LinkedHashMap<>();
|
request.put("req_date", REQUEST_DATE.format(LocalDate.now()));
|
request.put("req_seq_id", command.refundNo());
|
request.put("huifu_id", command.huifuId().trim());
|
request.put("org_req_date", command.confirmRequestDate());
|
request.put("org_req_seq_id", command.confirmNo());
|
putIfPresent(request, "remark", command.remark());
|
log.info("Huifu delay confirmation refund request: {}", JSON.toJSONString(Map.of(
|
"req_seq_id", command.refundNo(),
|
"huifu_id", command.huifuId().trim(),
|
"org_req_seq_id", command.confirmNo())));
|
return execute(request, CONFIRM_REFUND_FUNCTION, "汇付交易确认退款失败");
|
}
|
|
private HuifuDelayConfirmation execute(Map<String, Object> request, String function, String failureMessage) {
|
Map<String, Object> response;
|
try {
|
response = BasePayClient.request(request, function, false);
|
} catch (Exception exception) {
|
log.warn("{}: type={}, message={}", failureMessage, exception.getClass().getSimpleName(),
|
safe(exception.getMessage()));
|
throw new HuifuPaymentGatewayException(failureMessage, isOutcomeUnknown(exception), exception);
|
}
|
if (response == null) throw new HuifuPaymentGatewayException(failureMessage, true, null);
|
return HuifuDelayConfirmation.from(response);
|
}
|
|
private void ensureConfigured() {
|
if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "汇付支付尚未启用");
|
if (blank(properties.getSystemId()) || blank(properties.getProductId())
|
|| blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置不完整");
|
}
|
}
|
|
private void initializeSdk() {
|
synchronized (SDK_INIT_LOCK) {
|
BasePay.debug = false;
|
BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment())
|
? BasePay.MODE_PROD : BasePay.MODE_TEST;
|
MerConfig config = new MerConfig();
|
config.setSysId(properties.getSystemId().trim());
|
config.setProductId(properties.getProductId().trim());
|
config.setRsaPrivateKey(properties.getPrivateKey().trim());
|
config.setRsaPublicKey(properties.getPlatformPublicKey().trim());
|
try {
|
BasePay.initWithMerConfig(config);
|
} catch (Exception exception) {
|
throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置初始化失败");
|
}
|
}
|
}
|
|
private static void putIfPresent(Map<String, Object> request, String key, String value) {
|
if (value != null && !value.isBlank()) request.put(key, value.trim());
|
}
|
|
private boolean blank(String value) {
|
return value == null || value.isBlank();
|
}
|
|
private String safe(String message) {
|
if (message == null) return "";
|
return message.length() > 300 ? message.substring(0, 300) : message;
|
}
|
|
private boolean isOutcomeUnknown(Exception exception) {
|
String message = safe(exception.getMessage()).toLowerCase();
|
return message.contains("timeout") || message.contains("no response") || message.contains("timed out");
|
}
|
|
/** 交易确认与交易确认退款请求的参数,两笔请求都用业务请求号做幂等键。 */
|
public record HuifuDelayConfirmCommand(
|
String confirmNo,
|
String huifuId,
|
String paymentNo,
|
String originalProviderNo,
|
String originalRequestDate,
|
String remark
|
) {
|
}
|
|
/** 交易确认退款请求:{@code confirmNo} 是原交易确认的请求流水号。 */
|
public record HuifuDelayConfirmRefundCommand(
|
String refundNo,
|
String huifuId,
|
String confirmNo,
|
String confirmRequestDate,
|
String remark
|
) {
|
}
|
|
/** 金额字段按斗拱的两位小数元字符串保留,避免本地换算引入精度问题。 */
|
public record HuifuDelayConfirmation(
|
Outcome outcome,
|
String code,
|
String message,
|
String unconfirmedAmount,
|
String confirmedAmount,
|
String providerTransactionNo
|
) {
|
|
public enum Outcome { SUCCEEDED, PROCESSING, FAILED }
|
|
public boolean settled() {
|
return outcome != Outcome.PROCESSING;
|
}
|
|
static HuifuDelayConfirmation from(Map<String, Object> response) {
|
Map<String, Object> data = HuifuPayload.businessData(response);
|
String code = HuifuPayload.code(data);
|
String message = HuifuPayload.firstNonBlank(
|
HuifuPayload.value(data, "sub_resp_desc", "trans_desc"), HuifuPayload.message(data));
|
String status = HuifuPayload.value(data, "trans_stat", "trans_status", "transStat", "transStatus");
|
String unconfirmed = HuifuPayload.value(data, "unconfirm_amt", "unconfirmAmt");
|
String confirmed = HuifuPayload.value(data, "confirmed_amt", "confirmedAmt");
|
String providerNo = HuifuPayload.value(data, "hf_seq_id", "org_hf_seq_id", "hfSeqId", "orgHfSeqId");
|
return new HuifuDelayConfirmation(classify(status, code), code, message, unconfirmed, confirmed, providerNo);
|
}
|
|
/**
|
* 交易确认的同步响应只有 {@code resp_code} 和金额字段,没有交易状态;受理成功码是
|
* {@code 00000000},{@code 00000100} 表示还在处理中。明确的失败码按失败处理。
|
*/
|
private static Outcome classify(String status, String code) {
|
boolean accepted = HuifuPayload.successCode(code);
|
if (HuifuPayload.status(status) == HuifuPayload.TransStatus.FAILED) return Outcome.FAILED;
|
if (accepted) return Outcome.SUCCEEDED;
|
if ("00000100".equals(code == null ? "" : code.trim())) return Outcome.PROCESSING;
|
return code == null || code.isBlank() ? Outcome.PROCESSING : Outcome.FAILED;
|
}
|
|
/** 供日志与故障定位使用的一句话结论。 */
|
public String describe() {
|
if (!message.isBlank()) return message;
|
if (!code.isBlank()) return "Huifu delay operation returned code " + code;
|
return "The provider returned no description";
|
}
|
|
/** 把「两位小数元」转成本地分;空值或非法值返回 null。 */
|
public Long confirmedAmountInCent() {
|
return HuifuPayload.amountInCent(confirmedAmount);
|
}
|
}
|
}
|