package com.talentcard.server.service;
|
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.temporal.TemporalAdjusters;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import com.talentcard.server.entity.TicketPackage;
|
import com.talentcard.server.entity.TicketPackageClaim;
|
import com.talentcard.server.entity.TicketPackageCode;
|
import com.talentcard.server.mapper.TicketPackageClaimMapper;
|
import com.talentcard.server.mapper.TicketPackageCodeMapper;
|
import com.talentcard.server.mapper.TicketPackageLevelRuleMapper;
|
import com.talentcard.server.mapper.TicketPackageMapper;
|
import com.talentcard.server.request.TicketPackageClaimRequest;
|
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@Service
|
public class TicketPackagePortalService {
|
private final TicketPackageMapper packageMapper;
|
private final TicketPackageCodeMapper codeMapper;
|
private final TicketPackageClaimMapper claimMapper;
|
private final TicketPackageLevelRuleMapper packageLevelRuleMapper;
|
|
public TicketPackagePortalService(
|
TicketPackageMapper packageMapper,
|
TicketPackageCodeMapper codeMapper,
|
TicketPackageClaimMapper claimMapper,
|
TicketPackageLevelRuleMapper packageLevelRuleMapper
|
) {
|
this.packageMapper = packageMapper;
|
this.codeMapper = codeMapper;
|
this.claimMapper = claimMapper;
|
this.packageLevelRuleMapper = packageLevelRuleMapper;
|
}
|
|
@Transactional(readOnly = true)
|
public List<Map<String, Object>> packages(Long talentId, Long cardId, Long merchantId) {
|
CardPair cards = cards(talentId, cardId, false);
|
return portalPackageViews(merchantId, cards);
|
}
|
|
@Transactional(readOnly = true)
|
public boolean hasPortalPackages(Long merchantId) {
|
return merchantId != null && merchantId > 0 && !packageMapper.selectPortalPackages(merchantId).isEmpty();
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> merchantQuota(Long talentId, Long cardId, Long merchantId) {
|
return quotaSnapshot(talentId, cardId, merchantId, null);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> packageQuota(Long talentId, Long cardId, Long merchantId, Long packageId) {
|
return quotaSnapshot(talentId, cardId, merchantId, packageId);
|
}
|
|
private Map<String, Object> quotaSnapshot(Long talentId, Long cardId, Long merchantId, Long preferredPackageId) {
|
Long id = requireId(merchantId, "影院商户ID不能为空");
|
CardPair cards = cards(talentId, cardId, false);
|
List<Map<String, Object>> packages = portalPackageViews(id, cards);
|
if (packages.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "当前影院未配置可领取票包");
|
}
|
Map<String, Object> selected = selectQuotaPackage(packages, preferredPackageId);
|
int quotaTotal = intValue(value(selected, "packageQuotaTotal", "quotaTotal"));
|
int quotaUsed = intValue(value(selected, "packageQuotaUsed", "quotaUsed"));
|
int quotaRemaining = intValue(value(selected, "packageQuotaRemaining", "quotaRemaining"));
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("benefitType", "MOVIE_TICKET");
|
result.put("merchantId", id);
|
result.put("available", booleanValue(selected.get("available")) && quotaRemaining > 0);
|
result.put("quotaTotal", quotaTotal);
|
result.put("quotaUsed", quotaUsed);
|
result.put("quotaRemaining", quotaRemaining);
|
result.put("quotaResetCycle", value(selected, "quotaResetCycle"));
|
result.put("quotaWindowStart", value(selected, "quotaWindowStart"));
|
result.put("packageId", value(selected, "packageId"));
|
result.put("packageName", text(selected, "packageName"));
|
result.put("merchantName", text(selected, "merchantName"));
|
result.put("merchantNames", text(selected, "merchantNames"));
|
result.put("appName", text(selected, "appName"));
|
result.put("couponId", value(selected, "packageId"));
|
result.put("couponName", text(selected, "packageName"));
|
result.put("totalQuantity", value(selected, "totalQuantity"));
|
result.put("remainingCount", quotaRemaining);
|
result.put("availableCodes", value(selected, "availableCodes"));
|
result.put("sourceType", "TICKET_PACKAGE");
|
result.put("sourceCardId", cards.sourceCardId());
|
result.put("sourceTalentId", cards.sourceTalentId());
|
result.put("sourceCardType", cards.sourceCardType());
|
result.put("familyCard", cards.familyCard());
|
result.put("primaryCardId", cards.primaryCardId());
|
result.put("primaryTalentId", cards.primaryTalentId());
|
result.put("primaryCardNo", cards.primaryCardNo());
|
result.put("cardLevel", cards.primaryCardLevel());
|
result.put("packages", packages.stream().map(row -> {
|
Map<String, Object> packageQuota = new LinkedHashMap<>();
|
packageQuota.put("packageId", value(row, "packageId"));
|
packageQuota.put("packageName", text(row, "packageName"));
|
packageQuota.put("quotaTotal", value(row, "packageQuotaTotal", "quotaTotal"));
|
packageQuota.put("quotaUsed", value(row, "packageQuotaUsed", "quotaUsed"));
|
packageQuota.put("quotaRemaining", value(row, "packageQuotaRemaining", "quotaRemaining"));
|
packageQuota.put("available", value(row, "available"));
|
packageQuota.put("unavailableReason", text(row, "unavailableReason"));
|
return packageQuota;
|
}).toList());
|
return result;
|
}
|
|
@Transactional
|
public Map<String, Object> claimByMerchant(Long merchantId, Long talentId, Long cardId, String idempotencyKey) {
|
Long id = requireId(merchantId, "影院商户ID不能为空");
|
String key = clean(idempotencyKey);
|
if (key.isBlank()) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "领取凭证不能为空");
|
}
|
CardPair cards = cards(talentId, cardId, false);
|
TicketPackageClaim existing = claimMapper.selectByIdempotencyKey(key);
|
if (existing != null) {
|
if (!cards.sourceTalentId().equals(existing.getTalentId())
|
|| !cards.sourceCardId().equals(existing.getCardId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他用户使用");
|
}
|
if (existing.getMerchantId() != null && !id.equals(existing.getMerchantId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他影院使用");
|
}
|
return claimView(existing, false);
|
}
|
List<Map<String, Object>> packages = portalPackageViews(id, cards);
|
if (packages.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "当前影院未配置可领取票包");
|
}
|
for (Map<String, Object> row : packages) {
|
if (booleanValue(row.get("available"))) {
|
Long packageId = longValue(row, "packageId");
|
return claim(packageId, new TicketPackageClaimRequest(
|
cards.sourceTalentId(), cards.sourceCardId(), id, key), key);
|
}
|
}
|
throw new BusinessException(ErrorCode.CONFLICT, text(packages.get(0), "unavailableReason").isBlank()
|
? "当前影院暂无可领取票包" : text(packages.get(0), "unavailableReason"));
|
}
|
|
@Transactional
|
public Map<String, Object> claim(Long packageId, TicketPackageClaimRequest request, String headerIdempotencyKey) {
|
Long id = requireId(packageId, "票包ID不能为空");
|
String idempotencyKey = clean(first(headerIdempotencyKey, request.idempotencyKey()));
|
if (idempotencyKey.isBlank()) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "领取凭证不能为空");
|
}
|
TicketPackageClaim existing = claimMapper.selectByIdempotencyKey(idempotencyKey);
|
if (existing != null) {
|
if (!id.equals(existing.getPackageId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他票包使用");
|
}
|
if (!request.talentId().equals(existing.getTalentId())
|
|| (request.cardId() != null && !request.cardId().equals(existing.getCardId()))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他用户使用");
|
}
|
if (request.merchantId() != null && existing.getMerchantId() != null
|
&& !request.merchantId().equals(existing.getMerchantId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他影院使用");
|
}
|
return claimView(existing, false);
|
}
|
|
CardPair cards = cards(request.talentId(), request.cardId(), true);
|
TicketPackage ticketPackage = packageMapper.selectByIdForUpdate(id);
|
validatePackage(ticketPackage, cards.primaryCardLevel());
|
Long actualMerchantId = claimMerchantId(ticketPackage, request.merchantId());
|
Quota quota = quota(ticketPackage.getId(), cards.primaryCardLevel());
|
int packageUsed = intValue(claimMapper.countPackageClaimedInWindow(ticketPackage.getId(), cards.primaryCardId(), quota.windowStart()));
|
if (packageUsed >= quota.total()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包可兑换额度不足");
|
}
|
|
TicketPackageCode code = codeMapper.selectAvailableCodeForUpdate(ticketPackage.getId());
|
if (code == null) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包兑换码库存不足");
|
}
|
LocalDateTime now = LocalDateTime.now();
|
int locked = codeMapper.update(null, new UpdateWrapper<TicketPackageCode>()
|
.set("status", "LOCKED")
|
.set("locked_by_talent_id", cards.sourceTalentId())
|
.set("locked_by_card_id", cards.sourceCardId())
|
.set("locked_at", now)
|
.eq("id", code.getId())
|
.eq("status", "AVAILABLE")
|
.eq("deleted", 0));
|
if (locked != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包兑换码已被领取,请重试");
|
}
|
|
TicketPackageClaim claim = new TicketPackageClaim();
|
claim.setClaimNo(BusinessIdGenerator.next("TPC"));
|
claim.setPackageId(ticketPackage.getId());
|
claim.setCodeId(code.getId());
|
claim.setMerchantId(actualMerchantId);
|
claim.setTalentId(cards.sourceTalentId());
|
claim.setCardId(cards.sourceCardId());
|
claim.setPrimaryCardId(cards.primaryCardId());
|
claim.setCardNo(cards.sourceCardNo());
|
claim.setCardLevel(cards.primaryCardLevel());
|
claim.setTalentName(cards.sourceTalentName());
|
claim.setTalentMobile(cards.sourceTalentMobile());
|
claim.setExchangeCode(code.getExchangeCode());
|
claim.setStatus("ISSUED");
|
claim.setValidEnd(ticketPackage.getValidEnd());
|
claim.setIdempotencyKey(idempotencyKey);
|
claim.setDeleted(0);
|
try {
|
claimMapper.insert(claim);
|
} catch (DuplicateKeyException exception) {
|
releaseLockedCode(code.getId());
|
TicketPackageClaim concurrent = claimMapper.selectByIdempotencyKey(idempotencyKey);
|
if (concurrent != null) {
|
if (!id.equals(concurrent.getPackageId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取凭证已被其他票包使用");
|
}
|
return claimView(concurrent, false);
|
}
|
throw new BusinessException(ErrorCode.CONFLICT, "领取请求已提交,请稍后查询领取结果");
|
} catch (RuntimeException exception) {
|
releaseLockedCode(code.getId());
|
throw exception;
|
}
|
int issued = codeMapper.update(null, new UpdateWrapper<TicketPackageCode>()
|
.set("status", "ISSUED")
|
.set("issued_claim_id", claim.getId())
|
.set("issued_at", now)
|
.eq("id", code.getId())
|
.eq("status", "LOCKED")
|
.eq("deleted", 0));
|
if (issued != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "兑换码发放失败,请重试");
|
}
|
return claimView(claim, true, quota, packageUsed + 1);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> claimDetail(Long claimId, Long talentId) {
|
Long id = requireId(claimId, "领取记录ID不能为空");
|
Long userId = requireId(talentId, "持卡人ID不能为空");
|
Map<String, Object> row = claimMapper.selectPortalDetail(id, userId);
|
if (row == null || row.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "票包领取记录不存在或无权查看");
|
}
|
Long rowPackageId = longValue(row, "packageId", "package_id");
|
Long rowPrimaryCardId = longValue(row, "primaryCardId", "primary_card_id");
|
if (rowPrimaryCardId == null || rowPrimaryCardId <= 0) {
|
rowPrimaryCardId = longValue(row, "cardId", "card_id");
|
}
|
String rowCardLevel = text(row, "cardLevel", "card_level");
|
if (rowCardLevel.isBlank() && rowPrimaryCardId != null && rowPrimaryCardId > 0) {
|
rowCardLevel = text(claimMapper.selectCardLevelSnapshot(rowPrimaryCardId), "cardLevel", "card_level");
|
}
|
Quota quota = quota(rowPackageId, rowCardLevel);
|
int used = intValue(claimMapper.countPackageClaimedInWindow(rowPackageId, rowPrimaryCardId, quota.windowStart()));
|
int totalQuantity = intValue(value(row, "totalQuantity", "total_quantity"));
|
int issuedCodes = intValue(value(row, "issuedCodes", "issued_codes"));
|
int usedCodes = intValue(value(row, "usedCodes", "used_codes"));
|
int claimedCount = intValue(value(row, "claimedCount", "claimed_count"));
|
String status = text(row, "status");
|
String codeStatus = "USED".equals(status) ? "USED" : "ACTIVE";
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("claimId", value(row, "claimId"));
|
result.put("claimNo", text(row, "claimNo"));
|
result.put("packageId", value(row, "packageId"));
|
result.put("couponName", text(row, "packageName"));
|
result.put("packageName", text(row, "packageName"));
|
result.put("merchantId", value(row, "merchantId"));
|
result.put("merchantName", text(row, "merchantName"));
|
result.put("merchantNames", text(row, "merchantNames"));
|
result.put("appName", text(row, "appName"));
|
result.put("claimStatus", status);
|
result.put("exchangeCodeStatus", codeStatus);
|
result.put("exchangeCode", "ACTIVE".equals(codeStatus) ? text(row, "exchangeCode") : "");
|
result.put("validEnd", value(row, "validEnd"));
|
result.put("usedAt", value(row, "usedAt"));
|
// 使用记录里的观影券详情要显示「领取时间」,也就是领取记录的创建时间。
|
result.put("createdAt", value(row, "createdAt"));
|
result.put("totalQuantity", totalQuantity);
|
result.put("claimedCount", claimedCount);
|
result.put("remainingCount", Math.max(totalQuantity - claimedCount, 0));
|
result.put("totalCodes", intValue(value(row, "totalCodes", "total_codes")));
|
result.put("availableCodes", intValue(value(row, "availableCodes", "available_codes")));
|
result.put("lockedCodes", intValue(value(row, "lockedCodes", "locked_codes")));
|
result.put("issuedCodes", issuedCodes);
|
result.put("usedCodes", usedCodes);
|
result.put("voidCodes", intValue(value(row, "voidCodes", "void_codes")));
|
result.put("quotaTotal", quota.total());
|
result.put("quotaUsed", used);
|
result.put("quotaRemaining", Math.max(quota.total() - used, 0));
|
result.put("packageQuotaTotal", quota.total());
|
result.put("packageQuotaUsed", used);
|
result.put("packageQuotaRemaining", Math.max(quota.total() - used, 0));
|
result.put("quotaResetCycle", quota.resetCycle());
|
result.put("annualTotal", quota.total());
|
result.put("annualClaimed", used);
|
result.put("annualRemaining", Math.max(quota.total() - used, 0));
|
result.put("channel", text(row, "appName").isBlank() ? "指定商家APP" : text(row, "appName") + "APP");
|
result.put("verificationRule", text(row, "usageText").isBlank() ? "复制兑换码后前往指定商家APP兑换电影票" : text(row, "usageText"));
|
result.put("invalidReason", "USED".equals(status) ? "兑换码已使用" : "");
|
return result;
|
}
|
|
private void releaseLockedCode(Long codeId) {
|
codeMapper.update(null, new UpdateWrapper<TicketPackageCode>()
|
.set("status", "AVAILABLE")
|
.set("locked_by_talent_id", null)
|
.set("locked_by_card_id", null)
|
.set("locked_at", null)
|
.eq("id", codeId)
|
.eq("status", "LOCKED"));
|
}
|
|
private List<Map<String, Object>> portalPackageViews(Long merchantId, CardPair cards) {
|
return packageMapper.selectPortalPackages(merchantId).stream()
|
.map(row -> portalPackageView(row, cards))
|
.toList();
|
}
|
|
private Map<String, Object> selectQuotaPackage(List<Map<String, Object>> packages, Long preferredPackageId) {
|
if (preferredPackageId != null && preferredPackageId > 0) {
|
for (Map<String, Object> row : packages) {
|
Long packageId = longValue(row, "packageId");
|
if (preferredPackageId.equals(packageId)) {
|
return row;
|
}
|
}
|
}
|
for (Map<String, Object> row : packages) {
|
if (booleanValue(row.get("available"))) {
|
return row;
|
}
|
}
|
return packages.get(0);
|
}
|
|
private Map<String, Object> portalPackageView(Map<String, Object> row, CardPair cards) {
|
Quota quota;
|
try {
|
quota = quota(longValue(row, "id"), cards.primaryCardLevel());
|
} catch (BusinessException exception) {
|
quota = new Quota(0, "", LocalDate.of(1970, 1, 1).atStartOfDay());
|
}
|
boolean supports = supportsCard(text(row, "talentLevels", "talent_levels"), cards.primaryCardLevel());
|
int totalQuantity = intValue(value(row, "totalQuantity", "total_quantity"));
|
int issuedCodes = intValue(value(row, "issuedCodes", "issued_codes"));
|
int usedCodes = intValue(value(row, "usedCodes", "used_codes"));
|
int claimedCount = intValue(value(row, "claimedCount", "claimed_count"));
|
int availableCodes = intValue(value(row, "availableCodes", "available_codes"));
|
int packageUsed = intValue(claimMapper.countPackageClaimedInWindow(longValue(row, "id"), cards.primaryCardId(), quota.windowStart()));
|
int remainingQuota = Math.max(quota.total() - packageUsed, 0);
|
boolean available = supports && availableCodes > 0 && remainingQuota > 0;
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("packageId", value(row, "id"));
|
result.put("packageNo", text(row, "packageNo", "package_no"));
|
result.put("packageName", text(row, "packageName", "package_name"));
|
result.put("merchantId", value(row, "merchantId", "merchant_id"));
|
result.put("merchantName", text(row, "merchantName", "merchant_name"));
|
result.put("merchantNames", text(row, "merchantNames", "merchant_names"));
|
result.put("coverUrl", text(row, "coverUrl", "cover_url"));
|
result.put("claimPrice", value(row, "claimPrice", "claim_price"));
|
result.put("validStart", value(row, "validStart", "valid_start"));
|
result.put("validEnd", value(row, "validEnd", "valid_end"));
|
result.put("appName", text(row, "appName", "app_name"));
|
result.put("appJumpUrl", text(row, "appJumpUrl", "app_jump_url"));
|
result.put("usageText", text(row, "usageText", "usage_text"));
|
result.put("totalQuantity", totalQuantity);
|
result.put("claimedCount", claimedCount);
|
result.put("remainingCount", Math.max(totalQuantity - claimedCount, 0));
|
result.put("totalCodes", intValue(value(row, "totalCodes", "total_codes")));
|
result.put("availableCodes", availableCodes);
|
result.put("lockedCodes", intValue(value(row, "lockedCodes", "locked_codes")));
|
result.put("issuedCodes", issuedCodes);
|
result.put("usedCodes", usedCodes);
|
result.put("voidCodes", intValue(value(row, "voidCodes", "void_codes")));
|
result.put("quotaTotal", quota.total());
|
result.put("quotaUsed", packageUsed);
|
result.put("quotaRemaining", remainingQuota);
|
result.put("packageQuotaTotal", quota.total());
|
result.put("packageQuotaUsed", packageUsed);
|
result.put("packageQuotaRemaining", remainingQuota);
|
result.put("packageLimit", quota.total());
|
result.put("packageUsed", packageUsed);
|
result.put("quotaResetCycle", quota.resetCycle());
|
result.put("quotaWindowStart", quota.windowStart());
|
result.put("available", available);
|
result.put("unavailableReason", unavailableReason(supports, availableCodes, remainingQuota));
|
return result;
|
}
|
|
private Map<String, Object> claimView(TicketPackageClaim claim, boolean newlyIssued) {
|
Quota quota = quota(claim.getPackageId(), claim.getCardLevel());
|
int used = intValue(claimMapper.countPackageClaimedInWindow(
|
claim.getPackageId(), claim.getPrimaryCardId(), quota.windowStart()));
|
return claimView(claim, newlyIssued, quota, used);
|
}
|
|
private Map<String, Object> claimView(TicketPackageClaim claim, boolean newlyIssued, Quota quota, int used) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> detail = packageMapper.selectAdminDetail(claim.getPackageId());
|
result.put("claimId", claim.getId());
|
result.put("claimNo", claim.getClaimNo());
|
result.put("packageId", claim.getPackageId());
|
result.put("packageName", text(detail, "packageName"));
|
result.put("couponName", text(detail, "packageName"));
|
result.put("merchantId", claim.getMerchantId());
|
result.put("merchantName", text(detail, "merchantName"));
|
result.put("merchantNames", text(detail, "merchantNames"));
|
result.put("appName", text(detail, "appName"));
|
result.put("talentId", claim.getTalentId());
|
result.put("cardId", claim.getCardId());
|
result.put("primaryCardId", claim.getPrimaryCardId());
|
result.put("cardNo", claim.getCardNo());
|
result.put("cardLevel", claim.getCardLevel());
|
result.put("exchangeCode", claim.getExchangeCode());
|
result.put("status", claim.getStatus());
|
result.put("statusName", "USED".equals(claim.getStatus()) ? "已使用" : "已发放");
|
result.put("validEnd", claim.getValidEnd());
|
result.put("quotaTotal", quota.total());
|
result.put("quotaUsed", used);
|
result.put("quotaRemaining", Math.max(quota.total() - used, 0));
|
result.put("packageQuotaTotal", quota.total());
|
result.put("packageQuotaUsed", used);
|
result.put("packageQuotaRemaining", Math.max(quota.total() - used, 0));
|
result.put("quotaResetCycle", quota.resetCycle());
|
result.put("quotaWindowStart", quota.windowStart());
|
result.put("annualTotal", quota.total());
|
result.put("annualClaimed", used);
|
result.put("annualRemaining", Math.max(quota.total() - used, 0));
|
result.put("newlyIssued", newlyIssued);
|
return result;
|
}
|
|
private CardPair cards(Long talentId, Long cardId, boolean lock) {
|
if ((talentId == null || talentId <= 0) && (cardId == null || cardId <= 0)) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "持卡人ID或人才卡ID不能为空");
|
}
|
Map<String, Object> source = lock ? claimMapper.selectActiveCardSnapshotForUpdate(talentId, cardId)
|
: activeCardSnapshot(talentId, cardId);
|
if (source == null || source.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "有效人才卡不存在");
|
}
|
String cardType = text(source, "cardType", "card_type").toUpperCase(Locale.ROOT);
|
if ("PERSONAL".equals(cardType) || cardType.isBlank()) {
|
return new CardPair(
|
longValue(source, "talentId", "talent_id"),
|
longValue(source, "cardId", "card_id"),
|
text(source, "cardNo", "card_no"),
|
cardType,
|
text(source, "cardLevel", "card_level"),
|
text(source, "talentName", "talent_name"),
|
text(source, "talentMobile", "talent_mobile"),
|
longValue(source, "cardId", "card_id"),
|
longValue(source, "talentId", "talent_id"),
|
text(source, "cardNo", "card_no"),
|
text(source, "cardLevel", "card_level"),
|
false
|
);
|
}
|
Map<String, Object> primary = lock ? claimMapper.selectPrimaryCardByFamilyCardForUpdate(longValue(source, "cardId", "card_id"))
|
: primaryCardSnapshot(longValue(source, "cardId", "card_id"));
|
if (primary == null || primary.isEmpty()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "亲属附属卡未找到有效主卡");
|
}
|
return new CardPair(
|
longValue(source, "talentId", "talent_id"),
|
longValue(source, "cardId", "card_id"),
|
text(source, "cardNo", "card_no"),
|
cardType,
|
text(primary, "cardLevel", "card_level"),
|
text(source, "talentName", "talent_name"),
|
text(source, "talentMobile", "talent_mobile"),
|
longValue(primary, "cardId", "card_id"),
|
longValue(primary, "talentId", "talent_id"),
|
text(primary, "cardNo", "card_no"),
|
text(primary, "cardLevel", "card_level"),
|
true
|
);
|
}
|
|
private Map<String, Object> activeCardSnapshot(Long talentId, Long cardId) {
|
return claimMapper.selectActiveCardSnapshot(talentId, cardId);
|
}
|
|
private Map<String, Object> primaryCardSnapshot(Long familyCardId) {
|
return claimMapper.selectPrimaryCardByFamilyCard(familyCardId);
|
}
|
|
private void validatePackage(TicketPackage ticketPackage, String cardLevel) {
|
if (ticketPackage == null || intValue(ticketPackage.getDeleted()) == 1) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "票包不存在");
|
}
|
if (intValue(ticketPackage.getStatus()) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包当前不可领取");
|
}
|
LocalDate today = LocalDate.now();
|
if (ticketPackage.getValidStart() != null && ticketPackage.getValidStart().isAfter(today)) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包尚未开始领取");
|
}
|
if (ticketPackage.getValidEnd() != null && ticketPackage.getValidEnd().isBefore(today)) {
|
throw new BusinessException(ErrorCode.CONFLICT, "票包已过期");
|
}
|
if (!supportsCard(ticketPackage.getTalentLevels(), cardLevel)) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不适用该票包");
|
}
|
}
|
|
private Long claimMerchantId(TicketPackage ticketPackage, Long requestMerchantId) {
|
if (requestMerchantId == null || requestMerchantId <= 0) {
|
return ticketPackage.getMerchantId();
|
}
|
if (intValue(packageMapper.countPackageMerchant(ticketPackage.getId(), requestMerchantId)) < 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前影城不适用该票包");
|
}
|
return requestMerchantId;
|
}
|
|
private Quota quota(Long packageId, String cardLevel) {
|
String normalizedLevel = normalizeCardLevel(cardLevel);
|
com.talentcard.server.entity.TicketPackageLevelRule rule = null;
|
if (!normalizedLevel.isBlank()) {
|
rule = packageLevelRuleMapper.selectOne(new QueryWrapper<com.talentcard.server.entity.TicketPackageLevelRule>()
|
.eq("package_id", packageId)
|
.eq("deleted", 0)
|
.eq("talent_level", normalizedLevel)
|
.last("limit 1"));
|
}
|
List<String> aliases = cardLevelAliases(cardLevel);
|
if (rule == null && !aliases.isEmpty()) {
|
rule = packageLevelRuleMapper.selectOne(new QueryWrapper<com.talentcard.server.entity.TicketPackageLevelRule>()
|
.eq("package_id", packageId)
|
.eq("deleted", 0)
|
.in("talent_level", aliases)
|
.last("limit 1"));
|
}
|
if (rule == null) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型未配置该票包可兑换额度");
|
}
|
if (intValue(rule.getEnabled()) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型未启用该票包权益");
|
}
|
int total = intValue(rule.getClaimableCount());
|
if (total < 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型未配置该票包可兑换额度");
|
}
|
String resetCycle = clean(rule.getResetCycle());
|
return new Quota(total, resetCycle, windowStart(resetCycle));
|
}
|
|
private String normalizeCardLevel(String cardLevel) {
|
String raw = clean(cardLevel);
|
String upper = raw.toUpperCase(Locale.ROOT);
|
if (upper.contains("EXCELLENT") || raw.contains("卓越")) {
|
return "卓越人才卡";
|
}
|
if (upper.contains("OUTSTANDING") || raw.contains("杰出")) {
|
return "杰出人才卡";
|
}
|
if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) {
|
return "菁英人才卡";
|
}
|
return raw;
|
}
|
|
private List<String> cardLevelAliases(String cardLevel) {
|
String raw = clean(cardLevel);
|
String upper = raw.toUpperCase(Locale.ROOT);
|
if (upper.contains("EXCELLENT") || raw.contains("卓越")) {
|
return List.of(raw, "卓越人才卡", "卓越", "EXCELLENT");
|
}
|
if (upper.contains("OUTSTANDING") || raw.contains("杰出")) {
|
return List.of(raw, "杰出人才卡", "杰出", "OUTSTANDING");
|
}
|
if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) {
|
return List.of(raw, "菁英人才卡", "菁英", "精英人才卡", "精英", "ELITE");
|
}
|
return List.of(raw);
|
}
|
|
private LocalDateTime windowStart(String resetCycle) {
|
String cycle = clean(resetCycle).toUpperCase(Locale.ROOT);
|
LocalDate today = LocalDate.now();
|
if (cycle.isBlank() || "NONE".equals(cycle)) {
|
return LocalDate.of(1970, 1, 1).atStartOfDay();
|
}
|
if (cycle.contains("DAY") || cycle.contains("天") || cycle.contains("日")) {
|
return today.atStartOfDay();
|
}
|
if (cycle.contains("WEEK") || cycle.contains("周") || cycle.contains("星期")) {
|
return today.minusDays(today.getDayOfWeek().getValue() - 1L).atStartOfDay();
|
}
|
if (cycle.contains("MONTH") || cycle.contains("月")) {
|
return today.withDayOfMonth(1).atStartOfDay();
|
}
|
if (cycle.contains("YEAR") || cycle.contains("ANNUAL") || cycle.contains("年")) {
|
return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
|
}
|
return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
|
}
|
|
private boolean supportsCard(String configured, String cardLevel) {
|
String value = clean(configured);
|
String level = clean(cardLevel);
|
if (value.isBlank()) {
|
return true;
|
}
|
if (level.isBlank()) {
|
return false;
|
}
|
return value.contains(level) || cardLevelAliases(level).stream()
|
.filter(alias -> !alias.isBlank())
|
.anyMatch(value::contains);
|
}
|
|
private String unavailableReason(boolean supports, int availableCodes, int remainingQuota) {
|
if (!supports) {
|
return "当前人才卡不适用该票包";
|
}
|
if (availableCodes < 1) {
|
return "票包兑换码库存不足";
|
}
|
if (remainingQuota < 1) {
|
return "票包可兑换额度不足";
|
}
|
return "";
|
}
|
|
private Long requireId(Long id, String message) {
|
if (id == null || id <= 0) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, message);
|
}
|
return id;
|
}
|
|
private String first(String... values) {
|
for (String value : values) {
|
String text = clean(value);
|
if (!text.isBlank()) {
|
return text;
|
}
|
}
|
return "";
|
}
|
|
private Object value(Map<String, Object> row, String... keys) {
|
if (row == null) {
|
return null;
|
}
|
for (String key : keys) {
|
if (row.containsKey(key)) {
|
return row.get(key);
|
}
|
}
|
return null;
|
}
|
|
private String text(Map<String, Object> row, String... keys) {
|
Object value = value(row, keys);
|
return value == null ? "" : String.valueOf(value).trim();
|
}
|
|
private Long longValue(Map<String, Object> row, String... keys) {
|
Object value = value(row, keys);
|
return value instanceof Number number ? number.longValue() : null;
|
}
|
|
private int intValue(Object value) {
|
return value instanceof Number number ? number.intValue() : 0;
|
}
|
|
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 String clean(String value) {
|
return value == null ? "" : value.trim();
|
}
|
|
private record CardPair(
|
Long sourceTalentId,
|
Long sourceCardId,
|
String sourceCardNo,
|
String sourceCardType,
|
String primaryCardLevel,
|
String sourceTalentName,
|
String sourceTalentMobile,
|
Long primaryCardId,
|
Long primaryTalentId,
|
String primaryCardNo,
|
String primaryCardLevelSnapshot,
|
boolean familyCard
|
) {
|
}
|
|
private record Quota(int total, String resetCycle, LocalDateTime windowStart) {
|
}
|
}
|