shinnosuke
2026-08-12 f40c2939c6f784fc6721554794154a336d12bc3a
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -43,6 +43,7 @@
        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;
    }
@@ -52,7 +53,8 @@
        Map<String, Object> source = source(principal, request.verificationId(), false);
        ensurePending(source);
        Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId());
        return quote(rule, request.amount());
        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
@@ -66,15 +68,13 @@
            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> amounts = quote(rule, request.amount());
      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(), discountedAmount(rule, 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()
            where id = ? and status = 0
                """, principal.merchantId(), dynamicCodeId);
        if (codeUpdated == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效");
@@ -84,13 +84,13 @@
        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, expires_at, price_version,
                                          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, ?, ?)
                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("payAmount"), amounts.get("payAmount"),
                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")));
@@ -125,10 +125,7 @@
        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);
@@ -202,6 +199,50 @@
        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; };
    }
@@ -222,20 +263,34 @@
        return "true".equals(text) || "1".equals(text) || "yes".equals(text);
    }
    private Map<String, Object> quote(Map<String, Object> rule, BigDecimal inputAmount) {
    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 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 = number(rule.get("subsidyEnabled")) == 1 ? discountAmount : 0;
        long subsidyAmount = originalAmount - payAmount;
        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 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("""
@@ -275,6 +330,10 @@
                .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准");
    }
    String benefitTextForMerchant(Long merchantId, String cardLevel) {
        return benefitText(merchantId, cardLevel);
    }
    private boolean matchesCardLevel(String configured, String actual) {
        return normalizedCardLevel(configured).equals(normalizedCardLevel(actual));
    }