yeg
2026-09-28 85bfb9351803d97a33329d507f30a2f78bf1fc75
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
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.HuifuDelayConfirmRefundCommand;
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayConfirmation;
import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.HuifuDelayRevert;
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.TransactionDefinition;
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},因此「忘了确认」不会变成「提前把钱提走」。
 *
 * <p>撤销核销走反方向({@link #revertForOrder}):调用交易确认退款把已确认的钱退回延迟户,成功后状态
 * 退回 {@code pending},商户重新核销时会再确认一次。回退期间用 {@code reverting} 占位。
 */
@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";
    /**
     * 订单此刻不再是「已核销」,不能把钱放给商户。
     *
     * <p>撤销核销会把订单翻回待核销,此时钱必须留在延迟户:确认在放款前复查这个前提,
     * 避免「订单已回到待核销、钱却已经打给商户」。
     */
    public static final String NOT_VERIFIED = "NOT_VERIFIED";
 
    /** 钱已退回(或本来就在)通道延迟户,撤销核销可以继续。 */
    public static final String REVERTED = "REVERTED";
    /** 这笔资金不需要回退:没有支付记录、支付没有走延迟入账,或钱本来就在延迟户。 */
    public static final String REVERT_NOT_NEEDED = "REVERT_NOT_NEEDED";
    /** 通道明确拒绝回退(例如资金已结算到银行卡、基本户余额不足),钱没有动。 */
    public static final String REVERT_REJECTED = "REVERT_REJECTED";
    /** 回退结果未定或仍在处理中:钱的位置不确定,撤销核销必须停在「已核销」。 */
    public static final String REVERT_IN_FLIGHT = "REVERT_IN_FLIGHT";
    /**
     * 返回码在这条路径上语义未定:通道给了话,但我们不知道它是什么意思,需要人工核对。
     *
     * <p>和 {@link #REVERT_IN_FLIGHT} 分开是因为给商户的话不同:在途可以说「稍后重试」,
     * 语义未定只能请他联系平台——它也可能是「已结算」,让商户反复重试是没有意义的。
     */
    public static final String REVERT_NEEDS_REVIEW = "REVERT_NEEDS_REVIEW";
    /** 回退请求根本没发出去(本地配置或 SDK 出错):钱没动,可以重试。 */
    public static final String REVERT_NOT_SENT = "REVERT_NOT_SENT";
    /** 找不到支付记录,或缺少定位原确认请求的键。 */
    public static final String REVERT_UNKNOWN = "REVERT_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;
 
    /**
     * 资金侧的写入用独立事务提交。
     *
     * <p>撤销核销会在自己的事务里调用本服务,而通道调用不是事务性的:认领和结果如果跟着外层事务一起
     * 回滚,通道那边已经退回延迟户的钱在本地就再也看不到了——状态还写着 {@code confirmed},钱其实已经
     * 不在商户基本户。用独立事务提交,判断依据就永远留在库里。
     */
    private final TransactionTemplate moneyTransactions;
 
    public PaymentFundConfirmationService(JdbcTemplate jdbc, TransactionTemplate transactions,
                                          HuifuDelayAccountGateway gateway) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
        // 注入的 TransactionTemplate 是容器里的单例,支付、退款和自动提现共用它,不能改它的传播行为,
        // 否则那些服务会跟着变成 REQUIRES_NEW、外层事务被静默挂起。这里自己新建一个。
        this.moneyTransactions = new TransactionTemplate(transactions.getTransactionManager());
        this.moneyTransactions.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }
 
    /**
     * 为某个订单确认资金。订单必须已经有成功的支付记录。
     *
     * <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"));
        ClaimOutcome claimOutcome = transactions.execute(status -> claimConfirmation(orderId, paymentId));
        if (claimOutcome == ClaimOutcome.ORDER_NOT_VERIFIED) return NOT_VERIFIED;
        if (claimOutcome != ClaimOutcome.CLAIMED) {
            // 另一个请求已经在确认这笔资金,或者它刚被确认为终态。
            if (isConfirmed(currentStatus(paymentId))) return CONFIRMED;
            // 当天认领出来的「确认中」不能重发确认(claim 只在次日再认领),但「那次确认到底怎么样了」
            // 可以查:确认查询接口从写下那天起就是死代码,这里把它接上,让同一天的确认中也能收敛,
            // 而不是整整等一天。
            String queried = queryInFlightConfirmation(paymentId);
            return queried != null ? queried : 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);
    }
 
    /**
     * 查一次「确认中」那笔确认的结果。
     *
     * <p>认领规则刻意不允许当天重发确认(避免重复分账),于是当天认领出来的确认中只能等到次日才有人再看
     * ——这期间商户的钱明明已经有了结论却没人知道。确认查询接口({@code delaytrans.confirmquery})
     * 从写下那天起就是死代码,这里把它接上:查得到就按结果落地(成功 → confirmed,失败/在途 → pending,
     * 下次还能重发),查不到就保持原样交给后续轮次。
     *
     * @return 应用了查询结果后的结论;拿不到定位键或查询失败时返回 {@code null},表示「没查出来」。
     */
    private String queryInFlightConfirmation(long paymentId) {
        List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
                select p.funds_confirm_no, p.provider_huifu_id, o.collect_huifu_id
                from payment_transaction p
                join benefit_order o on o.id = p.order_id
                where p.id = ?
                limit 1
                """, paymentId));
        if (rows.isEmpty()) return null;
        Map<String, Object> payment = rows.get(0);
        String confirmNo = text(payment.get("fundsConfirmNo"));
        String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
        if (confirmNo.isBlank() || huifuId.isBlank()) return null;
        try {
            return applyResult(paymentId, confirmNo, gateway.queryConfirm(confirmNo, huifuId));
        } catch (HuifuPaymentGatewayException exception) {
            log.warn("Fund confirmation query for {} has no usable answer: {}", confirmNo, exception.getMessage());
            return null;
        } catch (RuntimeException exception) {
            log.warn("Fund confirmation query for {} failed locally: {}", confirmNo, exception.getMessage());
            return null;
        }
    }
 
    /**
     * 重发一次交易确认退款(撤销核销的回退)。
     *
     * <p>回退没有查询接口,而补偿任务原先只扫「待确认/确认中」——停在 {@code reverting} 的记录永远
     * 没人再看,撤销核销就会被永久卡住。回退以 {@code funds_revert_no} 为幂等键,通道对同一个请求号
     * 只会执行一次并回答它当前的状态,所以**幂等的重发就是它的查询对等物**:明确成功/失败就落地,
     * 仍然在途就继续停在 reverting 等下一轮。
     *
     * @return 与 {@link #revertForOrder(long)} 同一套结论常量。
     */
    public String retryRevertForOrder(long orderId) {
        List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
                select p.id, p.payment_no, p.funds_confirmation_status, p.funds_revert_no,
                       p.funds_revert_request_date, p.funds_confirmed_amount, p.funds_confirm_no,
                       p.funds_confirm_request_date, p.provider_huifu_id,
                       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 REVERT_UNKNOWN;
        Map<String, Object> payment = rows.get(0);
        if (!statusIs(text(payment.get("fundsConfirmationStatus")), "reverting")) return REVERT_NOT_NEEDED;
        String revertNo = text(payment.get("fundsRevertNo"));
        String requestDate = text(payment.get("fundsRevertRequestDate"));
        String confirmNo = text(payment.get("fundsConfirmNo"));
        String confirmRequestDate = text(payment.get("fundsConfirmRequestDate"));
        String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
        if (revertNo.isBlank() || requestDate.isBlank() || confirmNo.isBlank() || confirmRequestDate.isBlank() || huifuId.isBlank()) {
            // 缺定位键就重发不了,钱的位置仍然未定:留在 reverting 等人工,绝不猜。
            log.warn("Payment {} has an in-flight revert with no usable reference, it needs a human", payment.get("paymentNo"));
            return REVERT_UNKNOWN;
        }
        long paymentId = number(payment.get("id"));
        HuifuDelayRevert result;
        try {
            result = gateway.refund(new HuifuDelayConfirmRefundCommand(
                    revertNo, huifuId, confirmNo, confirmRequestDate, "撤销核销退回延迟户(补偿重发)"));
        } catch (HuifuPaymentGatewayException exception) {
            recordRevertAttempt(paymentId, revertNo, "", exception.getMessage());
            return REVERT_IN_FLIGHT;
        } catch (RuntimeException exception) {
            log.warn("Fund revert retry for payment {} failed before reaching the provider: {}",
                    payment.get("paymentNo"), exception.getMessage());
            return REVERT_IN_FLIGHT;
        }
        return applyRevertResult(paymentId, revertNo, result, payment);
    }
 
    /**
     * 撤销核销:把这笔已经确认给商户的资金退回通道延迟户。
     *
     * <p>调用方只有在拿到 {@link #REVERTED} 或 {@link #REVERT_NOT_NEEDED} 时才可以撤销核销。其余结果都
     * 表示钱的位置不确定、或仍在商户手里:此时翻单会造成「订单显示待核销、钱已经结算给商户」。
     *
     * <p>方法自身不开事务,认领与落结果各走独立事务(见 {@link #moneyTransactions}):通道调用不是
     * 事务性的,让这些写入跟着外层事务回滚,会把通道的真实结果丢掉。
     */
    public String revertForOrder(long orderId) {
        List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList("""
                select p.id, p.payment_no, p.funds_confirmation_status, p.funds_confirm_no,
                       p.funds_confirm_request_date, p.funds_confirmed_amount, p.provider_huifu_id,
                       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 REVERT_NOT_NEEDED;
 
        Map<String, Object> payment = rows.get(0);
        String fundsStatus = text(payment.get("fundsConfirmationStatus"));
        // 没有走延迟入账(支付即核销的到店消费),或钱本来就在延迟户:同样不需要回退。
        if (fundsStatus.isBlank() || statusIs(fundsStatus, "pending")) return REVERT_NOT_NEEDED;
        // 回退在途或确认在途:钱在哪一边未定,等它收敛后再撤销。
        if (statusIs(fundsStatus, "reverting") || statusIs(fundsStatus, "confirming")) return REVERT_IN_FLIGHT;
        if (!statusIs(fundsStatus, "confirmed")) return REVERT_UNKNOWN;
 
        String confirmNo = text(payment.get("fundsConfirmNo"));
        String confirmRequestDate = text(payment.get("fundsConfirmRequestDate"));
        if (confirmNo.isBlank() || confirmRequestDate.isBlank()) {
            // 没有原确认的定位键就撤销不了确认;钱确实在商户手里,不能翻单,只能人工处理。
            log.warn("Payment {} is confirmed but has no confirmation reference, the revert needs a human",
                    payment.get("paymentNo"));
            return REVERT_UNKNOWN;
        }
        String huifuId = firstNonBlank(text(payment.get("providerHuifuId")), text(payment.get("collectHuifuId")));
        if (huifuId.isBlank()) {
            log.warn("Payment {} has no collecting sub-merchant, the revert needs a human", payment.get("paymentNo"));
            return REVERT_UNKNOWN;
        }
 
        long paymentId = number(payment.get("id"));
        String revertNo = BusinessIdGenerator.next("DR");
        String requestDate = REQUEST_DATE.format(LocalDate.now());
        if (!claimRevert(paymentId, revertNo, requestDate)) {
            // 另一个请求已经在回退这笔钱。
            return REVERT_IN_FLIGHT;
        }
 
        HuifuDelayRevert result;
        try {
            result = gateway.refund(new HuifuDelayConfirmRefundCommand(
                    revertNo, huifuId, confirmNo, confirmRequestDate, "撤销核销退回延迟户"));
        } catch (HuifuPaymentGatewayException exception) {
            // 请求可能已经被通道受理:停在 reverting 交给人工核对,绝不按失败处理,也绝不放行撤销核销。
            log.warn("Fund revert for payment {} has an unknown result: {}",
                    payment.get("paymentNo"), exception.getMessage());
            recordRevertAttempt(paymentId, revertNo, "", exception.getMessage());
            return REVERT_IN_FLIGHT;
        } catch (RuntimeException exception) {
            // 通道根本没被调到(配置不完整、SDK 初始化失败之类):钱没动,可以重试。不能说成「已结算」,
            // 商户照着这句话去找平台就白跑了。
            log.warn("Fund revert for payment {} failed before reaching the provider: {}",
                    payment.get("paymentNo"), exception.getMessage());
            abandonRevertClaim(paymentId, exception.getMessage());
            return REVERT_NOT_SENT;
        }
 
        return applyRevertResult(paymentId, revertNo, result, payment);
    }
 
    /**
     * 认领这笔资金确认:只有仍为 pending 的那一次能拿到,避免重复向通道发确认。
     *
     * <p>同时收掉「认领之后进程就没了」留下的孤儿:状态停在 {@code confirming}、认领日期又不是今天的
     * 那些请求,说明那次确认没走到落结果就中断了(部署、崩溃、超时被杀)。补偿任务只挑得到这种行而
     * 不会重发,没有这一步这笔钱就永远冻在延迟户里。重新确认是幂等的——通道会按「已无未确认金额」
     * 返回,{@link HuifuDelayConfirmation} 把那个码当成功,所以隔天补发一次是安全的。
     *
     * <p>认领日期为空的行不动:那说明它不是我们认领出来的(手工数据),宁可留着给人工看。
     */
    private boolean claim(long paymentId) {
        String today = REQUEST_DATE.format(LocalDate.now());
        int claimed = jdbc.update("""
                update payment_transaction
                set funds_confirmation_status = 'confirming', funds_confirm_request_date = ?
                where id = ?
                  and (funds_confirmation_status = 'pending'
                       or (funds_confirmation_status = 'confirming'
                           and funds_confirm_request_date is not null
                           and funds_confirm_request_date < ?))
                """, today, paymentId, today);
        return claimed == 1;
    }
 
    /**
     * 在订单行锁里复查「订单此刻仍然已核销」,再认领资金确认。
     *
     * <p>撤销核销锁的是同一行,因此这个判断在整个认领期间是稳定的:撤销核销提交在前,这里会看到
     * {@code verify_status} 已经为 0 而拒绝认领;认领在前,撤销核销会看到 {@code confirming} 而拒绝翻单。
     * 两个方向都不会出现「订单回到待核销、钱却打给了商户」。
     */
    private ClaimOutcome claimConfirmation(long orderId, long paymentId) {
        List<Map<String, Object>> orders = DatabaseRows.normalize(jdbc.queryForList("""
                select verify_status, order_status, refund_status
                from benefit_order
                where id = ? and deleted = 0
                limit 1 for update
                """, orderId));
        if (orders.isEmpty()) return ClaimOutcome.ORDER_NOT_VERIFIED;
        Map<String, Object> order = orders.get(0);
        if (number(order.get("verifyStatus")) != 1 || number(order.get("orderStatus")) != 1
                || number(order.get("refundStatus")) != 0) {
            return ClaimOutcome.ORDER_NOT_VERIFIED;
        }
        return claim(paymentId) ? ClaimOutcome.CLAIMED : ClaimOutcome.NOT_CLAIMED;
    }
 
    private enum ClaimOutcome { CLAIMED, NOT_CLAIMED, ORDER_NOT_VERIFIED }
 
    /** 认领这笔资金回退:只有仍为 confirmed 的那一次能拿到,避免重复向通道发回退请求。 */
    private boolean claimRevert(long paymentId, String revertNo, String requestDate) {
        int claimed = moneyTransactions.execute(status -> jdbc.update("""
                update payment_transaction
                set funds_confirmation_status = 'reverting', funds_revert_no = ?,
                    funds_revert_request_date = ?, funds_revert_code = null, funds_revert_message = null
                where id = ? and funds_confirmation_status = 'confirmed'
                """, revertNo, requestDate, paymentId));
        return claimed == 1;
    }
 
    /**
     * 落回退结果。
     *
     * <p>只有通道明确说成功才把状态退回 {@code pending}(等于「钱在延迟户」)。明确拒绝退回
     * {@code confirmed},其余一律停在 {@code reverting}:这条路径上没有真实回执证明码的含义之前不猜,
     * 因为把失败当成功会让订单翻回待核销而钱还在商户基本户。
     */
    private String applyRevertResult(long paymentId, String revertNo, HuifuDelayRevert result,
                                     Map<String, Object> payment) {
        switch (result.outcome()) {
            case SUCCEEDED -> {
                // 上一轮的确认回执(funds_confirm_*)保留下来作为「曾经确认过」的证据;商户重新核销时
                // 会走一次全新的交易确认。
                Long revertedAmount = cents(payment.get("fundsConfirmedAmount"));
                moneyTransactions.executeWithoutResult(status -> jdbc.update("""
                        update payment_transaction
                        set funds_confirmation_status = 'pending', funds_revert_no = ?,
                            funds_revert_code = ?, funds_revert_message = ?,
                            funds_reverted_amount = ?, funds_reverted_at = now()
                        where id = ? and funds_confirmation_status = 'reverting'
                        """, revertNo, result.code(), result.describe(), revertedAmount, paymentId));
                log.info("Fund revert {} succeeded: the money is back in the delay account", revertNo);
                return REVERTED;
            }
            case FAILED -> {
                // 通道明确拒绝:钱没有动,状态退回可重试的 confirmed,并把回执留作证据。
                moneyTransactions.executeWithoutResult(status -> jdbc.update("""
                        update payment_transaction
                        set funds_confirmation_status = 'confirmed',
                            funds_revert_code = ?, funds_revert_message = ?
                        where id = ? and funds_confirmation_status = 'reverting'
                        """, result.code(), result.describe(), paymentId));
                log.warn("Fund revert {} was rejected: {} ({})", revertNo, result.describe(), result.code());
                return REVERT_REJECTED;
            }
            case NEEDS_REVIEW -> {
                // 通道给了话但语义未定(例如 23000003):不能假定钱回到了延迟户,留在 reverting 等人工。
                // 通道流水号要进日志:人工去控台核对时靠它定位这笔回退。
                recordRevertAttempt(paymentId, revertNo, result.code(), result.describe());
                log.warn("Fund revert {} has an undetermined result, the order stays verified:"
                                + " provider={} code={} desc={}",
                        revertNo, result.providerTransactionNo(), result.code(), result.describe());
                return REVERT_NEEDS_REVIEW;
            }
            default -> {
                // 通道还在处理:同样是钱的位置未定,但它是正常的在途,商户稍后重试就能拿到结果。
                recordRevertAttempt(paymentId, revertNo, result.code(), result.describe());
                return REVERT_IN_FLIGHT;
            }
        }
    }
 
    /** 只记录一次回退尝试的请求号与描述,不改状态。 */
    private void recordRevertAttempt(long paymentId, String revertNo, String code, String message) {
        moneyTransactions.executeWithoutResult(status -> jdbc.update("""
                update payment_transaction
                set funds_revert_no = ?, funds_revert_code = ?, funds_revert_message = ?
                where id = ?
                """, revertNo, code, message == null ? "" : message, paymentId));
    }
 
    /**
     * 放弃这次回退认领:请求根本没发出去(本地配置或 SDK 出错),钱没有动,退回 confirmed 让商户重试。
     *
     * <p>请求号与请求日期要清掉:它们是给通道侧定位用的,这次通道根本没收到。失败原因留在
     * {@code funds_revert_code/message} 里,免得排查时只看到一条干干净净的 confirmed。
     */
    private void abandonRevertClaim(long paymentId, String message) {
        moneyTransactions.executeWithoutResult(status -> jdbc.update("""
                update payment_transaction
                set funds_confirmation_status = 'confirmed', funds_revert_no = null,
                    funds_revert_request_date = null, funds_revert_code = 'LOCAL_ERROR',
                    funds_revert_message = ?
                where id = ? and funds_confirmation_status = 'reverting'
                """, message == null ? "" : message, paymentId));
    }
 
    /** 这笔资金是否确实在通道延迟户——撤销核销只有在这个前提下才允许翻单。 */
    public static boolean moneyBackInDelayAccount(String outcome) {
        return REVERTED.equals(outcome) || REVERT_NOT_NEEDED.equals(outcome);
    }
 
    /**
     * 回退没有成功时给商户的话。
     *
     * <p>不能只说「撤销失败」:商户需要分清是钱已经结算到银行卡(要联系平台),还是通道还在处理
     * (稍后重试就行),否则只会反复点。文案跟结论常量放在一起,免得将来新增一种结论时漏配。
     */
    public static String revertRefusalMessage(String outcome) {
        if (REVERT_REJECTED.equals(outcome)) return "这笔订单的资金已结算到商户银行卡,无法撤销核销";
        if (REVERT_IN_FLIGHT.equals(outcome)) return "这笔订单的资金正在退回中,请稍后重试";
        if (REVERT_NOT_SENT.equals(outcome)) return "撤销核销暂时无法发起,请稍后重试";
        return "这笔订单的资金状态无法核对,请联系平台处理";
    }
 
    /** 数据库里的状态是小写,比较必须大小写无关。 */
    private static boolean statusIs(String status, String expected) {
        return status != null && expected.equalsIgnoreCase(status.trim());
    }
 
    private static boolean isConfirmed(String status) {
        return statusIs(status, "confirmed");
    }
 
    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;
    }
 
    /** 库里的金额字段(分);空值保持 null,好和「没记过金额」区分开。 */
    private static Long cents(Object value) {
        return value instanceof Number number ? number.longValue() : null;
    }
 
    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;
    }
}