yeg
2026-09-14 5364ba7487eb7abe0d4adc67f3393f511ed123a7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package com.talentcard.server.business.payment;
 
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
 
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway;
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmCommand;
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmation;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
 
/**
 * 延迟交易的资金确认。
 *
 * <p>商户开通延迟入账后,用 {@code delay_acct_flag=Y} 下的支付会把钱留在通道的「待确认」状态。业务规则是
 * 「核销之后商户才能拿到钱」,所以核销成功时才调用交易确认接口,把这笔钱分配给收款子商户。
 *
 * <p>状态记在 {@code payment_transaction.funds_confirmation_status}:{@code NULL} 表示这笔支付没有延迟
 * (支付即核销的到店消费),{@code pending} 表示钱还在通道冻着,{@code confirmed} 表示已经分配完成。
 * 自动提现只认 {@code NULL} 和 {@code confirmed},因此「忘了确认」不会变成「提前把钱提走」。
 */
@Service
public class PaymentFundConfirmationService {
 
    /**
     * 资金没有走延迟入账,不需要确认。
     *
     * <p>这些常量是「返回给调用方的结论」,与 {@code payment_transaction.funds_confirmation_status} 里存的
     * 状态不同名:库里存小写({@code pending}/{@code confirming}/{@code confirmed}),所以比较一律走
     * {@link #isConfirmed(String)},不要直接拿常量去比字段值。
     */
    public static final String NOT_DELAYED = "NOT_DELAYED";
    /** 资金已分配给收款商户。 */
    public static final String CONFIRMED = "CONFIRMED";
    /** 支付本身还没有成功,稍后再确认。 */
    public static final String NOT_SETTLED = "NOT_SETTLED";
    /** 已经有一笔确认请求在途,交给定时任务收敛。 */
    public static final String PROCESSING = "PROCESSING";
    /** 通道明确拒绝,需要人工处理。 */
    public static final String FAILED = "FAILED";
    /** 找不到对应的支付记录。 */
    public static final String UNKNOWN = "UNKNOWN";
 
    private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final Logger log = LoggerFactory.getLogger(PaymentFundConfirmationService.class);
 
    private final JdbcTemplate jdbc;
    private final TransactionTemplate transactions;
    private final HuifuDelayAccountGateway gateway;
 
    public PaymentFundConfirmationService(JdbcTemplate jdbc, TransactionTemplate transactions,
                                          HuifuDelayAccountGateway gateway) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
    }
 
    /**
     * 为某个订单确认资金。订单必须已经有成功的支付记录。
     *
     * <p>幂等:资金已经确认、或这笔支付本来就没有延迟时直接返回,不会重复向通道发确认请求。
     */
    public String confirmForOrder(long orderId) {
        List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
                select p.id, p.payment_no, p.status, p.amount, p.provider_huifu_id, p.provider_transaction_no,
                       p.funds_confirmation_status, p.created_at,
                       o.collect_huifu_id
                from payment_transaction p
                join benefit_order o on o.id = p.order_id
                where p.order_id = ?
                order by p.id desc
                limit 1
                """, orderId));
        if (rows.isEmpty()) return UNKNOWN;
 
        Map<String, Object> payment = rows.get(0);
        String confirmationStatus = text(payment.get("fundsConfirmationStatus"));
        if (isConfirmed(confirmationStatus)) return CONFIRMED;
        if (confirmationStatus.isBlank()) return NOT_DELAYED;
        if (!"SUCCEEDED".equals(text(payment.get("status")))) return NOT_SETTLED;
 
        String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
        if (huifuId.isBlank()) {
            log.warn("Payment {} has no collecting sub-merchant, fund confirmation is left pending",
                    payment.get("paymentNo"));
            return PROCESSING;
        }
 
        long paymentId = number(payment.get("id"));
        if (!claim(paymentId)) {
            // 另一个请求已经在确认这笔资金,或者它刚被确认为终态。
            return isConfirmed(currentStatus(paymentId)) ? CONFIRMED : PROCESSING;
        }
 
        String confirmNo = BusinessIdGenerator.next("DC");
        HuifuDelayConfirmation result;
        try {
            result = gateway.confirm(new HuifuDelayConfirmCommand(
                    confirmNo,
                    huifuId,
                    text(payment.get("paymentNo")),
                    text(payment.get("providerTransactionNo")),
                    REQUEST_DATE.format(localDate(payment.get("createdAt"))),
                    "核销后确认资金"));
        } catch (HuifuPaymentGatewayException exception) {
            // 请求可能已经被通道受理:退回 pending 交给后续重试,绝不按失败处理,否则钱会永远冻在通道。
            log.warn("Fund confirmation for payment {} has an unknown result: {}",
                    payment.get("paymentNo"), exception.getMessage());
            recordAttempt(paymentId, confirmNo, "", exception.getMessage());
            return PROCESSING;
        } catch (RuntimeException exception) {
            log.warn("Fund confirmation for payment {} failed locally: {}",
                    payment.get("paymentNo"), exception.getMessage());
            return PROCESSING;
        }
 
        return applyResult(paymentId, confirmNo, result);
    }
 
    /** 认领这笔资金确认:只有仍为 pending 的那一次能拿到,避免重复向通道发确认。 */
    private boolean claim(long paymentId) {
        int claimed = jdbc.update("""
                update payment_transaction
                set funds_confirmation_status = 'confirming', funds_confirm_request_date = ?
                where id = ? and funds_confirmation_status = 'pending'
                """, REQUEST_DATE.format(LocalDate.now()), paymentId);
        return claimed == 1;
    }
 
    /** 数据库里的确认状态是小写,比较必须大小写无关。 */
    private static boolean isConfirmed(String status) {
        return status != null && "confirmed".equalsIgnoreCase(status.trim());
    }
 
    private String currentStatus(long paymentId) {
        List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList(
                "select funds_confirmation_status from payment_transaction where id = ?", paymentId));
        return rows.isEmpty() ? "" : text(rows.get(0).get("fundsConfirmationStatus"));
    }
 
    private String applyResult(long paymentId, String confirmNo, HuifuDelayConfirmation result) {
        switch (result.outcome()) {
            case SUCCEEDED -> {
                transactions.executeWithoutResult(status -> jdbc.update("""
                        update payment_transaction
                        set funds_confirmation_status = 'confirmed', funds_confirm_no = ?,
                            funds_confirmed_amount = ?, funds_confirm_code = ?, funds_confirm_message = ?,
                            funds_confirmed_at = now()
                        where id = ?
                        """, confirmNo, result.confirmedAmountInCent(), result.code(), result.describe(), paymentId));
                log.info("Payment fund confirmation {} succeeded: confirmed={}", confirmNo, result.confirmedAmount());
                return CONFIRMED;
            }
            case FAILED -> {
                // 通道明确拒绝:通常是参数或商户配置问题,保持可重试状态并留下证据。
                transactions.executeWithoutResult(status -> jdbc.update("""
                        update payment_transaction
                        set funds_confirmation_status = 'pending', funds_confirm_no = ?,
                            funds_confirm_code = ?, funds_confirm_message = ?
                        where id = ?
                        """, confirmNo, result.code(), result.describe(), paymentId));
                log.warn("Payment fund confirmation {} was rejected: {} ({})",
                        confirmNo, result.describe(), result.code());
                return FAILED;
            }
            default -> {
                transactions.executeWithoutResult(status -> jdbc.update("""
                        update payment_transaction
                        set funds_confirmation_status = 'pending', funds_confirm_no = ?,
                            funds_confirm_code = ?, funds_confirm_message = ?
                        where id = ?
                        """, confirmNo, result.code(), result.describe(), paymentId));
                return PROCESSING;
            }
        }
    }
 
    /** 只记录一次尝试的请求号与描述,不改状态。 */
    private void recordAttempt(long paymentId, String confirmNo, String code, String message) {
        jdbc.update("""
                update payment_transaction
                set funds_confirm_no = ?, funds_confirm_code = ?, funds_confirm_message = ?
                where id = ?
                """, confirmNo, code, message == null ? "" : message, paymentId);
    }
 
    private long number(Object value) {
        return value instanceof Number number ? number.longValue() : 0L;
    }
 
    private LocalDate localDate(Object value) {
        if (value instanceof java.sql.Timestamp timestamp) return timestamp.toLocalDateTime().toLocalDate();
        if (value instanceof java.time.LocalDateTime dateTime) return dateTime.toLocalDate();
        return LocalDate.now();
    }
 
    private String text(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
 
    private String firstNonBlank(String first, String second) {
        return first == null || first.isBlank() ? (second == null ? "" : second) : first;
    }
}