wenminjie
2026-08-12 7361c250cee0a13724bd4ac613a39e57b8576ec5
talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
@@ -50,11 +50,13 @@
    private final BenefitFreeStayVoucherMapper voucherMapper;
    private final BenefitFreeStayVerificationCodeMapper freeStayCodeMapper;
    private final TicketPackageCodeMapper codeMapper;
    private final TicketPackagePortalService ticketPackageService;
    public BenefitVerificationCodeService(
            BenefitQuotaService quotaService,
            BenefitMerchantMapper merchantMapper,
            TicketPackageCodeMapper codeMapper,
            TicketPackagePortalService ticketPackageService,
            BenefitCinemaCouponMapper couponMapper,
            BenefitCinemaCouponClaimMapper claimMapper,
            BenefitFreeStayVoucherMapper voucherMapper,
@@ -63,6 +65,7 @@
        this.quotaService = quotaService;
        this.merchantMapper = merchantMapper;
        this.codeMapper = codeMapper;
        this.ticketPackageService = ticketPackageService;
        this.couponMapper = couponMapper;
        this.claimMapper = claimMapper;
        this.voucherMapper = voucherMapper;
@@ -92,6 +95,9 @@
        String benefitType = quotaService.normalizeBenefitType(request.benefitType());
        if (BenefitQuotaService.MOVIE_TICKET.equals(benefitType)) {
            ensureCinemaMerchant(merchantId);
            if (ticketPackageService.hasPortalPackages(merchantId)) {
                return ticketPackageService.merchantQuota(request.talentId(), request.cardId(), merchantId);
            }
        } else {
            ensureHotelMerchant(merchantId);
        }
@@ -105,6 +111,7 @@
        result.put("quotaUsed", quota.used());
        result.put("quotaRemaining", quota.remaining());
        result.put("quotaResetCycle", quota.resetCycle());
        result.put("quotaDetail", quota.detail());
        result.put("quotaWindowStart", quota.windowStart());
        result.put("sourceCardId", quota.cardId());
        result.put("sourceTalentId", quota.talentId());
@@ -128,6 +135,10 @@
    private Map<String, Object> generateMovieCode(BenefitVerificationCodeRequest request, String idempotencyKey) {
        Long merchantId = requirePositive(request.merchantId(), "商户ID不能为空");
        ensureCinemaMerchant(merchantId);
        if (ticketPackageService.hasPortalPackages(merchantId)) {
            return generateMovieTicketPackageCode(request, idempotencyKey, merchantId);
        }
        BenefitQuotaService.QuotaSnapshot quota = quotaService.requireAvailable(
                BenefitQuotaService.MOVIE_TICKET, request.cardId(), request.talentId(), merchantId, null, 1);
@@ -194,6 +205,61 @@
        issueMoviePoolCode(poolCode, claimId);
        BenefitCinemaCouponClaim claim = movieClaimById(claimId);
        return movieCodeResponse(claim, couponMapper.selectById(couponId), quota, dailyLimit, dailyUsed + 1, true);
    }
    private Map<String, Object> generateMovieTicketPackageCode(BenefitVerificationCodeRequest request,
                                                               String idempotencyKey,
                                                               Long merchantId) {
        String claimKey = claimIdempotencyKey(idempotencyKey, "MOVIE");
        Map<String, Object> claim = ticketPackageService.claimByMerchant(
                merchantId, request.talentId(), request.cardId(), claimKey);
        Long packageId = longValue(claim.get("packageId"));
        Map<String, Object> quota = ticketPackageService.packageQuota(
                request.talentId(), request.cardId(), merchantId, packageId);
        LocalDateTime expiresAt = ticketPackageExpiresAt(claim.get("validEnd"));
        int quotaTotal = intValue(quota.get("quotaTotal"));
        int quotaUsed = intValue(quota.get("quotaUsed"));
        int quotaRemaining = intValue(quota.get("quotaRemaining"));
        boolean newlyIssued = booleanValue(claim.get("newlyIssued"));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("benefitType", BenefitQuotaService.MOVIE_TICKET);
        result.put("verifyCode", claim.get("exchangeCode"));
        result.put("displayCode", claim.get("exchangeCode"));
        result.put("exchangeCode", claim.get("exchangeCode"));
        result.put("expiresAt", expiresAt);
        result.put("expiresAtEpochMillis", expiresAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
        result.put("expiresInSeconds", Math.max(0, java.time.Duration.between(LocalDateTime.now(), expiresAt).toSeconds()));
        result.put("refreshAfterSeconds", MOVIE_CODE_SECONDS);
        result.put("sharedQuotaMode", "AUTO_DEDUCT");
        result.put("sourceType", "TICKET_PACKAGE_CLAIM");
        result.put("sourceId", claim.get("claimId"));
        result.put("claimId", claim.get("claimId"));
        result.put("merchantId", merchantId);
        result.put("merchantName", quota.get("merchantName"));
        result.put("packageId", claim.get("packageId"));
        result.put("couponId", claim.get("packageId"));
        result.put("couponName", first(claim.get("packageName"), quota.get("packageName")));
        result.put("packageName", first(claim.get("packageName"), quota.get("packageName")));
        result.put("quotaTotal", quotaTotal);
        result.put("quotaUsed", quotaUsed);
        result.put("quotaRemainingBefore", newlyIssued ? quotaRemaining + 1 : quotaRemaining);
        result.put("quotaRemaining", quotaRemaining);
        result.put("quotaResetCycle", quota.get("quotaResetCycle"));
        result.put("quotaWindowStart", quota.get("quotaWindowStart"));
        result.put("sourceCardId", quota.get("sourceCardId"));
        result.put("sourceTalentId", quota.get("sourceTalentId"));
        result.put("sourceCardType", quota.get("sourceCardType"));
        result.put("familyCard", quota.get("familyCard"));
        result.put("primaryCardId", quota.get("primaryCardId"));
        result.put("primaryTalentId", quota.get("primaryTalentId"));
        result.put("primaryCardNo", quota.get("primaryCardNo"));
        result.put("cardLevel", quota.get("cardLevel"));
        result.put("dailyLimit", quotaTotal);
        result.put("dailyUsed", quotaUsed);
        result.put("dailyRemaining", quotaRemaining);
        result.put("quotaEffect", newlyIssued ? "RESERVED_ON_CODE_GENERATION" : "EXISTING_ACTIVE_CODE");
        return result;
    }
    private Map<String, Object> generateHotelFreeStayCode(BenefitVerificationCodeRequest request) {
@@ -294,8 +360,7 @@
                .in("card_id", sharedCardIds)
                .eq("coupon_id", couponId)
                .apply("((upper(coalesce(code_status, 'ACTIVE')) = 'USED' and used_at >= current_date) or "
                        + "(upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE' and updated_at >= current_date "
                        + ")))"));
                        + "(upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE' and updated_at >= current_date))"));
        return intValue(count);
    }
    private BenefitCinemaCouponClaim movieClaimByIdempotencyKey(String key) {
@@ -339,7 +404,7 @@
    private boolean isCinemaMerchantType(String merchantType) {
        String type = merchantType == null ? "" : merchantType.trim();
        return "cinema".equalsIgnoreCase(type) || "theatre".equalsIgnoreCase(type)
                || "电影院".equals(type) || "影院".equals(type) || "影城".equals(type) || "剧院".equals(type);
                || "观影服务".equals(type) || "电影院".equals(type) || "影院".equals(type) || "影城".equals(type) || "剧院".equals(type);
    }
    private void ensureHotelMerchant(Long merchantId) {
@@ -443,6 +508,7 @@
        result.put("quotaUsed", quota.used());
        result.put("quotaRemainingBefore", quota.remaining());
        result.put("quotaResetCycle", quota.resetCycle());
        result.put("quotaDetail", quota.detail());
        result.put("quotaWindowStart", quota.windowStart());
        return result;
    }
@@ -458,6 +524,24 @@
    private boolean startsWithMoviePrefix(String code) {
        return code != null && code.startsWith(MOVIE_TOKEN_PREFIX);
    }
    private LocalDateTime ticketPackageExpiresAt(Object validEnd) {
        if (validEnd instanceof LocalDate date) {
            return date.plusDays(1).atStartOfDay();
        }
        if (validEnd instanceof java.sql.Date date) {
            return date.toLocalDate().plusDays(1).atStartOfDay();
        }
        String raw = validEnd == null ? "" : String.valueOf(validEnd).trim();
        if (raw.length() >= 10) {
            try {
                return LocalDate.parse(raw.substring(0, 10)).plusDays(1).atStartOfDay();
            } catch (RuntimeException ignored) {
                return LocalDateTime.now().plusYears(1);
            }
        }
        return LocalDateTime.now().plusYears(1);
    }
    private String claimIdempotencyKey(String raw, String token) {
@@ -510,6 +594,30 @@
        return value instanceof Number number ? number.intValue() : 0;
    }
    private Long longValue(Object value) {
        return value instanceof Number number ? number.longValue() : null;
    }
    private Object first(Object... values) {
        for (Object value : values) {
            if (value != null && !String.valueOf(value).isBlank()) {
                return value;
            }
        }
        return "";
    }
    private boolean booleanValue(Object value) {
        if (value instanceof Boolean bool) {
            return bool;
        }
        if (value instanceof Number number) {
            return number.intValue() != 0;
        }
        String raw = value == null ? "" : String.valueOf(value).trim();
        return "true".equalsIgnoreCase(raw) || "1".equals(raw);
    }
    private record MovieCouponChoice(BenefitCinemaCoupon coupon, int dailyLimit, int dailyUsed) {
    }
}