talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java
@@ -19,6 +19,7 @@
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * Second confirmation path for payments whose callback did not arrive or whose outcome was unknown.
@@ -221,6 +222,8 @@
     *       这次尝试停在 {@code CLOSING} 等后台重试,并记一条问题记录给人工。</li>
     *   <li>关单处理中:这次尝试停在新状态 {@code CLOSING}(关单中)——它既不能当已关闭(通道还可能
     *       收钱),也不该是终点,做成可等待、可重试的中间态。</li>
     *   <li>原交易已经终结({@code 23000000}/{@code 10000016}):不是「现在不让关」,而是「它已经
     *       结束了」——那张预支付单不可能再被支付,落成已关闭,不再交回客户端。</li>
     *   <li>其余拒绝(未满一分钟不允许关单、原交易不支持关单、关单请求本身失败):允许取消,但流水记为
     *       {@code CANCEL_UNCONFIRMED} 而不是已关闭——对账任务仍会盯着这笔请求,万一用户后来真的付了,
     *       会留下问题记录让人处理,而不是无声地变成一笔没有订单的悬账。</li>
@@ -267,6 +270,17 @@
            markClosing(transaction, close.code(), close.message());
            return new PendingPaymentConfirmation(PendingPaymentState.UNCONFIRMED, paymentNo);
        }
        if (close.originalTradeAlreadyTerminal()) {
            // 关单回执说的不是「现在不让关」,而是「原交易已经终结」。此时再关只会一直拿到同一句拒绝
            // (旧实现就把它当成了「关不掉=可能还能付」),而一张死掉的预支付单被交回客户端时,微信
            // 收银台会直接报「订单状态异常,支付请求无效」;订单也会挂成一个既付不了又取消不了的僵尸。
            // 这里落成已关闭:取消那条路与关单补偿都是先查过单、通道说过这笔钱没收下才走到关单的,
            // 「放弃支付」那条路则是用户刚关掉收银台——钱真收下了会有异步通知把它结算掉(迟到的成功
            // 记成「取消后才收到钱」,交给悬账退款与人工),不会无声地消失。
            markCancelledTransaction(paymentNo, "CLOSED", close.code(), close.message());
            issues.resolve("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, "system");
            return new PendingPaymentConfirmation(PendingPaymentState.NONE, paymentNo);
        }
        if (close.processing()) {
            issues.issue("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, orderNo, "closed at provider", "close processing", amount,
                    "Provider is still closing the payment");
@@ -281,10 +295,14 @@
     *
     * <p>到上限时通道既没说关掉、也没说钱收下了,而这张预支付单还可能被支付。此时订单不能继续挂着
     * 「待支付」——那样它既付不了(不允许在同一订单上再建一张预支付单)也永远不会被自动取消,变成一笔
     * 僵尸订单。所以这里把订单落成已取消,并把这次尝试留成 {@code CANCEL_UNCONFIRMED}:
     * 订单状态不由自动取消的排除表管理,而是由这次明确的「不再重试」结论结束;
     * 预支付单继续被对账盯着,万一用户后来真的付了,会记成「取消后才收到钱」的问题记录,
     * 僵尸订单:同一件商品再也下不了单。所以这里把订单落成已取消,并把这次尝试留成
     * {@code CANCEL_UNCONFIRMED}:订单状态不由自动取消的排除表管理,而是由这次明确的「不再重试」结论
     * 结束;预支付单继续被对账盯着,万一用户后来真的付了,会记成「取消后才收到钱」的问题记录,
     * 交给悬账自动退款与人工处理,而不是无声地留下没有出口的悬账。
     *
     * <p>这个终止条件按「这次尝试」而不是按它的当前状态来下:流水可能从没经过 {@code CLOSING}
     * (关单第一次就被拒,直接落在 {@code CANCEL_UNCONFIRMED}),只认 {@code CLOSING} 的话这类订单
     * 永远结束不了。
     */
    private void giveUpClosing(Map<String, Object> transaction) {
        String paymentNo = text(transaction.get("paymentNo"));
@@ -297,8 +315,12 @@
        issues.issue("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, orderNo, "closed at provider", "close attempts exhausted", amount,
                "Payment could not be closed after " + MAX_CLOSE_ATTEMPTS + " attempts; the prepay stays watchable");
        jdbc.update("""
                update benefit_order set order_status = 2
                where order_no = ? and deleted = 0 and order_status = 0
                update benefit_order o set order_status = 2
                where o.order_no = ? and o.deleted = 0 and o.order_status = 0
                  and not exists (
                    select 1 from payment_transaction p
                    where p.order_id = o.id and p.status = 'SUCCEEDED'
                  )
                """, orderNo);
    }
@@ -334,8 +356,11 @@
                """, status, code, message, paymentNo);
    }
    /** 通道对关单请求的答复。只有「关掉了」和「原交易已成功」是明确结论,其余一律当作没确认下来。 */
    /** 通道对关单请求的答复。只要还没说「关掉了/原交易已成功/原交易已终结」,就当作没确认下来。 */
    private record CloseResult(String code, String message, String status, String originalStatus) {
        /** 汇付「原订单已为终态」的两个返回码:原交易已终结(已关单或已完成),无法再关单。 */
        private static final Set<String> TERMINAL_CODES = Set.of("23000000", "10000016");
        private static CloseResult from(Map<String, Object> response) {
            Map<String, Object> data = response == null ? Map.of()
                    : (response.get("data") instanceof Map<?, ?> map ? (Map<String, Object>) map : response);
@@ -350,6 +375,16 @@
        boolean originalTradeSucceeded() {
            return "S".equalsIgnoreCase(originalStatus);
        }
        /**
         * 通道拒绝关单,理由是原交易已经终结。
         *
         * <p>这与「未满一分钟不允许关单」这类拒绝不是一回事:后者说的是「现在不让关」,预支付单还活着;
         * 这一句说的是「它已经结束了」,那张预支付单不可能再被支付。
         */
        boolean originalTradeAlreadyTerminal() {
            return TERMINAL_CODES.contains(code);
        }
        boolean processing() {
@@ -421,9 +456,10 @@
    private void retryClose(Map<String, Object> transaction) {
        String paymentNo = text(transaction.get("paymentNo"));
        if (number(transaction.get("closeAttempts")) >= MAX_CLOSE_ATTEMPTS) {
            // 到上限:先给还停在「关单中」的尝试一个终止条件(落成已取消并结束订单),之后只查不关。
            if (CLOSING_STATUS.equals(text(transaction.get("status")))) giveUpClosing(transaction);
            else watchOnly(transaction);
            // 到上限:关单已经问不出更多结论,再打通道只是重复。先查一次(钱真的来了要先落成已支付,
            // 那一支会把订单翻成已支付,下面的收尾就不会再取消它),再给这次尝试一个终止条件。
            watchOnly(transaction);
            giveUpClosing(transaction);
            return;
        }
        issues.resolve("PAYMENT_CLOSE_UNCONFIRMED", paymentNo, "system");