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.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; public HuifuPaymentService( JdbcTemplate jdbc, TransactionTemplate transactions, HuifuMiniProgramPaymentGateway gateway ) { this.jdbc = jdbc; this.transactions = transactions; this.gateway = gateway; } public Map beginMiniProgramPayment(String orderNo, Long talentId, int priceVersion, String idempotencyKey) { 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 response; try { response = gateway.createMiniProgramPayment(new HuifuMiniProgramPaymentCommand( prepared.paymentNo(), prepared.huifuId(), prepared.openId(), prepared.amountInCent(), prepared.goodsDescription(), prepared.expiresAt())); } 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); } 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> 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, 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 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); 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); } // 收款子商户在下单时已固定,支付请求只使用订单上的斗拱商户号。 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> 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 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); if ("PREPAY_CREATED".equals(status)) { return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false); } if ("CREATED".equals(status) || "CREATE_UNKNOWN".equals(status)) { return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true); } 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); } 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); } private void insertPaymentTransaction( String paymentNo, Map 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) values (?, ?, ?, ?, ?, ?, ?, ?, 'CREATED', ?, ?) """, paymentNo, PROVIDER, number(order.get("id")), orderNo, talentId, order.get("merchantId"), huifuId, payAmount, idempotencyKey, attemptNo); } public Map paymentStatus(String orderNo, Long talentId, String paymentNo) { List> 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 order = orders.get(0); if (number(order.get("orderStatus")) == 1) return completedResult(paymentNo == null ? "" : 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> 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 transaction = transactions.get(0); Map 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 pendingResult(String paymentNo, String payload) { Map 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 completedResult(String paymentNo) { Map result = new LinkedHashMap<>(); result.put("paymentNo", paymentNo); result.put("paymentStatus", "PAID"); return result; } private Map processingResult(String paymentNo) { Map 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" -> "CANCELLED"; case "CREATE_FAILED" -> "PAYMENT_CREATE_FAILED"; case "CREATED", "PREPAY_CREATED" -> "PAYMENT_PENDING"; case "CREATE_UNKNOWN" -> "PAYMENT_CONFIRMING"; default -> "UNKNOWN"; }; } private Object clientPayload(Map response) { Map 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 response) { Map 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 response) { Map 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 response, String fallback) { Map 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 businessData(Map response) { Object data = response.get("data"); return data instanceof Map map ? (Map) 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 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> 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 ) {} }