yeg
2026-09-17 e3f412c5474311a471345ea508db4875c9ec648e
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
package com.talentcard.server.business.payment;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.payment.PaymentReconciliationService.PendingPaymentConfirmation;
import com.talentcard.server.business.payment.PaymentReconciliationService.PendingPaymentState;
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentCommand;
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentGateway;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
 
@Service
public class HuifuPaymentService {
 
    private static final String PROVIDER = "HUIFU";
    private static final Logger log = LoggerFactory.getLogger(HuifuPaymentService.class);
 
    private final JdbcTemplate jdbc;
    private final TransactionTemplate transactions;
    private final HuifuMiniProgramPaymentGateway gateway;
    private final PaymentReconciliationService reconciliationService;
 
    public HuifuPaymentService(
            JdbcTemplate jdbc,
            TransactionTemplate transactions,
            HuifuMiniProgramPaymentGateway gateway,
            PaymentReconciliationService reconciliationService
    ) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
        this.reconciliationService = reconciliationService;
    }
 
    public Map<String, Object> beginMiniProgramPayment(String orderNo, Long talentId, int priceVersion, String idempotencyKey) {
        // 复用优先:本地已经持有这张订单的预支付单的支付参数、订单又还待支付时,直接把它交回客户端。
        // 这里刻意**不查通道**:查通道只会得到「处理中」这种没有结论的答复,而本地已有可用凭证——
        // 拿这个没有结论的答复把整笔订单停摆,正是用户报的死锁(见 docs/adr/0003 的 D3)。
        Map<String, Object> reusable = reusablePrepay(orderNo, talentId);
        if (reusable != null) {
            return pendingResult(string(reusable.get("paymentNo")), string(reusable.get("clientPaymentPayload")));
        }
 
        // 本地没有可复用的凭证时,通道是唯一知道钱有没有收下的地方。上一次支付可能已经被通道收下、
        // 回调却没到:那笔钱必须先落成本地已支付,否则这里会再发起一次支付(同一笔订单付第二次),
        // 或者把用户送进「订单已经支付,请勿重复发起支付」的死胡同。
        PendingPaymentConfirmation confirmation = reconciliationService.confirmPendingPayments(orderNo);
        if (confirmation.collected()) return completedResult(confirmation.paymentNo());
        // 通道答不出「钱收没收下」时不再抛错:那个不确定只属于这一次支付尝试,不该把用户堵在一句
        // 看不懂的报错上。返回可等待状态,页面渲染成「支付处理中,请稍候」。
        if (confirmation.state() == PendingPaymentState.UNCONFIRMED) return processingResult(confirmation.paymentNo());
 
        PreparedPayment prepared = transactions.execute(status -> prepare(orderNo, talentId, priceVersion, idempotencyKey));
        if (prepared == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付初始化失败");
        if (prepared.completed()) return completedResult(prepared.paymentNo());
        if (prepared.processing()) return processingResult(prepared.paymentNo());
        if (!prepared.callProvider()) return pendingResult(prepared.paymentNo(), prepared.clientPayload());
 
        Map<String, Object> response;
        try {
            response = gateway.createMiniProgramPayment(new HuifuMiniProgramPaymentCommand(
                    prepared.paymentNo(), prepared.huifuId(), prepared.openId(), prepared.amountInCent(), prepared.goodsDescription(), prepared.expiresAt(),
                    prepared.delayAccount()));
        } catch (HuifuPaymentGatewayException exception) {
            if (exception.isOutcomeUnknown()) {
                transactions.executeWithoutResult(status -> markCreateUnknown(prepared.paymentNo(), "GATEWAY_UNKNOWN", exception.getMessage()));
            } else {
                transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), "GATEWAY_REJECTED", exception.getMessage()));
            }
            throw new BusinessException(ErrorCode.CONFLICT, exception.getMessage());
        } catch (BusinessException exception) {
            transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), "LOCAL_ERROR", exception.getMessage()));
            throw exception;
        } catch (RuntimeException exception) {
            transactions.executeWithoutResult(status -> markCreateUnknown(prepared.paymentNo(), "GATEWAY_UNKNOWN", "支付结果确认中"));
            throw exception;
        }
        if (!providerAccepted(response)) {
            String message = responseText(response, "支付服务暂不可用");
            transactions.executeWithoutResult(status -> markCreateFailed(prepared.paymentNo(), responseCode(response), message));
            throw new BusinessException(ErrorCode.CONFLICT, message);
        }
        String clientPayload = writeJson(clientPayload(response));
        transactions.executeWithoutResult(status -> markPrepayCreated(prepared.paymentNo(), responseCode(response), responseText(response, ""), clientPayload));
        return pendingResult(prepared.paymentNo(), clientPayload);
    }
 
    /**
     * 本地是否还持有这张订单的预支付单的支付参数。
     *
     * <p>四个条件缺一不可:订单仍待支付且未过期、流水属于「本地尚未定局」的状态、支付参数还在本地、
     * 流水金额与订单当前实付一致。金额必须相等是因为预支付单是按金额建的:改了优惠券之后再拿旧单
     * 去收款会收错钱。流水上没有记 {@code price_version},金额相等是这里能拿到的最强判据。
     *
     * <p>{@code CLOSING}(关单中)与 {@code CANCEL_UNCONFIRMED}(关不掉的预支付单)同样在可复用之列:
     * 它们正是「通道侧这张单还可能被支付」的状态,此时新建一张预支付单等于同一笔订单可能被付两次
     * (D2),所以只能复用。
     */
    private Map<String, Object> reusablePrepay(String orderNo, Long talentId) {
        List<Map<String, Object>> rows = rows("""
                select p.payment_no, p.client_payment_payload
                from payment_transaction p
                join benefit_order o on o.id = p.order_id
                where p.provider = ? and p.order_no = ? and o.talent_id = ?
                  and o.deleted = 0 and o.order_status = 0
                  and (o.expires_at is null or o.expires_at > now())
                  and p.status in ('PREPAY_CREATED', 'CLOSING', 'CANCEL_UNCONFIRMED')
                  and coalesce(p.client_payment_payload, '') <> ''
                  and p.amount = o.pay_amount
                order by p.id desc
                limit 1
                """, PROVIDER, orderNo, talentId);
        return rows.isEmpty() ? null : rows.get(0);
    }
 
    /**
     * 这张订单上是否还有一张**可能被支付**的预支付单。
     *
     * <p>新建预支付单的前置不变量:上一张必须已经被证明不可能再被支付(已关闭,或通道明确说失败/
     * 交易不存在)。{@code CLOSING} 与 {@code CANCEL_UNCONFIRMED} 恰恰是「关不掉的预支付单」——通道
     * 既没说关掉、也没说钱收下了,它照样能被付进来。此时新建等于给同一笔订单留下两张可被支付的
     * 预支付单,通道确认两笔都成功时没有多收款退回流程(审计 §9 已复现过两次预授权)。
     */
    private String blockingAttempt(long orderId) {
        List<Map<String, Object>> rows = rows("""
                select payment_no from payment_transaction
                where order_id = ? and provider = ?
                  and status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED')
                order by id desc
                limit 1
                """, orderId, PROVIDER);
        return rows.isEmpty() ? "" : string(rows.get(0).get("paymentNo"));
    }
 
    private PreparedPayment prepare(String orderNo, Long talentId, int priceVersion, String idempotencyKey) {
        if (idempotencyKey == null || idempotencyKey.isBlank() || idempotencyKey.length() > 128) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少有效支付幂等键");
        }
        List<Map<String, Object>> orders = rows("""
                select o.id, o.order_no, o.talent_id, o.merchant_id, o.store_id, o.pay_amount, o.order_status, o.price_version,
                       o.expires_at, o.collect_huifu_id, o.order_type,
                       coalesce(o.product_name_snapshot, o.order_type, '人才权益服务') goods_description,
                       t.openid
                from benefit_order o
                join talent_user t on t.id = o.talent_id and t.deleted = 0
                where o.deleted = 0 and o.order_no = ? and o.talent_id = ?
                limit 1 for update
                """, orderNo, talentId);
        if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或无权支付");
        Map<String, Object> order = orders.get(0);
        long payAmount = number(order.get("payAmount"));
        if (number(order.get("orderStatus")) == 1) return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false, false);
        if (number(order.get("orderStatus")) != 0) throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可支付");
        if (number(order.get("priceVersion")) != priceVersion) throw new BusinessException(ErrorCode.CONFLICT, "订单金额已更新,请重新确认");
        LocalDateTime expiresAt = localDateTime(order.get("expiresAt"));
        if (expiresAt != null && !expiresAt.isAfter(LocalDateTime.now())) throw new BusinessException(ErrorCode.CONFLICT, "订单已超时,请重新下单");
        if (payAmount == 0) {
            jdbc.update("""
                    update benefit_order
                    set order_status = 1,
                        verify_status = case when order_type = 'STORE_CONSUME' then 1 else verify_status end
                    where id = ? and order_status = 0
                    """, number(order.get("id")));
            return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false, false);
        }
        // 收款子商户在下单时已固定,支付请求只使用订单上的斗拱商户号。
        String huifuId = string(order.get("collectHuifuId"));
        if (huifuId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "订单缺少收款子商户,请重新下单");
        String openId = string(order.get("openid"));
        if (openId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "当前用户未完成微信支付授权");
 
        List<Map<String, Object>> existingRows = rows("""
                select payment_no, order_no, talent_id, status, attempt_no, client_payment_payload
                from payment_transaction
                where idempotency_key = ?
                order by attempt_no desc
                limit 1 for update
                """, idempotencyKey);
        if (!existingRows.isEmpty()) {
            Map<String, Object> existing = existingRows.get(0);
            if (!orderNo.equals(string(existing.get("orderNo"))) || talentId.longValue() != number(existing.get("talentId"))) {
                throw new BusinessException(ErrorCode.CONFLICT, "支付请求已被其他订单使用,请重新发起支付");
            }
            String status = string(existing.get("status"));
            String paymentNo = string(existing.get("paymentNo"));
            if ("SUCCEEDED".equals(status)) return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, true, "", false, false);
            if ("PREPAY_CREATED".equals(status)) {
                // 支付参数没留在本地就没法把它复用给客户端,而这张预支付单可能已经可以被支付:
                // 既不新建也不报错,交给对账通道确认后再给结论。
                if (string(existing.get("clientPaymentPayload")).isBlank()) {
                    return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
                }
                return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false, false);
            }
            if ("CLOSING".equals(status) || "CANCEL_UNCONFIRMED".equals(status)) {
                // 关单中/关不掉的预支付单:通道还可能为它收钱,绝不能再建一张。
                return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
            }
            if ("CREATED".equals(status) || "CREATE_UNKNOWN".equals(status)) {
                return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
            }
            String blocking = blockingAttempt(number(order.get("id")));
            if (!blocking.isBlank()) {
                return new PreparedPayment(blocking, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
            }
            int nextAttempt = (int) number(existing.get("attemptNo")) + 1;
            String retryPaymentNo = BusinessIdGenerator.next("P");
            insertPaymentTransaction(retryPaymentNo, order, orderNo, talentId, payAmount, huifuId, idempotencyKey, nextAttempt);
            return new PreparedPayment(retryPaymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false, requiresDelayAccount(order));
        }
 
        String blocking = blockingAttempt(number(order.get("id")));
        if (!blocking.isBlank()) {
            return new PreparedPayment(blocking, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true, false);
        }
        String paymentNo = BusinessIdGenerator.next("P");
        insertPaymentTransaction(paymentNo, order, orderNo, talentId, payAmount, huifuId, idempotencyKey, 1);
        return new PreparedPayment(paymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false, requiresDelayAccount(order));
    }
 
    /**
     * 这笔订单的支付是否必须延迟入账。
     *
     * <p>只有「支付即核销」的到店消费订单不需要延迟:它在支付成功的同时就完成核销,不存在「钱已收但服务
     * 未确认」的窗口,延迟只会让商户的钱白等一道确认。其它订单类型(含以后新增的)一律延迟——这样即使
     * 将来新增了订单类型而没人记得改这里,未核销的钱也会默认被扣在通道,而不是直接落进商户账户。
     */
    private boolean requiresDelayAccount(Map<String, Object> order) {
        return !"STORE_CONSUME".equalsIgnoreCase(string(order.get("orderType")));
    }
 
    private void insertPaymentTransaction(
            String paymentNo,
            Map<String, Object> order,
            String orderNo,
            Long talentId,
            long payAmount,
            String huifuId,
            String idempotencyKey,
            int attemptNo
    ) {
        jdbc.update("""
                insert into payment_transaction(payment_no, provider, order_id, order_no, talent_id, merchant_id,
                                                provider_huifu_id, amount, status, idempotency_key, attempt_no,
                                                funds_confirmation_status)
                values (?, ?, ?, ?, ?, ?, ?, ?, 'CREATED', ?, ?, ?)
                """, paymentNo, PROVIDER, number(order.get("id")), orderNo, talentId, order.get("merchantId"), huifuId,
                payAmount, idempotencyKey, attemptNo, requiresDelayAccount(order) ? "pending" : null);
    }
 
    public Map<String, Object> paymentStatus(String orderNo, Long talentId, String paymentNo) {
        List<Map<String, Object>> orders = rows("""
                select id, order_status
                from benefit_order
                where deleted = 0 and order_no = ? and talent_id = ?
                limit 1
                """, orderNo, talentId);
        if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或无权查看");
 
        Map<String, Object> order = orders.get(0);
        if (number(order.get("orderStatus")) == 1) return completedResult(paymentNo == null ? "" : paymentNo);
        // 回调没到、本地还停在待支付时,通道是唯一知道钱有没有收下的地方。问一次就能把状态收敛回来,
        // 人才端也就不会一直停在待支付页。
        PendingPaymentConfirmation confirmation = reconciliationService.confirmPendingPayments(orderNo);
        if (confirmation.collected()) return completedResult(confirmation.paymentNo());
 
        String filter = paymentNo == null || paymentNo.isBlank() ? "" : " and payment_no = ?";
        Object[] args = paymentNo == null || paymentNo.isBlank()
                ? new Object[] { orderNo, talentId }
                : new Object[] { orderNo, talentId, paymentNo };
        List<Map<String, Object>> transactions = rows("""
                select payment_no, status, provider_response_code, provider_response_message, paid_at
                from payment_transaction
                where order_no = ? and talent_id = ?
                """ + filter + " order by id desc limit 1", args);
        if (transactions.isEmpty()) return Map.of("paymentStatus", "NOT_STARTED");
 
        Map<String, Object> transaction = transactions.get(0);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("paymentNo", transaction.get("paymentNo"));
        result.put("paymentStatus", statusText(string(transaction.get("status"))));
        result.put("providerCode", transaction.get("providerResponseCode"));
        result.put("providerMessage", transaction.get("providerResponseMessage"));
        result.put("paidAt", transaction.get("paidAt"));
        return result;
    }
 
    private void markPrepayCreated(String paymentNo, String responseCode, String responseMessage, String clientPayload) {
        jdbc.update("""
                update payment_transaction
                set status = 'PREPAY_CREATED', provider_response_code = ?, provider_response_message = ?, client_payment_payload = ?
                where payment_no = ? and status in ('CREATED', 'CREATE_FAILED')
                """, responseCode, responseMessage, clientPayload, paymentNo);
    }
 
    private void markCreateFailed(String paymentNo, String responseCode, String responseMessage) {
        jdbc.update("""
                update payment_transaction
                set status = 'CREATE_FAILED', provider_response_code = ?, provider_response_message = ?
                where payment_no = ? and status in ('CREATED', 'CREATE_FAILED')
                """, responseCode, responseMessage, paymentNo);
    }
 
    private void markCreateUnknown(String paymentNo, String responseCode, String responseMessage) {
        jdbc.update("""
                update payment_transaction
                set status = 'CREATE_UNKNOWN', provider_response_code = ?, provider_response_message = ?
                where payment_no = ? and status = 'CREATED'
                """, responseCode, responseMessage, paymentNo);
    }
 
    private Map<String, Object> pendingResult(String paymentNo, String payload) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("paymentNo", paymentNo);
        result.put("paymentStatus", "PAYMENT_PENDING");
        result.put("paymentParams", parsePayload(payload));
        log.info("Mini-program payment response: paymentNo={}, payload={}", paymentNo, logPayload(result));
        return result;
    }
 
    private Map<String, Object> completedResult(String paymentNo) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("paymentNo", paymentNo);
        result.put("paymentStatus", "PAID");
        return result;
    }
 
    private Map<String, Object> processingResult(String paymentNo) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("paymentNo", paymentNo);
        result.put("paymentStatus", "PAYMENT_PROCESSING");
        return result;
    }
 
    private String statusText(String status) {
        return switch (status) {
            case "SUCCEEDED" -> "PAID";
            case "CLOSED", "CANCEL_UNCONFIRMED" -> "CANCELLED";
            case "CREATE_FAILED" -> "PAYMENT_CREATE_FAILED";
            case "CREATED", "PREPAY_CREATED" -> "PAYMENT_PENDING";
            // 关单中与创建结果未定都是「还在确认」:客户端渲染成可等待的文案,而不是一个报错。
            case "CLOSING", "CREATE_UNKNOWN" -> "PAYMENT_PROCESSING";
            default -> "UNKNOWN";
        };
    }
 
    private Object clientPayload(Map<String, Object> response) {
        Map<String, Object> data = businessData(response);
        Object payInfo = data.get("pay_info");
        if (payInfo == null) payInfo = data.get("payInfo");
        if (payInfo instanceof String text && !text.isBlank()) {
            try {
                return JSON.parse(text);
            } catch (Exception ignored) {
                return Map.of("payInfo", text);
            }
        }
        return payInfo == null ? data : payInfo;
    }
 
    private boolean providerAccepted(Map<String, Object> response) {
        Map<String, Object> data = businessData(response);
        String status = string(data.get("trans_stat"));
        if (status.isBlank()) status = string(data.get("transStat"));
        Object payInfo = data.get("pay_info");
        if (payInfo == null) payInfo = data.get("payInfo");
        return payInfo != null && !String.valueOf(payInfo).isBlank()
                && ("P".equalsIgnoreCase(status) || "S".equalsIgnoreCase(status));
    }
 
    private String responseCode(Map<String, Object> response) {
        Map<String, Object> data = businessData(response);
        Object value = data.get("resp_code");
        if (value == null) value = data.get("code");
        if (value == null) value = data.get("sub_resp_code");
        return value == null ? "" : String.valueOf(value);
    }
 
    private String responseText(Map<String, Object> response, String fallback) {
        Map<String, Object> data = businessData(response);
        Object value = data.get("resp_desc");
        if (value == null) value = data.get("message");
        if (value == null) value = data.get("sub_resp_desc");
        return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value);
    }
 
    @SuppressWarnings("unchecked")
    private Map<String, Object> businessData(Map<String, Object> response) {
        Object data = response.get("data");
        return data instanceof Map<?, ?> map ? (Map<String, Object>) map : response;
    }
 
    private String writeJson(Object value) {
        try {
            return JSON.toJSONString(value);
        } catch (Exception exception) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付参数保存失败");
        }
    }
 
    private Object parsePayload(String payload) {
        if (payload == null || payload.isBlank()) return Map.of();
        try {
            return JSON.parse(payload);
        } catch (Exception exception) {
            return Map.of();
        }
    }
 
    private String logPayload(Object value) {
        try {
            return JSON.toJSONString(maskPaymentPayload(value));
        } catch (Exception ignored) {
            return "{\"log_error\":\"Unable to serialize payment response\"}";
        }
    }
 
    private Object maskPaymentPayload(Object value) {
        if (value instanceof Map<?, ?> source) {
            Map<String, Object> result = new LinkedHashMap<>();
            for (Map.Entry<?, ?> entry : source.entrySet()) {
                String key = String.valueOf(entry.getKey());
                result.put(key, sensitiveLogKey(key) ? maskLogValue(String.valueOf(entry.getValue())) : maskPaymentPayload(entry.getValue()));
            }
            return result;
        }
        if (value instanceof List<?> source) return source.stream().map(this::maskPaymentPayload).toList();
        if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
            try {
                return maskPaymentPayload(JSON.parse(text));
            } catch (Exception ignored) {
                return text;
            }
        }
        return value;
    }
 
    private boolean sensitiveLogKey(String key) {
        String normalized = key == null ? "" : key.toLowerCase();
        return normalized.contains("sign") || normalized.contains("secret") || normalized.contains("openid") || normalized.contains("authorization");
    }
 
    private String maskLogValue(String value) {
        if (value == null || value.isBlank()) return "";
        if (value.length() <= 8) return "***";
        return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
    }
 
    private LocalDateTime expiryOrDefault(LocalDateTime expiresAt) {
        return expiresAt == null ? LocalDateTime.now().plusMinutes(15) : expiresAt;
    }
 
    private long number(Object value) {
        return value instanceof Number number ? number.longValue() : 0;
    }
 
    private LocalDateTime localDateTime(Object value) {
        return value instanceof LocalDateTime time ? time : null;
    }
 
    private String string(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
 
    private List<Map<String, Object>> rows(String sql, Object... args) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, args));
    }
 
    private record PreparedPayment(
            String paymentNo,
            String huifuId,
            String openId,
            long amountInCent,
            String goodsDescription,
            LocalDateTime expiresAt,
            boolean callProvider,
            boolean completed,
            String clientPayload,
            boolean processing,
            boolean delayAccount
    ) {}
}