feat(pricing): 增加券码重复使用功能并优化相关数据结构

- 在 PriceVoucherUsageRecord 和 VoucherUsageRecordResp 中添加 usageSequence 字段,用于记录券码的使用序号- 更新 PriceVoucherCode 实体和相关 mapper,增加 currentUseCount 和 lastUsedTime 字段
- 修改 VoucherCodeServiceImpl 和 VoucherServiceImpl 中的券码使用逻辑,支持重复使用
- 新增VoucherBatchOverviewResp、VoucherDetailResp、VoucherUsageSummaryResp 和 VoucherValidationResp 等新的响应 DTO 类,用于提供券码批次概览、详情、使用统计和验证等功能
This commit is contained in:
2025-09-16 20:54:37 +08:00
parent 8380b02fbb
commit 1506ae95b8
10 changed files with 536 additions and 175 deletions

View File

@@ -49,74 +49,79 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
private VoucherBatchService voucherBatchService;
@Override
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
List<PriceVoucherCode> codes = new ArrayList<>();
for (int i = 0; i < count; i++) {
PriceVoucherCode code = new PriceVoucherCode();
code.setBatchId(batchId);
code.setScenicId(scenicId);
code.setCode(generateVoucherCode());
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
code.setCreateTime(new Date());
code.setDeleted(0);
codes.add(code);
}
for (PriceVoucherCode code : codes) {
voucherCodeMapper.insert(code);
}
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
List<PriceVoucherCode> codes = new ArrayList<>();
for (int i = 0; i < count; i++) {
PriceVoucherCode code = new PriceVoucherCode();
code.setBatchId(batchId);
code.setScenicId(scenicId);
code.setCode(generateVoucherCode());
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
code.setCurrentUseCount(0); // 初始化使用次数为0
code.setCreateTime(new Date());
code.setDeleted(0);
codes.add(code);
}
@Override
@Transactional
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
if (req.getScenicId() == null) {
throw new BizException(400, "景区ID不能为空");
}
if (req.getFaceId() == null) {
throw new BizException(400, "用户faceId不能为空");
}
if (!StringUtils.hasText(req.getCode())) {
throw new BizException(400, "券码不能为空");
}
// 验证券码是否存在且未被领取
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
.eq(PriceVoucherCode::getDeleted, 0);
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
if (voucherCode == null) {
throw new BizException(400, "券码不存在或不属于该景区");
}
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
throw new BizException(400, "券码已被领取或已使用");
}
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
throw new BizException(400, "该用户在此景区已领取过券码");
}
// 获取券码所属批次
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
if (batch == null || batch.getDeleted() == 1) {
throw new BizException(400, "券码批次不存在");
}
// 更新券码状态
voucherCode.setFaceId(req.getFaceId());
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
voucherCode.setClaimedTime(new Date());
voucherCodeMapper.updateById(voucherCode);
voucherBatchService.updateBatchClaimedCount(batch.getId());
return convertToResp(voucherCode, batch);
for (PriceVoucherCode code : codes) {
voucherCodeMapper.insert(code);
}
}
@Override
@Transactional
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
if (req.getScenicId() == null) {
throw new BizException(400, "景区ID不能为空");
}
if (req.getFaceId() == null) {
throw new BizException(400, "用户faceId不能为空");
}
if (!StringUtils.hasText(req.getCode())) {
throw new BizException(400, "券码不能为空");
}
// 验证券码是否存在且未被领取
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
.eq(PriceVoucherCode::getDeleted, 0);
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
if (voucherCode == null) {
throw new BizException(400, "券码不存在或不属于该景区");
}
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
throw new BizException(400, "券码已被领取或已使用");
}
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
throw new BizException(400, "该用户在此景区已领取过券码");
}
// 获取券码所属批次
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
if (batch == null || batch.getDeleted() == 1) {
throw new BizException(400, "券码批次不存在");
}
// 更新券码状态
voucherCode.setFaceId(req.getFaceId());
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
voucherCode.setClaimedTime(new Date());
// 确保currentUseCount被初始化
if (voucherCode.getCurrentUseCount() == null) {
voucherCode.setCurrentUseCount(0);
}
voucherCodeMapper.updateById(voucherCode);
voucherBatchService.updateBatchClaimedCount(batch.getId());
return convertToResp(voucherCode, batch);
}
@Override
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
@@ -165,26 +170,31 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
}
@Override
@Transactional
public void markCodeAsUsed(Long codeId, String remark) {
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
if (code == null || code.getDeleted() == 1) {
throw new BizException(404, "券码不存在");
}
if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) {
throw new BizException(400, "券码状态异常,无法使用");
}
code.setStatus(VoucherCodeStatus.USED.getCode());
code.setUsedTime(new Date());
code.setRemark(remark);
voucherCodeMapper.updateById(code);
voucherBatchService.updateBatchUsedCount(code.getBatchId());
@Transactional
public void markCodeAsUsed(Long codeId, String remark) {
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
if (code == null || code.getDeleted() == 1) {
throw new BizException(404, "券码不存在");
}
if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) {
throw new BizException(400, "券码状态异常,无法使用");
}
// 更新使用计数和时间
Integer currentUseCount = code.getCurrentUseCount() != null ? code.getCurrentUseCount() : 0;
code.setCurrentUseCount(currentUseCount + 1);
code.setLastUsedTime(new Date());
code.setStatus(VoucherCodeStatus.USED.getCode());
code.setUsedTime(new Date());
code.setRemark(remark);
voucherCodeMapper.updateById(code);
voucherBatchService.updateBatchUsedCount(code.getBatchId());
}
@Override
public boolean canClaimVoucher(Long faceId, Long scenicId) {
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);

View File

@@ -181,78 +181,82 @@ public class VoucherServiceImpl implements IVoucherService {
}
/**
* 标记券码为已使用(支持可重复使用)
*
* @param voucherCode 券码
* @param remark 使用备注
* @param orderId 订单ID
* @param discountAmount 优惠金额
* @param faceId 人脸ID
*/
@Override
public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId) {
if (!StringUtils.hasText(voucherCode)) {
return;
}
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
log.warn("券码不存在或已删除: {}", voucherCode);
return;
}
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
if (batchConfig == null || batchConfig.getDeleted() == 1) {
log.warn("券码批次不存在或已删除: batchId={}", voucherCodeEntity.getBatchId());
return;
}
Date now = new Date();
// 创建使用记录
PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord();
usageRecord.setVoucherCodeId(voucherCodeEntity.getId());
usageRecord.setVoucherCode(voucherCode);
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId());
usageRecord.setScenicId(voucherCodeEntity.getScenicId());
usageRecord.setBatchId(voucherCodeEntity.getBatchId());
usageRecord.setUseTime(now);
usageRecord.setOrderId(orderId);
usageRecord.setDiscountAmount(discountAmount);
usageRecord.setRemark(remark);
usageRecord.setCreateTime(now);
usageRecord.setDeleted(0);
usageRecordMapper.insert(usageRecord);
// 更新券码使用次数和状态
Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ?
voucherCodeEntity.getCurrentUseCount() + 1 : 1;
voucherCodeEntity.setCurrentUseCount(currentUseCount);
voucherCodeEntity.setLastUsedTime(now);
// 检查是否达到最大使用次数
Integer maxUseCount = batchConfig.getMaxUseCount();
if (maxUseCount != null && currentUseCount >= maxUseCount) {
if (maxUseCount == 1) {
// 兼容原有逻辑,单次使用设为USED状态
voucherCodeEntity.setStatus(VoucherCodeStatus.USED.getCode());
voucherCodeEntity.setUsedTime(now);
} else {
// 多次使用达到上限设为CLAIMED_EXHAUSTED状态
voucherCodeEntity.setStatus(VoucherCodeStatus.CLAIMED_EXHAUSTED.getCode());
}
}
voucherCodeMapper.updateById(voucherCodeEntity);
// 更新批次统计(使用记录数,不是使用券码数)
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}, faceId={}",
voucherCode, currentUseCount, maxUseCount, faceId);
* 标记券码为已使用(支持可重复使用)
*
* @param voucherCode 券码
* @param remark 使用备注
* @param orderId 订单ID
* @param discountAmount 优惠金额
* @param faceId 人脸ID
*/
@Override
public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId) {
if (!StringUtils.hasText(voucherCode)) {
return;
}
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
log.warn("券码不存在或已删除: {}", voucherCode);
return;
}
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
if (batchConfig == null || batchConfig.getDeleted() == 1) {
log.warn("券码批次不存在或已删除: batchId={}", voucherCodeEntity.getBatchId());
return;
}
Date now = new Date();
// 计算新的使用次数和序号
Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ?
voucherCodeEntity.getCurrentUseCount() : 0;
Integer newUseCount = currentUseCount + 1;
// 创建使用记录
PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord();
usageRecord.setVoucherCodeId(voucherCodeEntity.getId());
usageRecord.setVoucherCode(voucherCode);
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId());
usageRecord.setScenicId(voucherCodeEntity.getScenicId());
usageRecord.setBatchId(voucherCodeEntity.getBatchId());
usageRecord.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用
usageRecord.setUseTime(now);
usageRecord.setOrderId(orderId);
usageRecord.setDiscountAmount(discountAmount);
usageRecord.setRemark(remark);
usageRecord.setCreateTime(now);
usageRecord.setDeleted(0);
usageRecordMapper.insert(usageRecord);
// 更新券码使用次数和状态
voucherCodeEntity.setCurrentUseCount(newUseCount);
voucherCodeEntity.setLastUsedTime(now);
// 检查是否达到最大使用次数
Integer maxUseCount = batchConfig.getMaxUseCount();
if (maxUseCount != null && newUseCount >= maxUseCount) {
if (maxUseCount == 1) {
// 兼容原有逻辑,单次使用设为USED状态
voucherCodeEntity.setStatus(VoucherCodeStatus.USED.getCode());
voucherCodeEntity.setUsedTime(now);
} else {
// 多次使用达到上限设为CLAIMED_EXHAUSTED状态
voucherCodeEntity.setStatus(VoucherCodeStatus.CLAIMED_EXHAUSTED.getCode());
}
}
voucherCodeMapper.updateById(voucherCodeEntity);
// 更新批次统计(使用记录数,不是使用券码数)
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}, faceId={}, sequence={}",
voucherCode, newUseCount, maxUseCount, faceId, newUseCount);
}
@Override
public boolean canClaimVoucher(Long faceId, Long scenicId) {
if (faceId == null || scenicId == null) {