package com.talentcard.server.business.payment;
|
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.MoneyUtils;
|
import com.talentcard.server.business.order.OrderFundBoundaryService;
|
import com.talentcard.server.business.order.OrderRefundRequest;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
|
import com.talentcard.server.business.payment.huifu.HuifuRefundCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuRefundGateway;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
@Service
|
public class HuifuRefundService {
|
|
private static final String PROVIDER = "HUIFU";
|
private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
private final JdbcTemplate jdbc;
|
private final TransactionTemplate transactions;
|
private final HuifuRefundGateway gateway;
|
private final HuifuPaymentProperties huifuPaymentProperties;
|
private final OrderFundBoundaryService orderFundBoundary;
|
|
public HuifuRefundService(JdbcTemplate jdbc, TransactionTemplate transactions, HuifuRefundGateway gateway, HuifuPaymentProperties huifuPaymentProperties, OrderFundBoundaryService orderFundBoundary) {
|
this.jdbc = jdbc;
|
this.transactions = transactions;
|
this.gateway = gateway;
|
this.huifuPaymentProperties = huifuPaymentProperties;
|
this.orderFundBoundary = orderFundBoundary;
|
}
|
|
public Map<String, Object> request(String orderNo, OrderRefundRequest request) {
|
PreparedRefund prepared = transactions.execute(status -> prepare(orderNo, request));
|
if (prepared == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Refund initialization failed");
|
|
Map<String, Object> response;
|
try {
|
response = gateway.createRefund(new HuifuRefundCommand(
|
prepared.refundNo(), prepared.huifuId(), prepared.paymentNo(), prepared.originalProviderNo(),
|
prepared.originalRequestDate(), prepared.refundAmount()));
|
} catch (HuifuPaymentGatewayException exception) {
|
if (!exception.isOutcomeUnknown()) {
|
markFailed(prepared.refundNo(), prepared.orderId(), "GATEWAY_REJECTED", exception.getMessage());
|
throw new BusinessException(ErrorCode.CONFLICT, "Refund request was rejected");
|
}
|
return detail(prepared.refundNo());
|
} catch (BusinessException exception) {
|
markFailed(prepared.refundNo(), prepared.orderId(), "LOCAL_ERROR", exception.getMessage());
|
throw exception;
|
} catch (RuntimeException exception) {
|
return detail(prepared.refundNo());
|
}
|
|
ProviderResult result = ProviderResult.from(response);
|
if (result.success()) {
|
markSucceeded(prepared, result);
|
} else if (result.failed()) {
|
markFailed(prepared.refundNo(), prepared.orderId(), result.code(), result.message());
|
throw new BusinessException(ErrorCode.CONFLICT, "Refund request was rejected");
|
} else {
|
updateProviderResult(prepared.refundNo(), result);
|
}
|
return detail(prepared.refundNo());
|
}
|
|
public Map<String, Object> query(String refundNo) {
|
Map<String, Object> refund = detail(refundNo);
|
if (number(refund.get("refundStatus")) != 1) return refund;
|
HuifuRefundCommand command = new HuifuRefundCommand(
|
refundNo,
|
refundHuifuId(refund.get("providerHuifuId")),
|
text(refund.get("providerOriginalReqSeqId")),
|
text(refund.get("providerOriginalNo")),
|
text(refund.get("providerOriginalReqDate")),
|
number(refund.get("refundAmount")));
|
try {
|
ProviderResult result = ProviderResult.from(gateway.queryRefund(command, text(refund.get("providerRefundNo"))));
|
if (result.success()) markSucceeded(new PreparedRefund(refundNo, number(refund.get("orderId")), number(refund.get("refundAmount"))), result);
|
else if (result.failed()) markFailed(refundNo, number(refund.get("orderId")), result.code(), result.message());
|
else updateProviderResult(refundNo, result);
|
} catch (HuifuPaymentGatewayException ignored) {
|
// Keep the refund pending when the provider outcome is unknown.
|
}
|
return detail(refundNo);
|
}
|
|
public Map<String, Object> detail(String refundNo) {
|
List<Map<String, Object>> rows = rows("""
|
select r.id, r.order_id, r.refund_no, r.order_no, r.order_type, r.paid_amount, r.refund_amount,
|
r.refundable_amount, r.refund_status, r.provider, r.provider_refund_no, r.provider_huifu_id,
|
r.provider_original_no, r.provider_original_req_seq_id, r.provider_original_req_date,
|
r.provider_response_code, r.provider_response_message, r.reason, r.operator_name, r.refund_time
|
from order_refund r
|
where r.deleted = 0 and r.refund_no = ?
|
""", refundNo);
|
if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "Refund record not found");
|
return rows.get(0);
|
}
|
|
private PreparedRefund prepare(String orderNo, OrderRefundRequest request) {
|
List<Map<String, Object>> orders = rows("""
|
select o.id, o.order_no, o.talent_id, o.card_id, o.benefit_id, o.merchant_id, o.order_type,
|
o.pay_amount, o.order_status, o.verify_status,
|
p.payment_no, p.provider_transaction_no, p.created_at as payment_created_at,
|
coalesce(p.provider_huifu_id, o.collect_huifu_id) as huifu_id
|
from benefit_order o
|
left join payment_transaction p on p.order_id = o.id and p.provider = ? and p.status = 'SUCCEEDED'
|
where o.deleted = 0 and o.order_no = ?
|
order by p.id desc
|
limit 1 for update
|
""", PROVIDER, orderNo);
|
if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "Order not found");
|
Map<String, Object> order = orders.get(0);
|
// 支付、核销、退款、提现锁定都按订单串行化:锁住订单行之后才判断能不能退款。
|
OrderFundBoundaryService.LockedOrder locked = orderFundBoundary.lock(number(order.get("id")));
|
locked.requireRefundable();
|
if (locked.verifyStatus() == 1 && !"STORE_CONSUME".equals(text(order.get("orderType")))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "Verified orders cannot be refunded");
|
}
|
|
Integer active = jdbc.queryForObject("select count(*) from order_refund where deleted = 0 and order_id = ? and refund_status in (1,2)", Integer.class, order.get("id"));
|
if (active != null && active > 0) throw new BusinessException(ErrorCode.CONFLICT, "Refund has already been submitted for this order");
|
|
long paidAmount = number(order.get("payAmount"));
|
long successfulRefund = refundedAmount(number(order.get("id")));
|
long refundableAmount = Math.max(paidAmount - successfulRefund, 0);
|
long requestedAmount = MoneyUtils.yuanToCent(request.refundAmount());
|
long refundAmount = requestedAmount > 0 ? requestedAmount : refundableAmount;
|
if (refundAmount <= 0 || refundAmount > refundableAmount) throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid refund amount");
|
|
String paymentNo = text(order.get("paymentNo"));
|
String providerNo = text(order.get("providerTransactionNo"));
|
if (paymentNo.isBlank() && providerNo.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "Payment transaction is not ready for refund");
|
String huifuId = refundHuifuId(order.get("huifuId"));
|
String refundNo = BusinessIdGenerator.next("R");
|
String requestDate = requestDate(order.get("paymentCreatedAt"));
|
jdbc.update("""
|
insert into order_refund(refund_no, order_id, order_no, talent_id, card_id, benefit_id, merchant_id,
|
order_type, paid_amount, refund_amount, refundable_amount, refund_status,
|
provider, provider_huifu_id, provider_original_no, provider_original_req_seq_id, provider_original_req_date,
|
reason, operator_name)
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
""",
|
refundNo, order.get("id"), order.get("orderNo"), order.get("talentId"), order.get("cardId"),
|
order.get("benefitId"), order.get("merchantId"), order.get("orderType"), paidAmount, refundAmount,
|
refundableAmount - refundAmount, PROVIDER, huifuId, providerNo, paymentNo, requestDate, request.reason(), request.operatorName());
|
// 退款受理成功即刻生效:在结果确定前订单不再可核销,也不参与提现汇总。
|
orderFundBoundary.markRefundInProgress(number(order.get("id")));
|
return new PreparedRefund(refundNo, number(order.get("id")), huifuId, paymentNo, providerNo, requestDate, refundAmount);
|
}
|
|
private void markSucceeded(PreparedRefund prepared, ProviderResult result) {
|
transactions.executeWithoutResult(status -> {
|
jdbc.update("""
|
update order_refund
|
set refund_status = 2, provider_refund_no = ?, provider_response_code = ?, provider_response_message = ?, refund_time = now()
|
where refund_no = ? and refund_status = 1
|
""", result.providerNo(), result.code(), result.message(), prepared.refundNo());
|
jdbc.update("""
|
update benefit_order o
|
set o.order_status = 2
|
where o.id = ? and o.order_status = 1
|
and o.pay_amount <= (select coalesce(sum(r.refund_amount), 0) from order_refund r where r.order_id = o.id and r.refund_status = 2 and r.deleted = 0)
|
""", prepared.orderId());
|
orderFundBoundary.markRefunded(prepared.orderId());
|
});
|
}
|
|
/** 退款失败没有资金变动,订单回到未退款状态(可再次核销或再次退款)。 */
|
private void markFailed(String refundNo, long orderId, String code, String message) {
|
transactions.executeWithoutResult(status -> {
|
int failed = jdbc.update("update order_refund set refund_status = 3, provider_response_code = ?, provider_response_message = ? where refund_no = ? and refund_status = 1", code, message, refundNo);
|
// 只有本事务真的把这笔退款置为失败,才把订单退回未退款状态;否则会覆盖并发的退款成功。
|
if (failed == 1) orderFundBoundary.markRefundFailed(orderId);
|
});
|
}
|
|
private void updateProviderResult(String refundNo, ProviderResult result) {
|
jdbc.update("update order_refund set provider_refund_no = coalesce(?, provider_refund_no), provider_response_code = ?, provider_response_message = ? where refund_no = ? and refund_status = 1", result.providerNo(), result.code(), result.message(), refundNo);
|
}
|
|
private long refundedAmount(long orderId) {
|
Long total = jdbc.queryForObject("select coalesce(sum(refund_amount), 0) from order_refund where deleted = 0 and refund_status = 2 and order_id = ?", Long.class, orderId);
|
return total == null ? 0 : total;
|
}
|
|
/**
|
* The merchant a refund must be requested from: the sub-merchant fixed on the payment or the
|
* order. Payments made before the collecting sub-merchant was recorded were collected by the
|
* configured platform merchant, so those fall back to it instead of the refund failing.
|
*/
|
private String refundHuifuId(Object storedHuifuId) {
|
String resolved = text(storedHuifuId);
|
return resolved.isBlank() ? text(huifuPaymentProperties.getMerchantId()) : resolved;
|
}
|
|
private String requestDate(Object value) {
|
if (value instanceof LocalDateTime dateTime) return REQUEST_DATE.format(dateTime);
|
if (value instanceof LocalDate date) return date.format(DateTimeFormatter.BASIC_ISO_DATE);
|
return REQUEST_DATE.format(LocalDateTime.now());
|
}
|
|
private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
|
private long number(Object value) { return value instanceof Number number ? number.longValue() : 0; }
|
private String text(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
|
|
private record PreparedRefund(String refundNo, long orderId, String huifuId, String paymentNo, String originalProviderNo, String originalRequestDate, long refundAmount) {
|
private PreparedRefund(String refundNo, long orderId, long refundAmount) { this(refundNo, orderId, "", "", "", "", refundAmount); }
|
}
|
|
private record ProviderResult(boolean success, boolean failed, String code, String message, String providerNo) {
|
private static ProviderResult from(Map<String, Object> response) {
|
Map<String, Object> data = response.get("data") instanceof Map<?, ?> map ? (Map<String, Object>) map : response;
|
String status = textValue(data.get("trans_stat"));
|
String code = textValue(data.get("sub_resp_code"));
|
if (code.isBlank()) code = textValue(data.get("resp_code"));
|
String message = textValue(data.get("sub_resp_desc"));
|
if (message.isBlank()) message = textValue(data.get("resp_desc"));
|
String providerNo = textValue(data.get("hf_seq_id"));
|
boolean codeSuccess = "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code);
|
boolean success = codeSuccess && (status.isBlank() || "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status) || "PAID".equalsIgnoreCase(status));
|
boolean failed = !success && !("P".equalsIgnoreCase(status) || "PROCESSING".equalsIgnoreCase(status) || "PENDING".equalsIgnoreCase(status));
|
return new ProviderResult(success, failed, code, message, providerNo);
|
}
|
private static String textValue(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
|
}
|
}
|