shinnosuke
2026-08-28 3eb95d53e1037cc5e10b1da3a62b0ff4d5fc9536
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
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;
 
    public ManualConsumptionService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
 
    @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());
        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,
                                          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"),
                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) {
        List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId);
        if (merchants.isEmpty()) return false;
        Object raw = merchants.get(0).get("merchantRightsConfig");
        return raw != null && !string(raw).isBlank();
    }
 
    private List<Map<String, Object>> configuredRules(Long merchantId, String cardLevel, String sourceAreaType) {
        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 || string(raw).isBlank()) return List.of();
        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(string(raw), 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.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) {
        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);
        String configuredDescription = configuredRules(merchantId, cardLevel, sourceAreaType).stream()
                .map(rule -> string(rule.get("consumeDesc")))
                .filter(value -> !value.isBlank())
                .findFirst()
                .orElse("");
        if (configuredDescription.isBlank()) {
            configuredDescription = configuredRules(merchantId, cardLevel, sourceAreaType).stream()
                    .map(rule -> string(rule.get("merchantDetail")))
                    .filter(value -> !value.isBlank())
                    .findFirst()
                    .orElse("");
        }
        if (configuredDescription.isBlank()) {
            configuredDescription = configuredRules(merchantId, cardLevel, sourceAreaType).stream()
                    .map(rule -> string(rule.get("benefitText")))
                    .filter(value -> !value.isBlank())
                    .findFirst()
                    .orElse("");
        }
        if (!configuredDescription.isBlank()) return configuredDescription;
        return benefits.stream().filter(benefit -> matchesCardLevel(string(benefit.get("cardLevel")), cardLevel))
                .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准");
    }
 
    /** 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, "");
    }
 
    boolean matchesConfiguredLevel(String configured, String actual, String sourceAreaType) {
        if (!"EXCELLENT".equals(normalizedCardLevel(actual))) return matchesCardLevel(configured, actual);
        String configuredUpper = configured.trim().toUpperCase();
        String areaUpper = 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));
    }
}