feat(pricing): 增加券码重复使用功能并优化相关接口

- 在 VoucherCodeResp 和 VoucherInfo 中添加可重复使用券码相关字段
- 新增 getVoucherDetail、getVoucherUsageSummary 和 getBatchOverview接口
- 优化 calculateVoucherDiscount 接口,支持重复使用券码的计算
- 在 PriceVoucherUsageRecordMapper 中添加按券码ID和用户ID查询使用记录的方法
This commit is contained in:
2025-09-16 19:46:56 +08:00
parent 90a21c0933
commit 8380b02fbb
6 changed files with 346 additions and 0 deletions

View File

@@ -114,6 +114,10 @@ public class VoucherInfo {
* 剩余可使用次数
*/
private Integer remainingUseCount;
/**
* 是否还能使用
*/
private Boolean canUseMore;
/**
* 最后使用时间

View File

@@ -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;
}

View File

@@ -33,6 +33,17 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper<PriceVoucherUs
*/
@Select("SELECT * FROM price_voucher_usage_record WHERE voucher_code = #{voucherCode} AND deleted = 0 ORDER BY use_time DESC")
List<PriceVoucherUsageRecord> 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<PriceVoucherUsageRecord> selectByVoucherCodeAndFaceId(@Param("voucherCodeId") Long voucherCodeId, @Param("faceId") Long faceId);
/**
* 根据用户和景区查询使用记录

View File

@@ -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

View File

@@ -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());

View File

@@ -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<PriceVoucherUsageRecord> 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<PriceVoucherUsageRecord> 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<VoucherUsageSummaryResp.UsageTimeline> 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<PriceVoucherCode> 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<VoucherInfo> 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;
}