package com.talentcard.server.business.payment.huifu;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
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.huifu.bspay.sdk.opps.core.request.V3TradePaymentJspayRequest;
|
import com.huifu.bspay.sdk.opps.core.request.V3TradePaymentScanpayQueryRequest;
|
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;
|
|
@Service
|
public class HuifuMiniProgramPaymentGateway {
|
|
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(HuifuMiniProgramPaymentGateway.class);
|
|
private final HuifuPaymentProperties properties;
|
private volatile boolean initialized;
|
|
public HuifuMiniProgramPaymentGateway(HuifuPaymentProperties properties) {
|
this.properties = properties;
|
}
|
|
public Map<String, Object> createMiniProgramPayment(HuifuMiniProgramPaymentCommand command) {
|
ensureConfigured();
|
initializeSdk();
|
V3TradePaymentJspayRequest request = new V3TradePaymentJspayRequest();
|
request.setReqDate(REQUEST_DATE.format(LocalDateTime.now()));
|
request.setReqSeqId(command.paymentNo());
|
request.setHuifuId(properties.getMerchantId().trim());
|
request.setGoodsDesc(command.goodsDescription());
|
request.setTransAmt(BigDecimal.valueOf(command.amountInCent(), 2).toPlainString());
|
request.setTradeType("T_MINIAPP");
|
|
Map<String, String> wxData = new LinkedHashMap<>();
|
wxData.put("sub_openid", command.openId());
|
wxData.put("sub_appid", properties.getMiniProgramAppId().trim());
|
Map<String, Object> extendInfo = new LinkedHashMap<>();
|
extendInfo.put("wx_data", JSON.toJSONString(wxData));
|
extendInfo.put("notify_url", properties.getNotifyUrl().trim());
|
extendInfo.put("delay_acct_flag", properties.isDelayAccount() ? "Y" : "N");
|
extendInfo.put("time_expire", command.expiresAt().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
|
request.setExtendInfo(extendInfo);
|
log.info("Huifu mini-program payment request: {}", logJson(Map.of(
|
"req_date", REQUEST_DATE.format(LocalDateTime.now()),
|
"req_seq_id", command.paymentNo(),
|
"huifu_id", properties.getMerchantId().trim(),
|
"goods_desc", command.goodsDescription(),
|
"trans_amt", BigDecimal.valueOf(command.amountInCent(), 2).toPlainString(),
|
"trade_type", "T_MINIAPP",
|
"system_id", properties.getSystemId().trim(),
|
"product_id", properties.getProductId().trim(),
|
"extend_info", extendInfo
|
)));
|
try {
|
Map<String, Object> response = BasePayClient.request(request, false);
|
log.info("Huifu mini-program payment response: {}", logJson(response));
|
return response;
|
} catch (Exception exception) {
|
// 支付参数包含用户 OpenID 与签名材料,日志仅保留 SDK 错误类型及平台消息。
|
log.warn("Huifu mini-program payment request failed: type={}, message={}",
|
exception.getClass().getSimpleName(), safeMessage(exception.getMessage()));
|
throw new HuifuPaymentGatewayException(
|
"汇付支付创建失败,请稍后重试",
|
isOutcomeUnknown(exception),
|
exception
|
);
|
}
|
}
|
|
public Map<String, Object> queryMiniProgramPayment(HuifuPaymentQueryCommand command) {
|
ensureConfigured();
|
initializeSdk();
|
V3TradePaymentScanpayQueryRequest request = new V3TradePaymentScanpayQueryRequest();
|
request.setHuifuId(properties.getMerchantId().trim());
|
request.setOrgReqDate(command.requestDate());
|
request.setOrgReqSeqId(command.paymentNo());
|
if (!blank(command.providerTransactionNo())) request.setOrgHfSeqId(command.providerTransactionNo());
|
try {
|
return BasePayClient.request(request, false);
|
} catch (Exception exception) {
|
log.warn("Huifu mini-program payment query failed: type={}, message={}",
|
exception.getClass().getSimpleName(), safeMessage(exception.getMessage()));
|
throw new HuifuPaymentGatewayException("Huifu payment query failed", isOutcomeUnknown(exception), exception);
|
}
|
}
|
|
private void ensureConfigured() {
|
if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "汇付支付尚未启用");
|
if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getMerchantId())
|
|| blank(properties.getMiniProgramAppId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())
|
|| blank(properties.getNotifyUrl())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置不完整");
|
}
|
}
|
|
private void initializeSdk() {
|
if (initialized) return;
|
synchronized (SDK_INIT_LOCK) {
|
if (initialized) return;
|
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);
|
initialized = true;
|
} catch (Exception exception) {
|
throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置初始化失败");
|
}
|
}
|
}
|
|
private boolean blank(String value) {
|
return value == null || value.isBlank();
|
}
|
|
private String safeMessage(String message) {
|
if (message == null || message.isBlank()) return "";
|
return message.length() > 500 ? message.substring(0, 500) : message;
|
}
|
|
private String logJson(Object value) {
|
try {
|
return JSON.toJSONString(sanitizeForLog(value));
|
} catch (Exception ignored) {
|
return "{\"log_error\":\"Unable to serialize payment payload\"}";
|
}
|
}
|
|
private Object sanitizeForLog(Object value) {
|
if (value instanceof Map<?, ?> source) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
String key = String.valueOf(entry.getKey());
|
result.put(key, isSensitiveKey(key) ? mask(String.valueOf(entry.getValue())) : sanitizeForLog(entry.getValue()));
|
}
|
return result;
|
}
|
if (value instanceof List<?> source) return source.stream().map(this::sanitizeForLog).toList();
|
if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
|
try {
|
return sanitizeForLog(JSON.parse(text));
|
} catch (Exception ignored) {
|
return text;
|
}
|
}
|
return value;
|
}
|
|
private boolean isSensitiveKey(String key) {
|
String normalized = key == null ? "" : key.toLowerCase();
|
return normalized.contains("private") || normalized.contains("secret") || normalized.contains("sign")
|
|| normalized.contains("openid") || normalized.contains("authorization") || normalized.contains("certificate");
|
}
|
|
private String mask(String value) {
|
if (value == null || value.isBlank()) return "";
|
if (value.length() <= 8) return "***";
|
return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
|
}
|
|
private boolean isOutcomeUnknown(Exception exception) {
|
String message = safeMessage(exception.getMessage()).toLowerCase();
|
return message.contains("no response") || message.contains("timeout") || message.contains("timed out");
|
}
|
}
|