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
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
package com.talentcard.server.business.merchant;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.ArrayList;
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.merchant.auth.MerchantPrincipal;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class ManualConsumptionService {
    private static final String TALENT_CODE = "TALENT_CODE";
    private static final int ATTEMPT_PENDING = 0;
    private static final int ATTEMPT_ORDER_CREATED = 3;
 
    private final JdbcTemplate jdbc;
    private final MerchantPaymentReadinessService paymentReadinessService;
 
    public ManualConsumptionService(JdbcTemplate jdbc, MerchantPaymentReadinessService paymentReadinessService) {
        this.jdbc = jdbc;
        this.paymentReadinessService = paymentReadinessService;
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> detail(MerchantPrincipal principal, String verificationId) {
        Map<String, Object> source = source(principal, verificationId, false);
        ensurePending(source);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("verificationId", verificationId);
        result.put("customerName", displayName(string(source.get("talentName")), number(source.get("gender"))));
        result.put("customerMobile", maskMobile(string(source.get("talentMobile"))));
        result.put("cardLevel", string(source.get("cardLevel")));
        List<Map<String, Object>> matchedRules = rules(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType")));
        if (matchedRules.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前商户未配置适用于该人才卡的到店消费权益");
        result.put("benefitText", benefitText(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType"))));
        result.put("rules", matchedRules);
        result.put("coupons", coupons(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel"))));
        result.put("expiresAt", source.get("expiresAt"));
        return result;
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> quote(MerchantPrincipal principal, ManualConsumptionCreateRequest request) {
        Map<String, Object> source = source(principal, request.verificationId(), false);
        ensurePending(source);
        Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType")), request.ruleId());
        Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), discountedAmount(rule, request.amount()));
        return quote(rule, coupon, request.amount());
    }
 
    @Transactional
    public Map<String, Object> create(MerchantPrincipal principal, ManualConsumptionCreateRequest request, String idempotencyKey) {
        if (idempotencyKey == null || idempotencyKey.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键");
        }
        Map<String, Object> source = source(principal, request.verificationId(), true);
        Long existingOrderId = number(source.get("manualOrderId"));
        if (number(source.get("attemptStatus")) == ATTEMPT_ORDER_CREATED && existingOrderId != null) {
            return createdOrder(existingOrderId);
        }
        ensurePending(source);
        Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType")), request.ruleId());
        Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), discountedAmount(rule, request.amount()));
        Map<String, Object> amounts = quote(rule, coupon, request.amount());
        // 需要支付的订单在创建时固定收款子商户;能力未就绪时不消耗人才码,避免产生无法结算的订单。
        MerchantPaymentReadinessService.CollectingMerchant collectingMerchant = number(amounts.get("payAmount")) > 0
                ? paymentReadinessService.requireCollectingMerchant(principal.merchantId(), principal.storeId())
                : null;
        Long dynamicCodeId = number(source.get("dynamicCodeId"));
        int codeUpdated = consumeScannedTalentCode(principal, dynamicCodeId);
        if (codeUpdated == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效");
        }
 
        Map<String, Object> merchant = merchant(principal);
        String orderNo = BusinessIdGenerator.next("MC");
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, store_id, order_type,
                                          order_amount, subsidy_amount, base_pay_amount, pay_amount, coupon_id, coupon_amount, expires_at, price_version,
                                          merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
                                          merchant_longitude_snapshot, merchant_latitude_snapshot, product_name_snapshot,
                                          collect_onboarding_id, collect_store_id, collect_huifu_id,
                                          order_status, verify_status, merchant_verification_attempt_id, client_idempotency_key)
                values (?, ?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
                """, orderNo, number(source.get("talentId")), number(source.get("cardId")), string(source.get("cardLevel")), principal.merchantId(), principal.storeId(),
                amounts.get("originalAmount"), amounts.get("subsidyAmount"), amounts.get("basePayAmount"), amounts.get("payAmount"), request.couponId(), amounts.get("couponAmount"),
                LocalDateTime.now().plusMinutes(15), merchant.get("merchantName"), merchant.get("address"), merchant.get("contactMobile"),
                merchant.get("longitude"), merchant.get("latitude"), "到店消费 - " + rule.get("ruleName"),
                MerchantPaymentReadinessService.CollectingMerchant.applicationIdOf(collectingMerchant),
                MerchantPaymentReadinessService.CollectingMerchant.storeIdOf(collectingMerchant),
                MerchantPaymentReadinessService.CollectingMerchant.huifuIdOf(collectingMerchant),
                number(source.get("attemptId")), "manual-consumption-" + number(source.get("attemptId")));
        Long orderId = jdbc.queryForObject("select id from benefit_order where merchant_verification_attempt_id = ?", Long.class, number(source.get("attemptId")));
        jdbc.update("""
                update merchant_verification_attempt set status = ?, manual_order_id = ?
                where id = ? and status = ?
                """, ATTEMPT_ORDER_CREATED, orderId, number(source.get("attemptId")), ATTEMPT_PENDING);
        return createdOrder(orderId);
    }
 
    private Map<String, Object> source(MerchantPrincipal principal, String verificationId, boolean forUpdate) {
        String lock = forUpdate ? " for update" : "";
        List<Map<String, Object>> sources = rows("""
                select a.id attempt_id, a.dynamic_code_id, a.status attempt_status, a.expires_at, a.manual_order_id,
                       d.talent_id, d.card_id, c.card_level,
                       t.name talent_name, t.mobile talent_mobile, t.gender, t.source_area_type
                from merchant_verification_attempt a
                join talent_dynamic_code d on d.id = a.dynamic_code_id
                join talent_card c on c.id = d.card_id and c.deleted = 0
                join talent_user t on t.id = d.talent_id and t.deleted = 0
                where a.verification_id = ? and a.credential_type = ? and a.merchant_id = ? and a.store_id = ? and a.operator_id = ?
                limit 1
                """ + lock, verificationId.trim(), TALENT_CODE, principal.merchantId(), principal.storeId(), principal.operatorId());
        if (sources.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "人才码核销凭证不存在或无权操作");
        }
        return sources.get(0);
    }
 
    int consumeScannedTalentCode(MerchantPrincipal principal, Long dynamicCodeId) {
        return jdbc.update("""
                update talent_dynamic_code set status = 1, merchant_id = ?, used_at = now()
                where id = ? and status in (0, 2)
                """, principal.merchantId(), dynamicCodeId);
    }
 
    private void ensurePending(Map<String, Object> source) {
        if (number(source.get("attemptStatus")) != ATTEMPT_PENDING) {
            throw new BusinessException(ErrorCode.CONFLICT, "该人才码已处理,请重新扫码");
        }
    }
 
    private List<Map<String, Object>> rules(Long merchantId, String cardLevel, String sourceAreaType) {
        List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel, sourceAreaType);
        if (!configured.isEmpty() || hasStructuredRightsConfig(merchantId)) return configured;
        return rows("""
                select id, card_level, rule_name, discount_rate, subsidy_enabled
                from merchant_store_consume_rule
                where merchant_id = ? and status = 1 and (valid_start is null or valid_start <= current_date)
                  and (valid_end is null or valid_end >= current_date)
                order by sort_no desc, id desc
                """, merchantId).stream().filter(rule -> matchesCardLevel(string(rule.get("cardLevel")), cardLevel)).map(rule -> {
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("id", String.valueOf(rule.get("id")));
            result.put("name", rule.get("ruleName"));
            result.put("discountRate", decimal(rule.get("discountRate")));
            result.put("subsidyEnabled", number(rule.get("subsidyEnabled")) == 1);
            return result;
        }).toList();
    }
 
    boolean hasApplicableRule(Long merchantId, String cardLevel, String sourceAreaType) {
        return !rules(merchantId, cardLevel, sourceAreaType).isEmpty();
    }
 
    private boolean hasStructuredRightsConfig(Long merchantId) {
        return !rightsConfigs(merchantId).isEmpty();
    }
 
    private List<Map<String, Object>> rightsConfigs(Long merchantId) {
        List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId);
        if (merchants.isEmpty()) return List.of();
        Object raw = merchants.get(0).get("merchantRightsConfig");
        if (raw == null) {
            for (Map.Entry<String, Object> entry : merchants.get(0).entrySet()) {
                if (entry.getKey() != null && entry.getKey().toLowerCase().contains("rights")) {
                    raw = entry.getValue();
                    break;
                }
            }
        }
        if (raw == null || string(raw).isBlank()) return List.of();
        String json = raw instanceof byte[] bytes ? new String(bytes, java.nio.charset.StandardCharsets.UTF_8) : string(raw);
        List<Map<String, Object>> configs = new ArrayList<>();
        try {
            if (raw instanceof List<?> list) {
                for (Object item : list) if (item instanceof Map<?, ?> map) {
                    Map<String, Object> value = new LinkedHashMap<>();
                    for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue());
                    configs.add(value);
                }
            } else {
                for (Object item : JSON.parseArray(json, Map.class)) if (item instanceof Map<?, ?> map) {
                    Map<String, Object> value = new LinkedHashMap<>();
                    for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue());
                    configs.add(value);
                }
            }
        } catch (Exception ignored) {
            return List.of();
        }
        return configs;
    }
 
    private List<Map<String, Object>> configuredRules(Long merchantId, String cardLevel, String sourceAreaType) {
        return rightsConfigs(merchantId).stream()
                .filter(right -> booleanValue(right.get("enabled"))
                        && configuredStoreConsumptionEnabled(right)
                        && matchesConfiguredLevel(string(right.get("level")), cardLevel, sourceAreaType))
                .map(this::configuredRule)
                .filter(rule -> decimal(rule.get("discountRate")).signum() > 0 || decimal(rule.get("fixedReduction")).signum() > 0)
                .toList();
    }
 
    boolean configuredStoreConsumptionEnabled(Map<String, Object> right) {
        if (!booleanValue(right.get("storeConsume"))) return false;
        return booleanValue(right.get("consumeEnabled")) || "paid".equalsIgnoreCase(string(right.get("parkingType")));
    }
 
    Map<String, Object> configuredRule(Map<String, Object> right) {
        String level = normalizedCardLevel(string(right.get("level")));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", configRuleId(level));
        result.put("cardLevel", level);
        result.put("ruleName", string(right.get("tag")).isBlank() ? "到店消费优惠" : right.get("tag"));
        result.put("name", result.get("ruleName"));
        result.put("discountRate", configuredDiscountRate(right));
        result.put("fixedReduction", configuredFixedReduction(right));
        result.put("subsidyEnabled", configuredSubsidyEnabled(right) ? 1 : 0);
        result.put("merchantDetail", right.get("detail"));
        result.put("consumeDesc", right.get("consumeDesc"));
        result.put("benefitText", configuredBenefitText(right));
        return result;
    }
 
    private Map<String, Object> rule(Long merchantId, String cardLevel, String sourceAreaType, Long ruleId) {
        List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel, sourceAreaType);
        for (Map<String, Object> candidate : configured) if (String.valueOf(candidate.get("id")).equals(String.valueOf(ruleId))) return candidate;
        List<Map<String, Object>> found = rows("""
                select id, card_level, rule_name, discount_rate, subsidy_enabled
                from merchant_store_consume_rule
                where id = ? and merchant_id = ? and status = 1 and (valid_start is null or valid_start <= current_date)
                  and (valid_end is null or valid_end >= current_date)
                limit 1
                """, ruleId, merchantId);
        if (found.isEmpty() || !matchesCardLevel(string(found.get(0).get("cardLevel")), cardLevel)) throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不可使用所选优惠方式");
        return found.get(0);
    }
 
    private List<Map<String, Object>> coupons(Long merchantId, Long talentId, String cardLevel) {
        return rows("""
                select c.id, c.coupon_name as name, c.coupon_type as couponType, c.coupon_amount as couponAmount, c.talent_levels as talentLevels,
                       c.condition_type as conditionType, c.condition_amount as conditionAmount, c.valid_end as validEnd
                from merchant_coupon c
                where c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 1
                  and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date)
                  and not exists (
                    select 1 from benefit_order o
                    where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0
                      and (o.expires_at is null or o.expires_at > current_timestamp)
                  )
                order by c.coupon_amount desc, c.id desc
                """, merchantId, talentId).stream().filter(c -> supportsCard(string(c.get("talentLevels")), cardLevel)).map(c -> {
                    Map<String,Object> result = new LinkedHashMap<>(c); result.put("couponAmount", decimal(c.get("couponAmount")).movePointRight(2).longValue()); return result;
                }).toList();
    }
 
    private Map<String, Object> usableCoupon(Long merchantId, Long talentId, String cardLevel, Long couponId, BigDecimal amount) {
        if (couponId == null) return null;
        List<Map<String,Object>> found = rows("""
                select c.id, c.coupon_amount as couponAmount, c.condition_type as conditionType, c.condition_amount as conditionAmount,
                       c.talent_levels as talentLevels
                from merchant_coupon c
                where c.id = ? and c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 1
                  and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date)
                  and not exists (
                    select 1 from benefit_order o
                    where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0
                      and (o.expires_at is null or o.expires_at > current_timestamp)
                  )
                """, couponId, merchantId, talentId);
        if (found.isEmpty() || !supportsCard(string(found.get(0).get("talentLevels")), cardLevel)) throw new BusinessException(ErrorCode.CONFLICT, "优惠券当前不可用,请重新选择");
        Map<String,Object> coupon = found.get(0);
        String condition = string(coupon.get("conditionType"));
        if (!"NONE".equalsIgnoreCase(condition) && coupon.get("conditionAmount") != null && amount.compareTo(decimal(coupon.get("conditionAmount"))) < 0) throw new BusinessException(ErrorCode.CONFLICT, "消费金额未达到优惠券使用条件");
        return coupon;
    }
 
    private boolean supportsCard(String levels, String cardLevel) {
        if (levels == null || levels.isBlank()) return true;
        return java.util.Arrays.stream(levels.split("[,、,]" )).anyMatch(v -> matchesCardLevel(v, cardLevel));
    }
 
    private long configRuleId(String level) {
        return switch (level) { case "EXCELLENT" -> -1L; case "OUTSTANDING" -> -2L; case "ELITE" -> -3L; default -> -9L; };
    }
 
    private BigDecimal configuredDiscountRate(Map<String, Object> right) {
        try {
            Object value = "折扣".equals(string(right.get("consumeType"))) ? right.get("consumeDiscount") : right.get("discount");
            BigDecimal discount = decimal(string(value));
            return discount.signum() > 0 && discount.compareTo(BigDecimal.TEN) <= 0 ? discount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
 
    private BigDecimal configuredFixedReduction(Map<String, Object> right) {
        if (!"优惠".equals(string(right.get("consumeType")))) return BigDecimal.ZERO;
        try {
            BigDecimal reduction = decimal(string(right.get("consumeCoupon")));
            return reduction.signum() > 0 ? reduction : BigDecimal.ZERO;
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
 
    private boolean configuredSubsidyEnabled(Map<String, Object> right) {
        String consumeType = string(right.get("consumeType"));
        if ("折扣".equals(consumeType)) return booleanValue(right.get("consumeDiscountSubsidy"));
        if ("优惠".equals(consumeType)) return booleanValue(right.get("consumeCouponSubsidy"));
        return booleanValue(right.get("governmentSubsidy"));
    }
 
    private boolean booleanValue(Object value) {
        if (value instanceof Boolean bool) return bool;
        if (value instanceof Number number) return number.intValue() != 0;
        String text = string(value).toLowerCase();
        return "true".equals(text) || "1".equals(text) || "yes".equals(text);
    }
 
    Map<String, Object> quote(Map<String, Object> rule, Map<String,Object> coupon, BigDecimal inputAmount) {
        long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact();
        long payAmount = discountedAmountCents(rule, originalAmount);
        long basePayAmount = payAmount;
        long couponAmount = coupon == null ? 0 : Math.min(decimal(coupon.get("couponAmount")).movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue(), basePayAmount);
        payAmount = basePayAmount - couponAmount;
        long discountAmount = originalAmount - payAmount;
        long subsidyAmount = booleanValue(rule.get("subsidyEnabled")) ? discountAmount : 0;
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("originalAmount", originalAmount);
        result.put("discountAmount", discountAmount);
        result.put("payAmount", payAmount);
        result.put("basePayAmount", basePayAmount);
        result.put("couponAmount", couponAmount);
        result.put("subsidyAmount", subsidyAmount);
        result.put("ruleName", rule.get("ruleName"));
        return result;
    }
 
    private BigDecimal discountedAmount(Map<String, Object> rule, BigDecimal inputAmount) {
        long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact();
        return BigDecimal.valueOf(discountedAmountCents(rule, originalAmount), 2);
    }
 
    private long discountedAmountCents(Map<String, Object> rule, long originalAmount) {
        BigDecimal fixedReduction = rule.get("fixedReduction") == null ? BigDecimal.ZERO : decimal(rule.get("fixedReduction"));
        if (fixedReduction.signum() > 0) {
            long reductionCents = fixedReduction.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact();
            return Math.max(0, originalAmount - reductionCents);
        }
        BigDecimal rate = decimal(rule.get("discountRate"));
        if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效");
        return BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact();
    }
    private Map<String, Object> merchant(MerchantPrincipal principal) {
        List<Map<String, Object>> merchants = rows("""
                select merchant_name, address, contact_mobile, longitude, latitude
                from merchant where id = ? and status = 1 and deleted = 0
                limit 1
                """, principal.merchantId());
        if (merchants.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前商户不可创建到店消费订单");
        return merchants.get(0);
    }
 
    private Map<String, Object> createdOrder(Long orderId) {
        List<Map<String, Object>> orders = rows("""
                select order_no, order_amount, pay_amount, subsidy_amount, price_version, expires_at
                from benefit_order where id = ? and deleted = 0 limit 1
                """, orderId);
        if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "到店消费订单不存在");
        Map<String, Object> order = orders.get(0);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("orderNo", order.get("orderNo"));
        result.put("originalAmount", order.get("orderAmount"));
        result.put("payAmount", order.get("payAmount"));
        result.put("subsidyAmount", order.get("subsidyAmount"));
        result.put("priceVersion", order.get("priceVersion"));
        result.put("expiresAt", order.get("expiresAt"));
        return result;
    }
 
    private String benefitText(Long merchantId, String cardLevel, String sourceAreaType) {
        String configuredDescription = consumeDescFromRights(merchantId, cardLevel, sourceAreaType);
        if (!configuredDescription.isBlank()) return configuredDescription;
        List<Map<String, Object>> benefits = rows("""
                select card_level, benefit_text from merchant_card_benefit
                where merchant_id = ? and status = 1 order by sort_no desc, id desc
                """, merchantId);
        return benefits.stream().filter(benefit -> matchesCardLevel(string(benefit.get("cardLevel")), cardLevel))
                .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准");
    }
 
    private String consumeDescFromRights(Long merchantId, String cardLevel, String sourceAreaType) {
        List<Map<String, Object>> configs = rightsConfigs(merchantId).stream()
                .filter(right -> booleanValue(right.get("enabled")))
                .toList();
        String description = firstRightsText(configs, cardLevel, sourceAreaType, "consumeDesc");
        if (!description.isBlank()) return description;
        return firstRightsText(configs, cardLevel, sourceAreaType, "detail");
    }
 
    private String firstRightsText(List<Map<String, Object>> configs, String cardLevel, String sourceAreaType, String field) {
        for (Map<String, Object> right : configs) {
            if (!matchesConfiguredLevel(string(right.get("level")), cardLevel, sourceAreaType)) continue;
            String text = string(right.get(field)).trim();
            if (!text.isBlank()) return text;
        }
        if (!sourceAreaKnown(sourceAreaType)) {
            for (Map<String, Object> right : configs) {
                if (!matchesCardLevel(string(right.get("level")), cardLevel)) continue;
                String text = string(right.get(field)).trim();
                if (!text.isBlank()) return text;
            }
        }
        return "";
    }
 
    private boolean sourceAreaKnown(String sourceAreaType) {
        return localArea(sourceAreaType) || outsideArea(sourceAreaType);
    }
 
    private boolean localArea(String sourceAreaType) {
        String area = string(sourceAreaType).trim().toUpperCase();
        return area.contains("LOCAL") || area.contains("本地");
    }
 
    private boolean outsideArea(String sourceAreaType) {
        String area = string(sourceAreaType).trim().toUpperCase();
        return area.contains("OUTSIDE") || area.contains("外地");
    }
 
    /** Build the displayed benefit from the same structured fields used for pricing. */
    private String configuredBenefitText(Map<String, Object> right) {
        List<String> parts = new ArrayList<>();
        String consumeType = string(right.get("consumeType"));
        if ("优惠".equals(consumeType)) {
            String amount = string(right.get("consumeCoupon"));
            if (!amount.isBlank()) parts.add("到店消费立减" + amount + "元");
        } else {
            String discount = "折扣".equals(consumeType) ? string(right.get("consumeDiscount")) : string(right.get("discount"));
            if (!discount.isBlank()) parts.add("到店消费" + discount + "折");
        }
        return String.join(",", parts);
    }
 
    String benefitTextForMerchant(Long merchantId, String cardLevel) {
        return benefitText(merchantId, cardLevel, "");
    }
 
    String benefitTextForMerchant(Long merchantId, String cardLevel, String sourceAreaType) {
        return benefitText(merchantId, cardLevel, sourceAreaType);
    }
 
    boolean matchesConfiguredLevel(String configured, String actual, String sourceAreaType) {
        if (!"EXCELLENT".equals(normalizedCardLevel(actual))) return matchesCardLevel(configured, actual);
        String configuredUpper = configured.trim().toUpperCase();
        String areaUpper = string(sourceAreaType).trim().toUpperCase();
        if (configuredUpper.contains("本地") || configuredUpper.contains("LOCAL")) {
            return areaUpper.contains("LOCAL") || areaUpper.contains("本地");
        }
        if (configuredUpper.contains("外地") || configuredUpper.contains("OUTSIDE")) {
            return areaUpper.contains("OUTSIDE") || areaUpper.contains("外地");
        }
        return matchesCardLevel(configured, actual);
    }
 
    private boolean matchesCardLevel(String configured, String actual) {
        return normalizedCardLevel(configured).equals(normalizedCardLevel(actual));
    }
 
    private String normalizedCardLevel(String value) {
        String upper = string(value).trim().toUpperCase();
        if (upper.contains("卓越") || upper.equals("EXCELLENT")) return "EXCELLENT";
        if (upper.contains("杰出") || upper.equals("OUTSTANDING")) return "OUTSTANDING";
        if (upper.contains("菁英") || upper.equals("ELITE")) return "ELITE";
        return upper;
    }
 
    private boolean expired(Object value) {
        return value instanceof LocalDateTime time && !time.isAfter(LocalDateTime.now());
    }
 
    private Long number(Object value) {
        return value instanceof Number number ? number.longValue() : null;
    }
 
    private BigDecimal decimal(Object value) {
        if (value instanceof BigDecimal decimal) return decimal;
        if (value instanceof Number number) return BigDecimal.valueOf(number.doubleValue());
        return new BigDecimal(string(value));
    }
 
    private String string(Object value) {
        return value == null ? "" : String.valueOf(value);
    }
 
    private String maskName(String name) {
        if (name.isBlank()) return "持卡人";
        return name.length() == 1 ? name + "*" : name.substring(0, 1) + "*".repeat(name.length() - 1);
    }
 
    private String displayName(String name, Long gender) {
        if (name.isBlank()) return "持卡人";
        if (name.endsWith("先生") || name.endsWith("女士")) return name;
        return name + (gender != null && gender == 2 ? "女士" : "先生");
    }
 
    private String maskMobile(String mobile) {
        if (mobile.length() < 7) return "";
        return mobile.substring(0, 3) + "****" + mobile.substring(mobile.length() - 4);
    }
 
    private List<Map<String, Object>> rows(String sql, Object... args) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, args));
    }
}