From 8898f00c2150048a03ca561b7ed98dbf9c80732b Mon Sep 17 00:00:00 2001
From: wenminjie <1036699586@qq.com>
Date: 星期四, 06 八月 2026 16:18:51 +0800
Subject: [PATCH] flyway、对接小程序
---
talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java | 206 +++++++++++++++++++++++++++++++++------------------
1 files changed, 132 insertions(+), 74 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
index a229c44..ee19fa8 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
@@ -1,5 +1,11 @@
package com.talentcard.server.service;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import javax.imageio.ImageIO;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.qrcode.QRCodeWriter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -20,18 +26,21 @@
import com.talentcard.server.entity.BenefitFreeStayVerificationCode;
import com.talentcard.server.entity.BenefitFreeStayVoucher;
import com.talentcard.server.entity.BenefitMerchant;
+import com.talentcard.server.entity.TicketPackageCode;
import com.talentcard.server.mapper.BenefitCinemaCouponClaimMapper;
import com.talentcard.server.mapper.BenefitCinemaCouponMapper;
import com.talentcard.server.mapper.BenefitFreeStayVerificationCodeMapper;
import com.talentcard.server.mapper.BenefitFreeStayVoucherMapper;
import com.talentcard.server.mapper.BenefitMerchantMapper;
+import com.talentcard.server.mapper.TicketPackageCodeMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BenefitVerificationCodeService {
- public static final int CODE_SECONDS = 300;
- public static final String MOVIE_TOKEN_PREFIX = "MV2";
+ public static final int MOVIE_CODE_SECONDS = 0;
+ public static final int HOTEL_CODE_SECONDS = 60;
+ public static final String MOVIE_TOKEN_PREFIX = "WD-";
public static final String HOTEL_TOKEN_PREFIX = "HV2";
private final BenefitQuotaService quotaService;
@@ -40,10 +49,12 @@
private final BenefitCinemaCouponClaimMapper claimMapper;
private final BenefitFreeStayVoucherMapper voucherMapper;
private final BenefitFreeStayVerificationCodeMapper freeStayCodeMapper;
+ private final TicketPackageCodeMapper codeMapper;
public BenefitVerificationCodeService(
BenefitQuotaService quotaService,
BenefitMerchantMapper merchantMapper,
+ TicketPackageCodeMapper codeMapper,
BenefitCinemaCouponMapper couponMapper,
BenefitCinemaCouponClaimMapper claimMapper,
BenefitFreeStayVoucherMapper voucherMapper,
@@ -51,10 +62,59 @@
) {
this.quotaService = quotaService;
this.merchantMapper = merchantMapper;
+ this.codeMapper = codeMapper;
this.couponMapper = couponMapper;
this.claimMapper = claimMapper;
this.voucherMapper = voucherMapper;
this.freeStayCodeMapper = freeStayCodeMapper;
+ }
+
+ @Transactional(readOnly = true)
+ public byte[] movieCodeImage(Long claimId) {
+ BenefitCinemaCouponClaim claim = movieClaimById(claimId);
+ if (!activeMovieCode(claim)) {
+ throw new BusinessException(ErrorCode.CONFLICT, "电影票兑换码不可用");
+ }
+ try {
+ var matrix = new QRCodeWriter().encode(claim.getExchangeCode(), BarcodeFormat.QR_CODE, 480, 480);
+ BufferedImage image = MatrixToImageWriter.toBufferedImage(matrix);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ ImageIO.write(image, "PNG", output);
+ return output.toByteArray();
+ } catch (Exception ex) {
+ throw new BusinessException(ErrorCode.INTERNAL_ERROR, "电影票二维码生成失败");
+ }
+ }
+
+ @Transactional
+ public Map<String, Object> quota(BenefitVerificationCodeRequest request) {
+ Long merchantId = requirePositive(request.merchantId(), "商户ID不能为空");
+ String benefitType = quotaService.normalizeBenefitType(request.benefitType());
+ if (BenefitQuotaService.MOVIE_TICKET.equals(benefitType)) {
+ ensureCinemaMerchant(merchantId);
+ } else {
+ ensureHotelMerchant(merchantId);
+ }
+ BenefitQuotaService.QuotaSnapshot quota = quotaService.current(
+ benefitType, request.cardId(), request.talentId(), merchantId, null);
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("benefitType", benefitType);
+ result.put("merchantId", merchantId);
+ result.put("available", quota.remaining() > 0);
+ result.put("quotaTotal", quota.total());
+ result.put("quotaUsed", quota.used());
+ result.put("quotaRemaining", quota.remaining());
+ result.put("quotaResetCycle", quota.resetCycle());
+ result.put("quotaWindowStart", quota.windowStart());
+ result.put("sourceCardId", quota.cardId());
+ result.put("sourceTalentId", quota.talentId());
+ result.put("sourceCardType", quota.cardType());
+ result.put("familyCard", quota.familyCard());
+ result.put("primaryCardId", quota.primaryCardId());
+ result.put("primaryTalentId", quota.primaryTalentId());
+ result.put("primaryCardNo", quota.primaryCardNo());
+ result.put("cardLevel", quota.cardLevel());
+ return result;
}
@Transactional
@@ -68,7 +128,6 @@
private Map<String, Object> generateMovieCode(BenefitVerificationCodeRequest request, String idempotencyKey) {
Long merchantId = requirePositive(request.merchantId(), "商户ID不能为空");
- expireExpiredMovieCodes(merchantId);
BenefitQuotaService.QuotaSnapshot quota = quotaService.requireAvailable(
BenefitQuotaService.MOVIE_TICKET, request.cardId(), request.talentId(), merchantId, null, 1);
@@ -78,39 +137,40 @@
int dailyLimit = choice.dailyLimit();
int dailyUsed = choice.dailyUsed();
- String token = MOVIE_TOKEN_PREFIX + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
- String claimKey = claimIdempotencyKey(idempotencyKey, token);
+ String claimKey = claimIdempotencyKey(idempotencyKey, "MOVIE");
BenefitCinemaCouponClaim idempotent = movieClaimByIdempotencyKey(claimKey);
if (idempotent != null) {
ensureSameMovieClaim(idempotent, couponId, merchantId, quota.cardId());
- if (activeMovieCode(idempotent) && startsWithMoviePrefix(idempotent.getExchangeCode())) {
+ if (activeMovieCode(idempotent)) {
return movieCodeResponse(idempotent, coupon, quota, dailyLimit, dailyUsed, false);
}
throw new BusinessException(ErrorCode.CONFLICT, "幂等键对应的电影票核销码已过期或已使用");
}
BenefitCinemaCouponClaim existing = movieClaimForUpdate(couponId, quota.talentId());
- Long claimId;
if (existing != null) {
String status = upper(existing.getCodeStatus());
if ("USED".equals(status)) {
throw new BusinessException(ErrorCode.CONFLICT, "电影票已核销");
}
- if (activeMovieCode(existing) && startsWithMoviePrefix(existing.getExchangeCode())) {
+ if (activeMovieCode(existing)) {
return movieCodeResponse(existing, coupon, quota, dailyLimit, dailyUsed, false);
}
- boolean stockAlreadyReserved = "ACTIVE".equals(status) && !startsWithMoviePrefix(existing.getExchangeCode());
- if (!stockAlreadyReserved) {
- decrementMovieStock(couponId, merchantId);
- }
+ }
+
+ TicketPackageCode poolCode = reserveMoviePoolCode(merchantId, quota.talentId(), quota.cardId());
+ Long claimId;
+ if (existing != null) {
+ decrementMovieStock(couponId, merchantId);
BenefitCinemaCouponClaim update = new BenefitCinemaCouponClaim();
update.setId(existing.getId());
- update.setExchangeCode(token);
+ update.setExchangeCode(poolCode.getExchangeCode());
update.setCodeStatus("ACTIVE");
update.setStatus("RECEIVED");
update.setUsedAt(null);
update.setValidEnd(coupon.getValidEnd());
update.setIdempotencyKey(claimKey);
+ update.setDeleted(0);
claimMapper.updateById(update);
claimId = existing.getId();
} else {
@@ -124,13 +184,14 @@
claim.setCardNo(quota.cardNo());
claim.setValidEnd(coupon.getValidEnd());
claim.setStatus("RECEIVED");
- claim.setExchangeCode(token);
+ claim.setExchangeCode(poolCode.getExchangeCode());
claim.setCodeStatus("ACTIVE");
claim.setIdempotencyKey(claimKey);
claim.setDeleted(0);
claimMapper.insert(claim);
claimId = claim.getId();
}
+ issueMoviePoolCode(poolCode, claimId);
BenefitCinemaCouponClaim claim = movieClaimById(claimId);
return movieCodeResponse(claim, couponMapper.selectById(couponId), quota, dailyLimit, dailyUsed + 1, true);
}
@@ -140,7 +201,7 @@
ensureHotelMerchant(merchantId);
BenefitQuotaService.QuotaSnapshot quota = quotaService.requireAvailable(
BenefitQuotaService.HOTEL, request.cardId(), request.talentId(), merchantId, null, 1);
- BenefitFreeStayVoucher voucher = voucherForUpdate(quota.primaryTalentId(), merchantId, quota.primaryCardId(), quota.total());
+ BenefitFreeStayVoucher voucher = voucherForUpdate(quota, merchantId);
if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd())) {
throw new BusinessException(ErrorCode.CONFLICT, "酒店权益券当前不可用");
}
@@ -155,7 +216,7 @@
BenefitFreeStayVerificationCode code = new BenefitFreeStayVerificationCode();
code.setCodeToken(hotelFreeStayToken(quota.cardId()));
code.setVoucherId(voucher.getId());
- code.setExpiresAt(LocalDateTime.now().plusSeconds(CODE_SECONDS));
+ code.setExpiresAt(LocalDateTime.now().plusSeconds(HOTEL_CODE_SECONDS));
code.setStatus(0);
freeStayCodeMapper.insert(code);
return hotelCodeResponse(code, voucher, quota);
@@ -193,6 +254,27 @@
throw new BusinessException(ErrorCode.CONFLICT, "当天电影票领取次数已达上限");
}
+ private TicketPackageCode reserveMoviePoolCode(Long merchantId, Long talentId, Long cardId) {
+ TicketPackageCode code = codeMapper.selectAvailableByMerchantForUpdate(merchantId);
+ if (code == null) {
+ throw new BusinessException(ErrorCode.CONFLICT, "电影票兑换码码池库存不足,请联系后台导入兑换码");
+ }
+ int locked = codeMapper.lockForIssue(code.getId(), talentId, cardId);
+ if (locked != 1) {
+ throw new BusinessException(ErrorCode.CONFLICT, "电影票兑换码已被领取,请重试");
+ }
+ return code;
+ }
+
+ private void issueMoviePoolCode(TicketPackageCode code, Long claimId) {
+ codeMapper.update(null, new UpdateWrapper<TicketPackageCode>()
+ .set("status", "ISSUED")
+ .set("issued_claim_id", claimId)
+ .set("issued_at", LocalDateTime.now())
+ .eq("id", code.getId())
+ .eq("status", "LOCKED")
+ .eq("deleted", 0));
+ }
private void decrementMovieStock(Long couponId, Long merchantId) {
int updated = couponMapper.update(null, new UpdateWrapper<BenefitCinemaCoupon>()
.setSql("stock = stock - 1")
@@ -213,35 +295,9 @@
.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 "
- + "and updated_at > date_sub(now(), interval 300 second)))"));
+ + ")))"));
return intValue(count);
}
-
- private void expireExpiredMovieCodes(Long merchantId) {
- List<BenefitCinemaCouponClaim> expired = claimMapper.selectList(new QueryWrapper<BenefitCinemaCouponClaim>()
- .eq("deleted", 0)
- .eq("merchant_id", merchantId)
- .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'")
- .likeRight("exchange_code", MOVIE_TOKEN_PREFIX)
- .isNull("used_at")
- .apply("updated_at <= date_sub(now(), interval 300 second)")
- .last("for update"));
- for (BenefitCinemaCouponClaim claim : expired) {
- int updated = claimMapper.update(null, new UpdateWrapper<BenefitCinemaCouponClaim>()
- .set("code_status", "EXPIRED")
- .set("status", "EXPIRED")
- .eq("id", claim.getId())
- .eq("deleted", 0)
- .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'"));
- if (updated == 1) {
- couponMapper.update(null, new UpdateWrapper<BenefitCinemaCoupon>()
- .setSql("stock = stock + 1")
- .eq("id", claim.getCouponId())
- .eq("deleted", 0));
- }
- }
- }
-
private BenefitCinemaCouponClaim movieClaimByIdempotencyKey(String key) {
return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
.eq("deleted", 0)
@@ -250,9 +306,7 @@
}
private BenefitCinemaCouponClaim movieClaimForUpdate(Long couponId, Long talentId) {
- return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
- .eq("deleted", 0)
- .eq("coupon_id", couponId)
+ return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>().eq("coupon_id", couponId)
.eq("talent_id", talentId)
.last("limit 1 for update"));
}
@@ -277,56 +331,63 @@
private void ensureCinemaMerchant(Long merchantId) {
BenefitMerchant merchant = merchantMapper.selectById(merchantId);
if (merchant == null || intValue(merchant.getDeleted()) != 0 || intValue(merchant.getStatus()) != 1
- || !"cinema".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) {
+ || !isCinemaMerchantType(merchant.getMerchantType())) {
throw new BusinessException(ErrorCode.CONFLICT, "未找到有效影院商户");
}
+ }
+
+ 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);
}
private void ensureHotelMerchant(Long merchantId) {
BenefitMerchant merchant = merchantMapper.selectById(merchantId);
if (merchant == null || intValue(merchant.getDeleted()) != 0 || intValue(merchant.getStatus()) != 1
- || !"hotel".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) {
+ || !isHotelMerchantType(merchant.getMerchantType())) {
throw new BusinessException(ErrorCode.CONFLICT, "未找到有效酒店商户");
}
}
- private BenefitFreeStayVoucher voucherForUpdate(Long primaryTalentId, Long merchantId, Long primaryCardId, int totalCount) {
+ private boolean isHotelMerchantType(String merchantType) {
+ String type = merchantType == null ? "" : merchantType.trim();
+ return "hotel".equalsIgnoreCase(type) || "酒店服务".equals(type);
+ }
+
+ private BenefitFreeStayVoucher voucherForUpdate(BenefitQuotaService.QuotaSnapshot quota, Long merchantId) {
BenefitFreeStayVoucher voucher = voucherMapper.selectOne(new QueryWrapper<BenefitFreeStayVoucher>()
- .eq("talent_id", primaryTalentId)
+ .eq("talent_id", quota.primaryTalentId())
.eq("merchant_id", merchantId)
.last("limit 1 for update"));
LocalDate validEnd = LocalDate.now().plusYears(1);
if (voucher == null) {
voucher = new BenefitFreeStayVoucher();
voucher.setVoucherNo(BusinessIdGenerator.next("FSV"));
- voucher.setTalentId(primaryTalentId);
+ voucher.setTalentId(quota.primaryTalentId());
voucher.setMerchantId(merchantId);
- voucher.setCardId(primaryCardId);
- voucher.setTotalCount(totalCount);
- voucher.setUsedCount(0);
+ voucher.setCardId(quota.primaryCardId());
+ voucher.setTotalCount(quota.total());
+ voucher.setUsedCount(quota.used());
voucher.setValidEnd(validEnd);
voucher.setStatus(1);
voucherMapper.insert(voucher);
return voucher;
}
- if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd())) {
- voucher.setTotalCount(totalCount);
- voucher.setUsedCount(0);
- voucher.setValidEnd(validEnd);
- voucher.setStatus(1);
- voucherMapper.updateById(voucher);
- } else if (intValue(voucher.getTotalCount()) < totalCount) {
- voucher.setTotalCount(totalCount);
- voucherMapper.updateById(voucher);
- }
+ voucher.setCardId(quota.primaryCardId());
+ voucher.setTotalCount(quota.total());
+ voucher.setUsedCount(quota.used());
+ voucher.setValidEnd(validEnd);
+ voucher.setStatus(1);
+ voucherMapper.updateById(voucher);
return voucher;
}
-
private Map<String, Object> movieCodeResponse(BenefitCinemaCouponClaim claim, BenefitCinemaCoupon coupon,
BenefitQuotaService.QuotaSnapshot quota,
int dailyLimit, int dailyUsed, boolean newlyReserved) {
- LocalDateTime updatedAt = claim.getUpdatedAt() == null ? LocalDateTime.now() : claim.getUpdatedAt();
- LocalDateTime expiresAt = updatedAt.plusSeconds(CODE_SECONDS);
+ LocalDateTime expiresAt = coupon != null && coupon.getValidEnd() != null
+ ? coupon.getValidEnd().plusDays(1).atStartOfDay()
+ : LocalDateTime.now().plusYears(1);
BenefitMerchant merchant = merchantMapper.selectById(claim.getMerchantId());
Map<String, Object> result = baseResponse(BenefitQuotaService.MOVIE_TICKET, claim.getExchangeCode(), expiresAt, quota);
result.put("displayCode", claim.getExchangeCode());
@@ -367,7 +428,8 @@
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", CODE_SECONDS);
+ result.put("refreshAfterSeconds", BenefitQuotaService.HOTEL.equals(benefitType)
+ ? HOTEL_CODE_SECONDS : 0);
result.put("sharedQuotaMode", "AUTO_DEDUCT");
result.put("sourceCardId", quota.cardId());
result.put("sourceTalentId", quota.talentId());
@@ -391,11 +453,7 @@
}
private boolean activeMovieCode(BenefitCinemaCouponClaim claim) {
- return "ACTIVE".equals(upper(claim.getCodeStatus())) && !movieCodeExpired(claim.getUpdatedAt());
- }
-
- private boolean movieCodeExpired(LocalDateTime updatedAt) {
- return updatedAt != null && !updatedAt.plusSeconds(CODE_SECONDS).isAfter(LocalDateTime.now());
+ return "ACTIVE".equals(upper(claim.getCodeStatus())) && !expiredDate(claim.getValidEnd());
}
private boolean startsWithMoviePrefix(String code) {
@@ -454,4 +512,4 @@
private record MovieCouponChoice(BenefitCinemaCoupon coupon, int dailyLimit, int dailyUsed) {
}
-}
\ No newline at end of file
+}
--
Gitblit v1.7.1