shinnosuke
2026-08-06 e5c6b415449d0b7a5e9027dba009e46543004dc0
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
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", maskName(string(source.get("talentName"))));
        result.put("customerMobile", maskMobile(string(source.get("talentMobile"))));
        result.put("cardLevel", string(source.get("cardLevel")));
        result.put("benefitText", benefitText(principal.merchantId(), string(source.get("cardLevel"))));
        result.put("rules", rules(principal.merchantId(), string(source.get("cardLevel"))));
        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")), request.ruleId());
        Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), 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);
        if (expired(source.get("expiresAt"))) {
            throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码");
        }
        Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId());
        Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), request.amount());
        Map<String, Object> amounts = quote(rule, coupon, request.amount());
        Long dynamicCodeId = number(source.get("dynamicCodeId"));
        int codeUpdated = jdbc.update("""
                update talent_dynamic_code set status = 1, merchant_id = ?, used_at = now()
                where id = ? and status = 0 and expires_at > now()
                """, principal.merchantId(), 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, 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")), 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")));
        if (request.couponId() != null) {
            int used = jdbc.update("update merchant_coupon_claim set use_status = 1, order_id = ?, used_at = now() where id = ? and merchant_id = ? and talent_id = ? and claim_status = 1 and use_status = 0 and deleted = 0 and (expire_time is null or expire_time > now())", orderId, number(coupon.get("claimId")), principal.merchantId(), number(source.get("talentId")));
            if (used != 1) throw new BusinessException(ErrorCode.CONFLICT, "优惠券已被使用或已失效,请重新选择");
        }
        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
                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);
    }
 
    private void ensurePending(Map<String, Object> source) {
        if (number(source.get("attemptStatus")) != ATTEMPT_PENDING) {
            throw new BusinessException(ErrorCode.CONFLICT, "该人才码已处理,请重新扫码");
        }
        if (expired(source.get("expiresAt"))) {
            throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码");
        }
    }
 
    private List<Map<String, Object>> rules(Long merchantId, String cardLevel) {
        List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel);
        if (!configured.isEmpty()) 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();
    }
 
    private List<Map<String, Object>> configuredRules(Long merchantId, String cardLevel) {
        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")) && booleanValue(right.get("consumeEnabled")) && booleanValue(right.get("storeConsume")) && matchesCardLevel(string(right.get("level")), cardLevel)).map(this::configuredRule).filter(rule -> decimal(rule.get("discountRate")).signum() > 0).toList();
    }
 
    private 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.get("discount")));
        result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0);
        result.put("consumeDesc", right.get("consumeDesc"));
        return result;
    }
 
    private Map<String, Object> rule(Long merchantId, String cardLevel, Long ruleId) {
        List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel);
        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_claim cc join merchant_coupon c on c.id = cc.coupon_id
                where cc.merchant_id = ? and cc.talent_id = ? and cc.claim_status = 1 and cc.use_status = 0 and cc.deleted = 0
                  and c.deleted = 0 and c.status = 1 and c.store_consume = 1
                  and (cc.expire_time is null or cc.expire_time > now())
                  and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date)
                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 cc.id as claimId, 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_claim cc join merchant_coupon c on c.id = cc.coupon_id
                where cc.coupon_id = ? and cc.merchant_id = ? and cc.talent_id = ? and cc.claim_status = 1 and cc.use_status = 0 and cc.deleted = 0
                  and c.deleted = 0 and c.status = 1 and c.store_consume = 1
                  and (cc.expire_time is null or cc.expire_time > now())
                  and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date)
                """, 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(Object value) {
        try {
            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 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);
    }
 
    private 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();
        BigDecimal rate = decimal(rule.get("discountRate"));
        if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效");
        long payAmount = BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact();
        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 = number(rule.get("subsidyEnabled")) == 1 ? originalAmount - basePayAmount : 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 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) {
        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).stream().map(rule -> string(rule.get("consumeDesc"))).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("以当前商户到店消费规则为准");
    }
 
    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 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));
    }
}