package com.talentcard.server.business.payment;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.alibaba.fastjson.JSON;
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.payment.PaymentReconciliationService.PendingPaymentConfirmation;
|
import com.talentcard.server.business.payment.PaymentReconciliationService.PendingPaymentState;
|
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
@Service
|
public class HuifuPaymentService {
|
|
private static final String PROVIDER = "HUIFU";
|
private static final Logger log = LoggerFactory.getLogger(HuifuPaymentService.class);
|
|
private final JdbcTemplate jdbc;
|
private final TransactionTemplate transactions;
|
private final HuifuMiniProgramPaymentGateway gateway;
|
private final PaymentReconciliationService reconciliationService;
|
|
public HuifuPaymentService(
|
JdbcTemplate jdbc,
|
TransactionTemplate transactions,
|
HuifuMiniProgramPaymentGateway gateway,
|
PaymentReconciliationService reconciliationService
|
) {
|
this.jdbc = jdbc;
|
this.transactions = transactions;
|
this.gateway = gateway;
|
this.reconciliationService = reconciliationService;
|
}
|
|
public Map<String, Object> beginMiniProgramPayment(String orderNo, Long talentId, int priceVersion, String idempotencyKey) {
|
// 上一张凭证被要求关过(关单中/关单没结论)时,先问一次关单结论再决定能不能复用:
|
// 关单其实已经成功 → 它当场落成已关闭,下面给用户建一张新的;关单没生效 → 它还是可付的,
|
// 下面复用回去。这一步只在「被要求关过」的凭证上打通道,正常的复用(PREPAY_CREATED)不受影响。
|
reconciliationService.concludeUndecidedCloses(orderNo);
|
|
// 复用优先:本地已经持有这张订单的预支付单的支付参数、订单又还待支付时,直接把它交回客户端。
|
// 这里刻意**不查通道**:查通道只会得到「处理中」这种没有结论的答复,而本地已有可用凭证——
|
// 拿这个没有结论的答复把整笔订单停摆,正是用户报的死锁(见 docs/adr/0003 的 D3)。
|
Map<String, Object> reusable = reusablePrepay(orderNo, talentId);
|
if (reusable != null) {
|
return pendingResult(string(reusable.get("paymentNo")), string(reusable.get("clientPaymentPayload")));
|
}
|
|
// 本地没有可复用的凭证时,通道是唯一知道钱有没有收下的地方。上一次支付可能已经被通道收下、
|
// 回调却没到:那笔钱必须先落成本地已支付,否则这里会再发起一次支付(同一笔订单付第二次),
|
// 或者把用户送进「订单已经支付,请勿重复发起支付」的死胡同。
|
PendingPaymentConfirmation confirmation = reconciliationService.confirmPendingPayments(orderNo);
|
if (confirmation.collected()) return completedResult(confirmation.paymentNo());
|
// 通道答不出「钱收没收下」时不再抛错:那个不确定只属于这一次支付尝试,不该把用户堵在一句
|
// 看不懂的报错上。返回可等待状态,页面渲染成「支付处理中,请稍候」。
|
if (confirmation.state() == PendingPaymentState.UNCONFIRMED) return processingResult(confirmation.paymentNo());
|
|
PreparedPayment prepared = transactions.execute(status -> prepare(orderNo, talentId, priceVersion, idempotencyKey));
|
if (prepared == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付初始化失败");
|
if (prepared.completed()) return completedResult(prepared.paymentNo());
|
if (prepared.processing()) return processingResult(prepared.paymentNo());
|
if (!prepared.callProvider()) return pendingResult(prepared.paymentNo(), prepared.clientPayload());
|
|
Map<String, Object> response;
|
try {
|
response = gateway.createMiniProgramPayment(new HuifuMiniProgramPaymentCommand(
|
prepared.paymentNo(), prepared.huifuId(), prepared.openId(), prepared.amountInCent(), prepared.goodsDescription(), prepared.expiresAt(),
|
prepared.delayAccount()));
|
} catch (HuifuPaymentGatewayException exception) {
|
if (exception.isOutcomeUnknown()) {
|
transactions.executeWithoutResult(status -> markCreateUnknown(prepared.paymentNo(), "GATEWAY_UNKNOWN", exception.getMessage()));
|
} else {
|
transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), "GATEWAY_REJECTED", exception.getMessage()));
|
}
|
throw new BusinessException(ErrorCode.CONFLICT, exception.getMessage());
|
} catch (BusinessException exception) {
|
transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), "LOCAL_ERROR", exception.getMessage()));
|
throw exception;
|
} catch (RuntimeException exception) {
|
transactions.executeWithoutResult(status -> markCreateUnknown(prepared.paymentNo(), "GATEWAY_UNKNOWN", "支付结果确认中"));
|
throw exception;
|
}
|
if (!providerAccepted(response)) {
|
String message = responseText(response, "支付服务暂不可用");
|
transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), responseCode(response), message));
|
throw new BusinessException(ErrorCode.CONFLICT, message);
|
}
|
String clientPayload = writeJson(clientPayload(response));
|
transactions.executeWithoutResult(status -> markPrepayCreated(prepared.paymentNo(), responseCode(response), responseText(response, ""), clientPayload));
|
return pendingResult(prepared.paymentNo(), clientPayload);
|
}
|
|
/**
|
* 本地是否还持有这张订单的预支付单的支付参数。
|
*
|
* <p>四个条件缺一不可:订单仍待支付且未过期、流水状态在可复用之列、支付参数还在本地、
|
* 流水金额与订单当前实付一致。金额必须相等是因为预支付单是按金额建的:改了优惠券之后再拿旧单
|
* 去收款会收错钱。流水上没有记 {@code price_version},金额相等是这里能拿到的最强判据。
|
*
|
* <p>可复用之列只有两种,差别在于「有没有被要求关过」:
|
* <ul>
|
* <li>{@code PREPAY_CREATED}:从没被要求关过,凭证当然还能付。</li>
|
* <li>{@code CANCEL_UNCONFIRMED}:要求关过,**但通道明确说这次关单没生效**(拒绝关单、关单请求
|
* 本身失败)。既然没关掉,它就还是那张可付的凭证——交回去,用户能付,也不会因此多出第二张
|
* 可付的凭证。</li>
|
* </ul>
|
*
|
* <p>{@code CLOSING}(关单中)**不在可复用之列**:那次关单可能已经生效,凭证可能已经死了,
|
* 交回去只会换来微信收银台一句「订单状态异常,支付请求无效」。它的结论由关单查询给出
|
* ({@link PaymentReconciliationService#concludeUndecidedCloses}):问出来是已关闭,用户下一次点
|
* 支付就拿到一张全新的凭证。
|
*/
|
private Map<String, Object> reusablePrepay(String orderNo, Long talentId) {
|
List<Map<String, Object>> rows = rows("""
|
select p.payment_no, p.client_payment_payload
|
from payment_transaction p
|
join benefit_order o on o.id = p.order_id
|
where p.provider = ? and p.order_no = ? and o.talent_id = ?
|
and o.deleted = 0 and o.order_status = 0
|
and (o.expires_at is null or o.expires_at > now())
|
and p.status in ('PREPAY_CREATED', 'CANCEL_UNCONFIRMED')
|
and coalesce(p.client_payment_payload, '') <> ''
|
and p.amount = o.pay_amount
|
order by p.id desc
|
limit 1
|
""", PROVIDER, orderNo, talentId);
|
return rows.isEmpty() ? null : rows.get(0);
|
}
|
|
/**
|
* 这张订单上是否还有一张**可能被支付**的预支付单。
|
*
|
* <p>新建预支付单的前置不变量:上一张必须已经被证明不可能再被支付(已关闭,或通道明确说失败/
|
* 交易不存在)。{@code CLOSING} 与 {@code CANCEL_UNCONFIRMED} 恰恰是「关不掉的预支付单」——通道
|
* 既没说关掉、也没说钱收下了,它照样能被付进来。此时新建等于给同一笔订单留下两张可被支付的
|
* 预支付单,通道确认两笔都成功时没有多收款退回流程(审计 §9 已复现过两次预授权)。
|
*/
|
private String blockingAttempt(long orderId) {
|
List<Map<String, Object>> rows = rows("""
|
select payment_no from payment_transaction
|
where order_id = ? and provider = ?
|
and status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED')
|
order by id desc
|
limit 1
|
""", orderId, PROVIDER);
|
return rows.isEmpty() ? "" : string(rows.get(0).get("paymentNo"));
|
}
|
|
private PreparedPayment prepare(String orderNo, Long talentId, int priceVersion, String idempotencyKey) {
|
if (idempotencyKey == null || idempotencyKey.isBlank() || idempotencyKey.length() > 128) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少有效支付幂等键");
|
}
|
List<Map<String, Object>> orders = rows("""
|
select o.id, o.order_no, o.talent_id, o.merchant_id, o.store_id, o.pay_amount, o.order_status, o.price_version,
|
o.expires_at, o.collect_huifu_id, o.order_type,
|
coalesce(o.product_name_snapshot, o.order_type, '人才权益服务') goods_description,
|
t.openid
|
from benefit_order o
|
join talent_user t on t.id = o.talent_id and t.deleted = 0
|
where o.deleted = 0 and o.order_no = ? and o.talent_id = ?
|
limit 1 for update
|
""", orderNo, talentId);
|
if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或无权支付");
|
Map<String, Object> order = orders.get(0);
|
long payAmount = number(order.get("payAmount"));
|
if (number(order.get("orderStatus")) == 1) return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false, false);
|
if (number(order.get("orderStatus")) != 0) throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可支付");
|
if (number(order.get("priceVersion")) != priceVersion) throw new BusinessException(ErrorCode.CONFLICT, "订单金额已更新,请重新确认");
|
LocalDateTime expiresAt = localDateTime(order.get("expiresAt"));
|
if (expiresAt != null && !expiresAt.isAfter(LocalDateTime.now())) throw new BusinessException(ErrorCode.CONFLICT, "订单已超时,请重新下单");
|
if (payAmount == 0) {
|
jdbc.update("""
|
update benefit_order
|
set order_status = 1,
|
verify_status = case when order_type = 'STORE_CONSUME' then 1 else verify_status end
|
where id = ? and order_status = 0
|
""", number(order.get("id")));
|
return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false, false);
|
}
|
// 收款子商户在下单时已固定,支付请求只使用订单上的斗拱商户号。
|
String huifuId = string(order.get("collectHuifuId"));
|
if (huifuId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "订单缺少收款子商户,请重新下单");
|
String openId = string(order.get("openid"));
|
if (openId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "当前用户未完成微信支付授权");
|
|
List<Map<String, Object>> existingRows = rows("""
|
select payment_no, order_no, talent_id, status, attempt_no, client_payment_payload
|
from payment_transaction
|
where idempotency_key = ?
|
order by attempt_no desc
|
limit 1 for update
|
""", idempotencyKey);
|
if (!existingRows.isEmpty()) {
|
Map<String, Object> existing = existingRows.get(0);
|
if (!orderNo.equals(string(existing.get("orderNo"))) || talentId.longValue() != number(existing.get("talentId"))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "支付请求已被其他订单使用,请重新发起支付");
|
}
|
String status = string(existing.get("status"));
|
String paymentNo = string(existing.get("paymentNo"));
|
if ("SUCCEEDED".equals(status)) return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, true, "", false, false);
|
if ("PREPAY_CREATED".equals(status)) {
|
// 支付参数没留在本地就没法把它复用给客户端,而这张预支付单可能已经可以被支付:
|
// 既不新建也不报错,交给对账通道确认后再给结论。
|
if (string(existing.get("clientPaymentPayload")).isBlank()) {
|
return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
|
}
|
return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false, false);
|
}
|
if ("CLOSING".equals(status) || "CANCEL_UNCONFIRMED".equals(status)) {
|
// 关单中/关不掉的预支付单:通道还可能为它收钱,绝不能再建一张。
|
return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
|
}
|
if ("CREATED".equals(status) || "CREATE_UNKNOWN".equals(status)) {
|
return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
|
}
|
String blocking = blockingAttempt(number(order.get("id")));
|
if (!blocking.isBlank()) {
|
return new PreparedPayment(blocking, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
|
}
|
int nextAttempt = (int) number(existing.get("attemptNo")) + 1;
|
String retryPaymentNo = BusinessIdGenerator.next("P");
|
insertPaymentTransaction(retryPaymentNo, order, orderNo, talentId, payAmount, huifuId, idempotencyKey, nextAttempt);
|
return new PreparedPayment(retryPaymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false, requiresDelayAccount(order));
|
}
|
|
String blocking = blockingAttempt(number(order.get("id")));
|
if (!blocking.isBlank()) {
|
return new PreparedPayment(blocking, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
|
}
|
String paymentNo = BusinessIdGenerator.next("P");
|
insertPaymentTransaction(paymentNo, order, orderNo, talentId, payAmount, huifuId, idempotencyKey, 1);
|
return new PreparedPayment(paymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false, requiresDelayAccount(order));
|
}
|
|
/**
|
* 这笔订单的支付是否必须延迟入账。
|
*
|
* <p>只有「支付即核销」的到店消费订单不需要延迟:它在支付成功的同时就完成核销,不存在「钱已收但服务
|
* 未确认」的窗口,延迟只会让商户的钱白等一道确认。其它订单类型(含以后新增的)一律延迟——这样即使
|
* 将来新增了订单类型而没人记得改这里,未核销的钱也会默认被扣在通道,而不是直接落进商户账户。
|
*/
|
private boolean requiresDelayAccount(Map<String, Object> order) {
|
return !"STORE_CONSUME".equalsIgnoreCase(string(order.get("orderType")));
|
}
|
|
private void insertPaymentTransaction(
|
String paymentNo,
|
Map<String, Object> order,
|
String orderNo,
|
Long talentId,
|
long payAmount,
|
String huifuId,
|
String idempotencyKey,
|
int attemptNo
|
) {
|
jdbc.update("""
|
insert into payment_transaction(payment_no, provider, order_id, order_no, talent_id, merchant_id,
|
provider_huifu_id, amount, status, idempotency_key, attempt_no,
|
funds_confirmation_status)
|
values (?, ?, ?, ?, ?, ?, ?, ?, 'CREATED', ?, ?, ?)
|
""", paymentNo, PROVIDER, number(order.get("id")), orderNo, talentId, order.get("merchantId"), huifuId,
|
payAmount, idempotencyKey, attemptNo, requiresDelayAccount(order) ? "pending" : null);
|
}
|
|
public Map<String, Object> paymentStatus(String orderNo, Long talentId, String paymentNo) {
|
List<Map<String, Object>> orders = rows("""
|
select id, order_status
|
from benefit_order
|
where deleted = 0 and order_no = ? and talent_id = ?
|
limit 1
|
""", orderNo, talentId);
|
if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或无权查看");
|
|
Map<String, Object> order = orders.get(0);
|
if (number(order.get("orderStatus")) == 1) return completedResult(paymentNo == null ? "" : paymentNo);
|
// 回调没到、本地还停在待支付时,通道是唯一知道钱有没有收下的地方。问一次就能把状态收敛回来,
|
// 人才端也就不会一直停在待支付页。「关单中」的凭证也在这里问一次关单结论:它往往早已关掉,
|
// 用户点「刷新支付状态」就该拿到那个结论,而不是等到下一轮对账。
|
reconciliationService.concludeUndecidedCloses(orderNo);
|
PendingPaymentConfirmation confirmation = reconciliationService.confirmPendingPayments(orderNo);
|
if (confirmation.collected()) return completedResult(confirmation.paymentNo());
|
|
String filter = paymentNo == null || paymentNo.isBlank() ? "" : " and payment_no = ?";
|
Object[] args = paymentNo == null || paymentNo.isBlank()
|
? new Object[] { orderNo, talentId }
|
: new Object[] { orderNo, talentId, paymentNo };
|
List<Map<String, Object>> transactions = rows("""
|
select payment_no, status, provider_response_code, provider_response_message, paid_at
|
from payment_transaction
|
where order_no = ? and talent_id = ?
|
""" + filter + " order by id desc limit 1", args);
|
if (transactions.isEmpty()) return Map.of("paymentStatus", "NOT_STARTED");
|
|
Map<String, Object> transaction = transactions.get(0);
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("paymentNo", transaction.get("paymentNo"));
|
result.put("paymentStatus", statusText(string(transaction.get("status"))));
|
result.put("providerCode", transaction.get("providerResponseCode"));
|
result.put("providerMessage", transaction.get("providerResponseMessage"));
|
result.put("paidAt", transaction.get("paidAt"));
|
return result;
|
}
|
|
private void markPrepayCreated(String paymentNo, String responseCode, String responseMessage, String clientPayload) {
|
jdbc.update("""
|
update payment_transaction
|
set status = 'PREPAY_CREATED', provider_response_code = ?, provider_response_message = ?, client_payment_payload = ?
|
where payment_no = ? and status in ('CREATED', 'CREATE_FAILED')
|
""", responseCode, responseMessage, clientPayload, paymentNo);
|
}
|
|
private void markCreateFailed(String paymentNo, String responseCode, String responseMessage) {
|
jdbc.update("""
|
update payment_transaction
|
set status = 'CREATE_FAILED', provider_response_code = ?, provider_response_message = ?
|
where payment_no = ? and status in ('CREATED', 'CREATE_FAILED')
|
""", responseCode, responseMessage, paymentNo);
|
}
|
|
private void markCreateUnknown(String paymentNo, String responseCode, String responseMessage) {
|
jdbc.update("""
|
update payment_transaction
|
set status = 'CREATE_UNKNOWN', provider_response_code = ?, provider_response_message = ?
|
where payment_no = ? and status = 'CREATED'
|
""", responseCode, responseMessage, paymentNo);
|
}
|
|
private Map<String, Object> pendingResult(String paymentNo, String payload) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("paymentNo", paymentNo);
|
result.put("paymentStatus", "PAYMENT_PENDING");
|
result.put("paymentParams", parsePayload(payload));
|
log.info("Mini-program payment response: paymentNo={}, payload={}", paymentNo, logPayload(result));
|
return result;
|
}
|
|
private Map<String, Object> completedResult(String paymentNo) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("paymentNo", paymentNo);
|
result.put("paymentStatus", "PAID");
|
return result;
|
}
|
|
private Map<String, Object> processingResult(String paymentNo) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("paymentNo", paymentNo);
|
result.put("paymentStatus", "PAYMENT_PROCESSING");
|
return result;
|
}
|
|
private String statusText(String status) {
|
return switch (status) {
|
case "SUCCEEDED" -> "PAID";
|
case "CLOSED", "CANCEL_UNCONFIRMED" -> "CANCELLED";
|
case "CREATE_FAILED" -> "PAYMENT_CREATE_FAILED";
|
case "CREATED", "PREPAY_CREATED" -> "PAYMENT_PENDING";
|
// 关单中与创建结果未定都是「还在确认」:客户端渲染成可等待的文案,而不是一个报错。
|
case "CLOSING", "CREATE_UNKNOWN" -> "PAYMENT_PROCESSING";
|
default -> "UNKNOWN";
|
};
|
}
|
|
private Object clientPayload(Map<String, Object> response) {
|
Map<String, Object> data = businessData(response);
|
Object payInfo = data.get("pay_info");
|
if (payInfo == null) payInfo = data.get("payInfo");
|
if (payInfo instanceof String text && !text.isBlank()) {
|
try {
|
return JSON.parse(text);
|
} catch (Exception ignored) {
|
return Map.of("payInfo", text);
|
}
|
}
|
return payInfo == null ? data : payInfo;
|
}
|
|
private boolean providerAccepted(Map<String, Object> response) {
|
Map<String, Object> data = businessData(response);
|
String status = string(data.get("trans_stat"));
|
if (status.isBlank()) status = string(data.get("transStat"));
|
Object payInfo = data.get("pay_info");
|
if (payInfo == null) payInfo = data.get("payInfo");
|
return payInfo != null && !String.valueOf(payInfo).isBlank()
|
&& ("P".equalsIgnoreCase(status) || "S".equalsIgnoreCase(status));
|
}
|
|
private String responseCode(Map<String, Object> response) {
|
Map<String, Object> data = businessData(response);
|
Object value = data.get("resp_code");
|
if (value == null) value = data.get("code");
|
if (value == null) value = data.get("sub_resp_code");
|
return value == null ? "" : String.valueOf(value);
|
}
|
|
private String responseText(Map<String, Object> response, String fallback) {
|
Map<String, Object> data = businessData(response);
|
Object value = data.get("resp_desc");
|
if (value == null) value = data.get("message");
|
if (value == null) value = data.get("sub_resp_desc");
|
return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value);
|
}
|
|
@SuppressWarnings("unchecked")
|
private Map<String, Object> businessData(Map<String, Object> response) {
|
Object data = response.get("data");
|
return data instanceof Map<?, ?> map ? (Map<String, Object>) map : response;
|
}
|
|
private String writeJson(Object value) {
|
try {
|
return JSON.toJSONString(value);
|
} catch (Exception exception) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付参数保存失败");
|
}
|
}
|
|
private Object parsePayload(String payload) {
|
if (payload == null || payload.isBlank()) return Map.of();
|
try {
|
return JSON.parse(payload);
|
} catch (Exception exception) {
|
return Map.of();
|
}
|
}
|
|
private String logPayload(Object value) {
|
try {
|
return JSON.toJSONString(maskPaymentPayload(value));
|
} catch (Exception ignored) {
|
return "{\"log_error\":\"Unable to serialize payment response\"}";
|
}
|
}
|
|
private Object maskPaymentPayload(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, sensitiveLogKey(key) ? maskLogValue(String.valueOf(entry.getValue())) : maskPaymentPayload(entry.getValue()));
|
}
|
return result;
|
}
|
if (value instanceof List<?> source) return source.stream().map(this::maskPaymentPayload).toList();
|
if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
|
try {
|
return maskPaymentPayload(JSON.parse(text));
|
} catch (Exception ignored) {
|
return text;
|
}
|
}
|
return value;
|
}
|
|
private boolean sensitiveLogKey(String key) {
|
String normalized = key == null ? "" : key.toLowerCase();
|
return normalized.contains("sign") || normalized.contains("secret") || normalized.contains("openid") || normalized.contains("authorization");
|
}
|
|
private String maskLogValue(String value) {
|
if (value == null || value.isBlank()) return "";
|
if (value.length() <= 8) return "***";
|
return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
|
}
|
|
private LocalDateTime expiryOrDefault(LocalDateTime expiresAt) {
|
return expiresAt == null ? LocalDateTime.now().plusMinutes(15) : expiresAt;
|
}
|
|
private long number(Object value) {
|
return value instanceof Number number ? number.longValue() : 0;
|
}
|
|
private LocalDateTime localDateTime(Object value) {
|
return value instanceof LocalDateTime time ? time : null;
|
}
|
|
private String string(Object value) {
|
return value == null ? "" : String.valueOf(value).trim();
|
}
|
|
private List<Map<String, Object>> rows(String sql, Object... args) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, args));
|
}
|
|
private record PreparedPayment(
|
String paymentNo,
|
String huifuId,
|
String openId,
|
long amountInCent,
|
String goodsDescription,
|
LocalDateTime expiresAt,
|
boolean callProvider,
|
boolean completed,
|
String clientPayload,
|
boolean processing,
|
boolean delayAccount
|
) {}
|
}
|