| | |
| | | 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, |
| | |
| | | this.quotaService = quotaService; |
| | | this.merchantMapper = merchantMapper; |
| | | this.codeMapper = codeMapper; |
| | | this.ticketPackageService = ticketPackageService; |
| | | this.couponMapper = couponMapper; |
| | | this.claimMapper = claimMapper; |
| | | this.voucherMapper = voucherMapper; |
| | |
| | | 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); |
| | | } |
| | |
| | | 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()); |
| | |
| | | |
| | | 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); |
| | |
| | | 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) { |
| | |
| | | .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) { |
| | |
| | | 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) { |
| | |
| | | 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; |
| | | } |
| | |
| | | |
| | | 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) { |
| | |
| | | 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) { |
| | | } |
| | | } |