From 8380b02fbbbb39c2c131282171fe2bd58f706b1c Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 16 Sep 2025 19:46:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=E5=A2=9E=E5=8A=A0=E5=88=B8?= =?UTF-8?q?=E7=A0=81=E9=87=8D=E5=A4=8D=E4=BD=BF=E7=94=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 VoucherCodeResp 和 VoucherInfo 中添加可重复使用券码相关字段 - 新增 getVoucherDetail、getVoucherUsageSummary 和 getBatchOverview接口 - 优化 calculateVoucherDiscount 接口,支持重复使用券码的计算 - 在 PriceVoucherUsageRecordMapper 中添加按券码ID和用户ID查询使用记录的方法 --- .../ycwl/basic/pricing/dto/VoucherInfo.java | 4 + .../pricing/dto/resp/VoucherCodeResp.java | 26 ++ .../mapper/PriceVoucherUsageRecordMapper.java | 11 + .../pricing/service/IVoucherService.java | 25 ++ .../service/impl/VoucherCodeServiceImpl.java | 18 ++ .../service/impl/VoucherServiceImpl.java | 262 ++++++++++++++++++ 6 files changed, 346 insertions(+) diff --git a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java index 88f7ce89..b69a69d2 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java @@ -114,6 +114,10 @@ public class VoucherInfo { * 剩余可使用次数 */ private Integer remainingUseCount; + /** + * 是否还能使用 + */ + private Boolean canUseMore; /** * 最后使用时间 diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java index b7f0f8ad..ee9de462 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java @@ -24,4 +24,30 @@ public class VoucherCodeResp { private String discountTypeName; private String discountDescription; private BigDecimal discountValue; + + // 可重复使用券码相关字段 + /** + * 当前使用次数 + */ + private Integer currentUseCount; + + /** + * 最大使用次数 + */ + private Integer maxUseCount; + + /** + * 剩余使用次数 + */ + private Integer remainingUseCount; + + /** + * 是否还能使用 + */ + private Boolean canUseMore; + + /** + * 最后使用时间 + */ + private Date lastUsedTime; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java index 66dc111e..01a58c1a 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java @@ -33,6 +33,17 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper selectByVoucherCode(@Param("voucherCode") String voucherCode); + + + /** + * 根据券码ID和用户ID查询使用记录 + * + * @param voucherCodeId 券码ID + * @param faceId 用户faceId + * @return 使用记录列表 + */ + @Select("SELECT * FROM price_voucher_usage_record WHERE voucher_code_id = #{voucherCodeId} AND face_id = #{faceId} AND deleted = 0 ORDER BY use_time DESC") + List selectByVoucherCodeAndFaceId(@Param("voucherCodeId") Long voucherCodeId, @Param("faceId") Long faceId); /** * 根据用户和景区查询使用记录 diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java index 4f05658c..ee2179ee 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -2,6 +2,9 @@ package com.ycwl.basic.pricing.service; import com.ycwl.basic.pricing.dto.DiscountDetectionContext; import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchOverviewResp; +import com.ycwl.basic.pricing.dto.resp.VoucherDetailResp; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageSummaryResp; import java.math.BigDecimal; import java.util.List; @@ -61,6 +64,28 @@ public interface IVoucherService { */ BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context); + /** + * 获取券码详细信息 + * @param voucherCode 券码 + * @param faceId 用户人脸ID(可选) + * @return 券码详细信息 + */ + VoucherDetailResp getVoucherDetail(String voucherCode, Long faceId); + + /** + * 获取券码使用统计摘要 + * @param voucherCode 券码 + * @return 券码使用统计摘要 + */ + VoucherUsageSummaryResp getVoucherUsageSummary(String voucherCode); + + /** + * 获取批次券码概览 + * @param batchId 批次ID + * @return 批次概览信息 + */ + VoucherBatchOverviewResp getBatchOverview(Long batchId); + /** * 获取最优的券码(如果用户有多个可用券码) * @param faceId 用户faceId diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index d1a90a65..b4077399 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -257,6 +257,24 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { resp.setDiscountTypeName(discountType.getName()); resp.setDiscountDescription(discountType.getDescription()); } + + // 设置可重复使用相关字段 + Integer currentUseCount = code.getCurrentUseCount() != null ? code.getCurrentUseCount() : 0; + resp.setCurrentUseCount(currentUseCount); + resp.setMaxUseCount(batch.getMaxUseCount()); + resp.setLastUsedTime(code.getLastUsedTime()); + + // 计算剩余使用次数 + if (batch.getMaxUseCount() != null) { + int remaining = batch.getMaxUseCount() - currentUseCount; + resp.setRemainingUseCount(Math.max(0, remaining)); + + // 计算是否还能使用 + resp.setCanUseMore(currentUseCount < batch.getMaxUseCount()); + } else { + resp.setRemainingUseCount(null); // 无限制 + resp.setCanUseMore(true); + } } VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus()); diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java index c5612a5b..a2860d93 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java @@ -1,5 +1,34 @@ package com.ycwl.basic.pricing.service.impl; +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.dto.resp.VoucherDetailResp; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageSummaryResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchOverviewResp; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherUsageRecordMapper; +import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import com.ycwl.basic.pricing.dto.DiscountDetectionContext; import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.dto.VoucherInfo; @@ -336,6 +365,232 @@ public class VoucherServiceImpl implements IVoucherService { }; } + @Override + public VoucherDetailResp getVoucherDetail(String voucherCode, Long faceId) { + if (!StringUtils.hasText(voucherCode)) { + return null; + } + + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + return null; + } + + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1) { + return null; + } + + VoucherDetailResp detail = new VoucherDetailResp(); + detail.setVoucherCode(voucherCode); + + // 设置批次信息 + VoucherDetailResp.BatchInfo batchInfo = new VoucherDetailResp.BatchInfo(); + batchInfo.setBatchId(batchConfig.getId()); + batchInfo.setBatchName(batchConfig.getBatchName()); + batchInfo.setDiscountType(batchConfig.getDiscountType()); + batchInfo.setDiscountValue(batchConfig.getDiscountValue()); + batchInfo.setApplicableProducts(batchConfig.getApplicableProducts()); + detail.setBatchInfo(batchInfo); + + // 设置使用信息 + VoucherDetailResp.UsageInfo usageInfo = new VoucherDetailResp.UsageInfo(); + Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? voucherCodeEntity.getCurrentUseCount() : 0; + usageInfo.setMaxUseCount(batchConfig.getMaxUseCount()); + usageInfo.setCurrentUseCount(currentUseCount); + usageInfo.setMaxUsePerUser(batchConfig.getMaxUsePerUser()); + usageInfo.setUseIntervalHours(batchConfig.getUseIntervalHours()); + + // 计算剩余使用次数和是否还能使用 + if (batchConfig.getMaxUseCount() != null) { + int remaining = batchConfig.getMaxUseCount() - currentUseCount; + usageInfo.setRemainingUseCount(Math.max(0, remaining)); + usageInfo.setCanUseMore(currentUseCount < batchConfig.getMaxUseCount()); + } else { + usageInfo.setRemainingUseCount(null); + usageInfo.setCanUseMore(true); + } + detail.setUsageInfo(usageInfo); + + // 设置用户信息 + if (faceId != null) { + VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo(); + userInfo.setFaceId(faceId); + + // 计算该用户使用此券码的次数 + List userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId); + userInfo.setUserUsageCount(userUsageRecords.size()); + + if (voucherCodeEntity.getLastUsedTime() != null) { + userInfo.setLastUsedTime(voucherCodeEntity.getLastUsedTime()); + + // 计算下次可用时间 + if (batchConfig.getUseIntervalHours() != null) { + Date nextAvailableTime = new Date(voucherCodeEntity.getLastUsedTime().getTime() + + batchConfig.getUseIntervalHours() * 60 * 60 * 1000L); + userInfo.setNextAvailableTime(nextAvailableTime); + } + } + detail.setUserInfo(userInfo); + } + + // 设置状态信息 + VoucherDetailResp.StatusInfo statusInfo = new VoucherDetailResp.StatusInfo(); + statusInfo.setStatus(voucherCodeEntity.getStatus()); + VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCodeEntity.getStatus()); + if (statusEnum != null) { + statusInfo.setStatusName(statusEnum.getName()); + } + statusInfo.setClaimedTime(voucherCodeEntity.getClaimedTime()); + detail.setStatusInfo(statusInfo); + + return detail; + } + + @Override + public VoucherUsageSummaryResp getVoucherUsageSummary(String voucherCode) { + if (!StringUtils.hasText(voucherCode)) { + return null; + } + + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + return null; + } + + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1) { + return null; + } + + VoucherUsageSummaryResp summary = new VoucherUsageSummaryResp(); + summary.setVoucherCode(voucherCode); + summary.setBatchId(batchConfig.getId()); + summary.setBatchName(batchConfig.getBatchName()); + + // 获取使用记录 + List usageRecords = usageRecordMapper.selectByVoucherCodeId(voucherCodeEntity.getId()); + summary.setTotalUsageRecords(usageRecords.size()); + + // 计算独立用户数 + long uniqueUsers = usageRecords.stream() + .mapToLong(PriceVoucherUsageRecord::getFaceId) + .distinct() + .count(); + summary.setUniqueUsers((int) uniqueUsers); + + // 设置使用统计 + VoucherUsageSummaryResp.UsageStatistics statistics = new VoucherUsageSummaryResp.UsageStatistics(); + Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? voucherCodeEntity.getCurrentUseCount() : 0; + statistics.setMaxUseCount(batchConfig.getMaxUseCount()); + statistics.setCurrentUseCount(currentUseCount); + + if (batchConfig.getMaxUseCount() != null) { + int remaining = batchConfig.getMaxUseCount() - currentUseCount; + statistics.setRemainingUseCount(Math.max(0, remaining)); + statistics.setCanUseMore(currentUseCount < batchConfig.getMaxUseCount()); + + // 计算使用率 + if (batchConfig.getMaxUseCount() > 0) { + BigDecimal usageRate = new BigDecimal(currentUseCount) + .divide(new BigDecimal(batchConfig.getMaxUseCount()), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)); + statistics.setUsageRate(usageRate); + } + } else { + statistics.setRemainingUseCount(null); + statistics.setCanUseMore(true); + statistics.setUsageRate(null); + } + summary.setUsageStatistics(statistics); + + // 设置使用时间轴 + List timeline = usageRecords.stream() + .map(record -> { + VoucherUsageSummaryResp.UsageTimeline item = new VoucherUsageSummaryResp.UsageTimeline(); + item.setUseTime(record.getUseTime()); + item.setFaceId(record.getFaceId()); + item.setOrderId(record.getOrderId()); + item.setDiscountAmount(record.getDiscountAmount()); + return item; + }) + .collect(Collectors.toList()); + summary.setUsageTimeline(timeline); + + return summary; + } + + @Override + public VoucherBatchOverviewResp getBatchOverview(Long batchId) { + if (batchId == null) { + return null; + } + + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(batchId); + if (batchConfig == null || batchConfig.getDeleted() == 1) { + return null; + } + + VoucherBatchOverviewResp overview = new VoucherBatchOverviewResp(); + overview.setBatchId(batchId); + overview.setBatchName(batchConfig.getBatchName()); + overview.setTotalCodes(batchConfig.getTotalCount()); + overview.setClaimedCodes(batchConfig.getClaimedCount()); + + // 查询批次下的券码统计 + List allCodes = voucherCodeMapper.selectByBatchId(batchId); + + // 计算已使用券码数(至少使用过一次) + int usedCodes = (int) allCodes.stream() + .filter(code -> code.getCurrentUseCount() != null && code.getCurrentUseCount() > 0) + .count(); + overview.setUsedCodes(usedCodes); + + // 计算已达到最大使用次数的券码数 + int exhaustedCodes = 0; + if (batchConfig.getMaxUseCount() != null) { + exhaustedCodes = (int) allCodes.stream() + .filter(code -> code.getCurrentUseCount() != null && + code.getCurrentUseCount() >= batchConfig.getMaxUseCount()) + .count(); + } + overview.setExhaustedCodes(exhaustedCodes); + + // 设置总使用记录数 + overview.setTotalUsageRecords(batchConfig.getUsedCount()); + + // 计算平均每个券码使用次数 + if (usedCodes > 0) { + BigDecimal averageUsage = new BigDecimal(batchConfig.getUsedCount()) + .divide(new BigDecimal(usedCodes), 2, RoundingMode.HALF_UP); + overview.setAverageUsagePerCode(averageUsage); + } else { + overview.setAverageUsagePerCode(BigDecimal.ZERO); + } + + // 计算重复使用率 + if (usedCodes > 0) { + int repeatUsedCodes = (int) allCodes.stream() + .filter(code -> code.getCurrentUseCount() != null && code.getCurrentUseCount() > 1) + .count(); + BigDecimal repeatUsageRate = new BigDecimal(repeatUsedCodes) + .divide(new BigDecimal(usedCodes), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)); + overview.setRepeatUsageRate(repeatUsageRate); + } else { + overview.setRepeatUsageRate(BigDecimal.ZERO); + } + + // 设置可重复使用配置 + VoucherBatchOverviewResp.RepeatableSettings settings = new VoucherBatchOverviewResp.RepeatableSettings(); + settings.setMaxUseCount(batchConfig.getMaxUseCount()); + settings.setMaxUsePerUser(batchConfig.getMaxUsePerUser()); + settings.setUseIntervalHours(batchConfig.getUseIntervalHours()); + overview.setRepeatableSettings(settings); + + return overview; + } + @Override public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) { List availableVouchers = getAvailableVouchers(faceId, scenicId); @@ -467,6 +722,13 @@ public class VoucherServiceImpl implements IVoucherService { voucherInfo.setRemainingUseCount(null); // 无限次 } + // 计算是否还能使用 + boolean canUseMore = true; + if (batchConfig.getMaxUseCount() != null) { + canUseMore = currentUseCount < batchConfig.getMaxUseCount(); + } + voucherInfo.setCanUseMore(canUseMore); + return voucherInfo; }