yeg
2026-09-12 d879291f56eb64adffa44f763238ea736b4be99b
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
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.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;
 
    public HuifuPaymentService(
            JdbcTemplate jdbc,
            TransactionTemplate transactions,
            HuifuMiniProgramPaymentGateway gateway
    ) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
    }
 
    public Map<String, Object> beginMiniProgramPayment(String orderNo, Long talentId, int priceVersion, String idempotencyKey) {
        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()));
        } 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);
    }
 
    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, 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);
        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);
        }
        // 收款子商户在下单时已固定,支付请求只使用订单上的斗拱商户号。
        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);
            if ("PREPAY_CREATED".equals(status)) {
                return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false);
            }
            if ("CREATED".equals(status) || "CREATE_UNKNOWN".equals(status)) {
                return new PreparedPayment(paymentNo, huifuId, "", 0, "", LocalDateTime.now(), false, false, "", true);
            }
            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);
        }
 
        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);
    }
 
    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)
                values (?, ?, ?, ?, ?, ?, ?, ?, 'CREATED', ?, ?)
                """, paymentNo, PROVIDER, number(order.get("id")), orderNo, talentId, order.get("merchantId"), huifuId, payAmount, idempotencyKey, attemptNo);
    }
 
    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);
 
        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" -> "CANCELLED";
            case "CREATE_FAILED" -> "PAYMENT_CREATE_FAILED";
            case "CREATED", "PREPAY_CREATED" -> "PAYMENT_PENDING";
            case "CREATE_UNKNOWN" -> "PAYMENT_CONFIRMING";
            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
    ) {}
}