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<String, Object> 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<String, Object> 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<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, 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);
|
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<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);
|
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<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)
|
values (?, ?, ?, ?, ?, ?, ?, ?, 'CREATED', ?, ?)
|
""", paymentNo, PROVIDER, number(order.get("id")), orderNo, talentId, order.get("merchantId"), huifuId, payAmount, idempotencyKey, attemptNo);
|
}
|
|
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);
|
|
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" -> "CANCELLED";
|
case "CREATE_FAILED" -> "PAYMENT_CREATE_FAILED";
|
case "CREATED", "PREPAY_CREATED" -> "PAYMENT_PENDING";
|
case "CREATE_UNKNOWN" -> "PAYMENT_CONFIRMING";
|
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
|
) {}
|
}
|