yeg
2026-09-18 0df97e3090a94493c1d8d9cd2909916d343c96e6
talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuRefundService.java
@@ -52,6 +52,8 @@
    public Map<String, Object> request(String orderNo, OrderRefundRequest request) {
        PreparedRefund prepared = transactions.execute(status -> prepare(orderNo, request, false));
        if (prepared == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Refund initialization failed");
        // 本地结清的退款在 prepare 的事务里就已经落定,没有通道答复要等(也没有通道可以问)。
        if (prepared.settledLocally()) return detail(prepared.refundNo());
        return submit(prepared);
    }
@@ -214,6 +216,15 @@
    }
    /**
     * 无实付金额订单的回执:它不是通道说的,是本地结清时自己写的。
     *
     * <p>写进 {@code provider_response_*} 是为了让这条记录自解释——事后看到「退款成功、金额 0、通道退款号为空」
     * 时,能直接从这两列读出「它从来没有出过本地」,不必再去通道上找一笔不存在的退款。
     */
    private static final ProviderResult LOCAL_REFUND_RESULT = new ProviderResult(
            true, false, "LOCAL_NO_PAYABLE_AMOUNT", "订单无实付金额(由权益免费额度覆盖),本地退款,无需通道退款", "");
    /**
     * 准备一笔退款:锁定订单行、确认这笔退款合法、算出可退金额、落一条退款记录。
     *
     * <p>「支付键可见性」不是只看已成功流水:本地已关闭、但通道确认收过钱的那一类悬账
@@ -260,14 +271,19 @@
        long refundableAmount = Math.max(paidAmount - successfulRefund - pendingRefund, 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");
        // 没有实付金额的订单(整单被权益免费额度覆盖、下单即 order_status=1 且从来没有支付尝试)里没有钱,
        // 通道那边也没有原交易可指。它仍然必须能退——要还回去的是那一次被占用的免费额度——所以它的出路是
        // 本地结清,而不是向通道发起退款:发起也只会得到「金额非法」,用户从此卡在待使用里出不去。
        // 悬账退款不在此列:悬账退的是通道真收过的钱,必须走通道。
        boolean settledLocally = !stranded && paidAmount <= 0 && refundAmount == 0;
        if (!settledLocally && (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"));
        if (!settledLocally && paymentNo.isBlank() && providerNo.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "Payment transaction is not ready for refund");
        String huifuId = settledLocally ? null : refundHuifuId(order.get("huifuId"));
        String refundNo = BusinessIdGenerator.next("R");
        String requestDate = requestDate(order.get("paymentCreatedAt"));
        String requestDate = settledLocally ? null : 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,
@@ -277,10 +293,17 @@
                """,
                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());
                refundableAmount - refundAmount, settledLocally ? null : 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);
        PreparedRefund result = new PreparedRefund(refundNo, number(order.get("id")), huifuId, paymentNo, providerNo, requestDate, refundAmount, settledLocally);
        if (settledLocally) {
            // 与 prepare 同一事务里当场结清,不把订单留在「退款中」:那个状态的含义是等通道的结论,而这
            // 单没有通道那一侧,留在这里就永远等不到(退款对账只扫 provider = HUIFU 的在途行)。
            markSucceeded(result, LOCAL_REFUND_RESULT);
        }
        return result;
    }
    private void markSucceeded(PreparedRefund prepared, ProviderResult result) {
@@ -347,8 +370,8 @@
    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 PreparedRefund(String refundNo, long orderId, String huifuId, String paymentNo, String originalProviderNo, String originalRequestDate, long refundAmount, boolean settledLocally) {
        private PreparedRefund(String refundNo, long orderId, long refundAmount) { this(refundNo, orderId, "", "", "", "", refundAmount, false); }
    }
    private record ProviderResult(boolean success, boolean failed, String code, String message, String providerNo) {