You've already forked FrameTour-BE
feat(pricing): 添加券码管理和使用功能
- 新增券码批次配置和券码实体 - 实现券码创建、领取、使用等接口 - 添加券码状态和优惠类型枚举 - 优化价格计算逻辑,支持券码优惠 - 新增优惠检测和应用相关功能
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券折扣提供者
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CouponDiscountProvider implements IDiscountProvider {
|
||||
|
||||
private final ICouponService couponService;
|
||||
|
||||
@Override
|
||||
public String getProviderType() {
|
||||
return "COUPON";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 80; // 优惠券优先级为80,低于券码的100
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||
List<DiscountInfo> discounts = new ArrayList<>();
|
||||
|
||||
if (!Boolean.TRUE.equals(context.getAutoUseCoupon()) || context.getUserId() == null) {
|
||||
return discounts;
|
||||
}
|
||||
|
||||
try {
|
||||
CouponInfo bestCoupon = couponService.selectBestCoupon(
|
||||
context.getUserId(),
|
||||
context.getProducts(),
|
||||
context.getCurrentAmount()
|
||||
);
|
||||
|
||||
if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
DiscountInfo discountInfo = new DiscountInfo();
|
||||
discountInfo.setDiscountId(bestCoupon.getCouponId());
|
||||
discountInfo.setDiscountType("COUPON");
|
||||
discountInfo.setDiscountName(bestCoupon.getCouponName());
|
||||
discountInfo.setDiscountDescription("优惠券减免");
|
||||
discountInfo.setDiscountAmount(bestCoupon.getActualDiscountAmount());
|
||||
discountInfo.setProviderType(getProviderType());
|
||||
discountInfo.setPriority(getPriority());
|
||||
discountInfo.setStackable(true); // 优惠券可与券码叠加
|
||||
discountInfo.setCouponId(bestCoupon.getCouponId());
|
||||
|
||||
discounts.add(discountInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("检测优惠券时发生异常", e);
|
||||
}
|
||||
|
||||
return discounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||
DiscountResult result = new DiscountResult();
|
||||
result.setDiscountInfo(discountInfo);
|
||||
|
||||
try {
|
||||
// 应用优惠券逻辑
|
||||
BigDecimal actualDiscount = discountInfo.getDiscountAmount();
|
||||
BigDecimal finalAmount = context.getCurrentAmount().subtract(actualDiscount);
|
||||
|
||||
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
finalAmount = BigDecimal.ZERO;
|
||||
actualDiscount = context.getCurrentAmount();
|
||||
}
|
||||
|
||||
result.setActualDiscountAmount(actualDiscount);
|
||||
result.setFinalAmount(finalAmount);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("成功应用优惠券: {}, 优惠金额: {}", discountInfo.getDiscountName(), actualDiscount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("应用优惠券失败: " + discountInfo.getDiscountName(), e);
|
||||
result.setSuccess(false);
|
||||
result.setFailureReason("优惠券应用失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||
return "COUPON".equals(discountInfo.getDiscountType()) &&
|
||||
Boolean.TRUE.equals(context.getAutoUseCoupon()) &&
|
||||
context.getUserId() != null;
|
||||
}
|
||||
}
|
@@ -13,6 +13,8 @@ import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -144,13 +146,13 @@ public class CouponServiceImpl implements ICouponService {
|
||||
throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限");
|
||||
}
|
||||
|
||||
LocalDateTime useTime = LocalDateTime.now();
|
||||
Date useTime = new Date();
|
||||
|
||||
// 设置使用时间、订单信息和景区信息
|
||||
record.setStatus(CouponStatus.USED);
|
||||
record.setUseTime(useTime);
|
||||
record.setOrderId(request.getOrderId());
|
||||
record.setUpdatedTime(LocalDateTime.now());
|
||||
record.setUpdatedTime(new Date());
|
||||
|
||||
// 如果请求中包含景区ID,记录到使用记录中
|
||||
if (request.getScenicId() != null && !request.getScenicId().isEmpty()) {
|
||||
|
@@ -0,0 +1,206 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.service.IDiscountDetectionService;
|
||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 优惠检测服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
||||
|
||||
private final List<IDiscountProvider> discountProviders = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
public DiscountDetectionServiceImpl(List<IDiscountProvider> providers) {
|
||||
this.discountProviders.addAll(providers);
|
||||
// 按优先级排序(优先级高的在前)
|
||||
this.discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
||||
|
||||
log.info("注册了 {} 个优惠提供者: {}",
|
||||
providers.size(),
|
||||
providers.stream().map(IDiscountProvider::getProviderType).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiscountInfo> detectAllAvailableDiscounts(DiscountDetectionContext context) {
|
||||
List<DiscountInfo> allDiscounts = new ArrayList<>();
|
||||
|
||||
for (IDiscountProvider provider : discountProviders) {
|
||||
try {
|
||||
List<DiscountInfo> providerDiscounts = provider.detectAvailableDiscounts(context);
|
||||
if (providerDiscounts != null && !providerDiscounts.isEmpty()) {
|
||||
allDiscounts.addAll(providerDiscounts);
|
||||
log.debug("提供者 {} 检测到 {} 个优惠", provider.getProviderType(), providerDiscounts.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("优惠提供者 {} 检测失败", provider.getProviderType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级排序
|
||||
allDiscounts.sort(Comparator.comparing(DiscountInfo::getPriority).reversed());
|
||||
|
||||
return allDiscounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context) {
|
||||
DiscountCombinationResult result = new DiscountCombinationResult();
|
||||
result.setOriginalAmount(context.getCurrentAmount());
|
||||
|
||||
try {
|
||||
List<DiscountInfo> availableDiscounts = detectAllAvailableDiscounts(context);
|
||||
result.setAvailableDiscounts(availableDiscounts);
|
||||
|
||||
if (availableDiscounts.isEmpty()) {
|
||||
result.setFinalAmount(context.getCurrentAmount());
|
||||
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||
result.setAppliedDiscounts(new ArrayList<>());
|
||||
result.setDiscountDetails(new ArrayList<>());
|
||||
result.setSuccess(true);
|
||||
return result;
|
||||
}
|
||||
|
||||
List<DiscountResult> appliedDiscounts = new ArrayList<>();
|
||||
List<DiscountDetail> discountDetails = new ArrayList<>();
|
||||
BigDecimal currentAmount = context.getCurrentAmount();
|
||||
|
||||
// 按优先级应用优惠
|
||||
for (DiscountInfo discountInfo : availableDiscounts) {
|
||||
IDiscountProvider provider = findProvider(discountInfo.getProviderType());
|
||||
if (provider == null || !provider.canApply(discountInfo, context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 更新上下文中的当前金额
|
||||
context.setCurrentAmount(currentAmount);
|
||||
|
||||
DiscountResult discountResult = provider.applyDiscount(discountInfo, context);
|
||||
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
||||
appliedDiscounts.add(discountResult);
|
||||
|
||||
// 创建显示用的优惠详情
|
||||
DiscountDetail detail = createDiscountDetail(discountResult);
|
||||
if (detail != null) {
|
||||
discountDetails.add(detail);
|
||||
}
|
||||
|
||||
// 更新当前金额
|
||||
currentAmount = discountResult.getFinalAmount();
|
||||
|
||||
log.info("成功应用优惠: {} - {}, 优惠金额: {}",
|
||||
discountInfo.getProviderType(),
|
||||
discountInfo.getDiscountName(),
|
||||
discountResult.getActualDiscountAmount());
|
||||
|
||||
// 如果是不可叠加的优惠(如全场免费),则停止应用其他优惠
|
||||
if (!Boolean.TRUE.equals(discountInfo.getStackable())) {
|
||||
log.info("遇到不可叠加优惠,停止应用其他优惠: {}", discountInfo.getDiscountName());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
log.warn("优惠应用失败: {} - {}, 原因: {}",
|
||||
discountInfo.getProviderType(),
|
||||
discountInfo.getDiscountName(),
|
||||
discountResult.getFailureReason());
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总优惠金额
|
||||
BigDecimal totalDiscountAmount = appliedDiscounts.stream()
|
||||
.map(DiscountResult::getActualDiscountAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 按显示顺序排序折扣详情
|
||||
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||
|
||||
result.setFinalAmount(currentAmount);
|
||||
result.setTotalDiscountAmount(totalDiscountAmount);
|
||||
result.setAppliedDiscounts(appliedDiscounts);
|
||||
result.setDiscountDetails(discountDetails);
|
||||
result.setSuccess(true);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算最优优惠组合失败", e);
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage("优惠计算失败: " + e.getMessage());
|
||||
result.setFinalAmount(context.getCurrentAmount());
|
||||
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context) {
|
||||
// 预览模式与正常计算相同,但不会实际标记优惠为已使用
|
||||
return calculateOptimalCombination(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerProvider(IDiscountProvider provider) {
|
||||
if (provider != null && !discountProviders.contains(provider)) {
|
||||
discountProviders.add(provider);
|
||||
// 重新排序
|
||||
discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
||||
log.info("注册新的优惠提供者: {}", provider.getProviderType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IDiscountProvider> getAllProviders() {
|
||||
return new ArrayList<>(discountProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定类型的优惠提供者
|
||||
*/
|
||||
private IDiscountProvider findProvider(String providerType) {
|
||||
return discountProviders.stream()
|
||||
.filter(provider -> providerType.equals(provider.getProviderType()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建显示用的优惠详情
|
||||
*/
|
||||
private DiscountDetail createDiscountDetail(DiscountResult discountResult) {
|
||||
DiscountInfo discountInfo = discountResult.getDiscountInfo();
|
||||
String providerType = discountInfo.getProviderType();
|
||||
|
||||
return switch (providerType) {
|
||||
case "VOUCHER" -> DiscountDetail.createVoucherDiscount(
|
||||
discountInfo.getVoucherCode(),
|
||||
discountInfo.getDiscountDescription(),
|
||||
discountResult.getActualDiscountAmount()
|
||||
);
|
||||
case "COUPON" -> DiscountDetail.createCouponDiscount(
|
||||
discountInfo.getDiscountName(),
|
||||
discountResult.getActualDiscountAmount()
|
||||
);
|
||||
default -> {
|
||||
// 其他类型的优惠,创建通用的折扣详情
|
||||
DiscountDetail detail = new DiscountDetail();
|
||||
detail.setDiscountType(providerType);
|
||||
detail.setDiscountName(discountInfo.getDiscountName());
|
||||
detail.setDiscountAmount(discountResult.getActualDiscountAmount());
|
||||
detail.setDescription(discountInfo.getDiscountDescription());
|
||||
detail.setSortOrder(10); // 默认排序
|
||||
yield detail;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -5,10 +5,7 @@ import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.exception.PriceCalculationException;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||
import com.ycwl.basic.pricing.service.IProductConfigService;
|
||||
import com.ycwl.basic.pricing.service.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -30,6 +27,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
private final IProductConfigService productConfigService;
|
||||
private final ICouponService couponService;
|
||||
private final IPriceBundleService bundleService;
|
||||
private final IDiscountDetectionService discountDetectionService;
|
||||
private final IVoucherService voucherService;
|
||||
|
||||
@Override
|
||||
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
||||
@@ -59,35 +58,60 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount);
|
||||
}
|
||||
|
||||
// 构建价格计算结果
|
||||
PriceCalculationResult result = new PriceCalculationResult();
|
||||
result.setOriginalAmount(originalTotalAmount); // 原总价
|
||||
result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计
|
||||
result.setProductDetails(request.getProducts());
|
||||
|
||||
// 处理优惠券
|
||||
BigDecimal couponDiscountAmount = BigDecimal.ZERO;
|
||||
if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) {
|
||||
CouponInfo bestCoupon = couponService.selectBestCoupon(
|
||||
request.getUserId(), request.getProducts(), totalAmount);
|
||||
|
||||
if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
result.setUsedCoupon(bestCoupon);
|
||||
couponDiscountAmount = bestCoupon.getActualDiscountAmount();
|
||||
discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount));
|
||||
}
|
||||
}
|
||||
// 使用新的优惠检测系统处理所有优惠(券码 + 优惠券)
|
||||
DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount);
|
||||
|
||||
// 计算总优惠金额
|
||||
BigDecimal totalDiscountAmount = discountDetails.stream()
|
||||
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
||||
// 合并所有优惠详情
|
||||
List<DiscountDetail> allDiscountDetails = new ArrayList<>(discountDetails);
|
||||
if (discountResult.getDiscountDetails() != null) {
|
||||
allDiscountDetails.addAll(discountResult.getDiscountDetails());
|
||||
}
|
||||
|
||||
// 重新排序
|
||||
allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||
|
||||
// 计算总优惠金额(包括限时立减、一口价和其他优惠)
|
||||
BigDecimal totalDiscountAmount = allDiscountDetails.stream()
|
||||
.map(DiscountDetail::getDiscountAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 设置结果
|
||||
result.setDiscountAmount(totalDiscountAmount);
|
||||
result.setDiscountDetails(allDiscountDetails);
|
||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||
|
||||
// 设置使用的券码和优惠券信息
|
||||
setUsedDiscountInfo(result, discountResult, request);
|
||||
|
||||
// 如果是预览模式,设置可用优惠列表
|
||||
if (Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
result.setAvailableDiscounts(discountResult.getAvailableDiscounts());
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn("优惠计算失败: {}", discountResult.getErrorMessage());
|
||||
|
||||
// 降级处理:仅使用基础优惠(限时立减、一口价)
|
||||
BigDecimal totalDiscountAmount = discountDetails.stream()
|
||||
.map(DiscountDetail::getDiscountAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
result.setDiscountAmount(totalDiscountAmount);
|
||||
result.setDiscountDetails(discountDetails);
|
||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||
}
|
||||
|
||||
// 按排序排列折扣明细
|
||||
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||
|
||||
result.setDiscountAmount(totalDiscountAmount);
|
||||
result.setDiscountDetails(discountDetails);
|
||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||
// 标记使用的优惠(仅在非预览模式下)
|
||||
if (!Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
markDiscountsAsUsed(result, request);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -269,4 +293,96 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
|
||||
return new ProductPriceInfo(actualPrice, originalPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠(券码 + 优惠券)
|
||||
*/
|
||||
private DiscountCombinationResult calculateDiscounts(PriceCalculationRequest request, BigDecimal currentAmount) {
|
||||
try {
|
||||
// 构建优惠检测上下文
|
||||
DiscountDetectionContext context = new DiscountDetectionContext();
|
||||
context.setUserId(request.getUserId());
|
||||
context.setFaceId(request.getFaceId());
|
||||
context.setScenicId(request.getScenicId());
|
||||
context.setProducts(request.getProducts());
|
||||
context.setCurrentAmount(currentAmount);
|
||||
context.setVoucherCode(request.getVoucherCode());
|
||||
context.setAutoUseCoupon(request.getAutoUseCoupon());
|
||||
context.setAutoUseVoucher(request.getAutoUseVoucher());
|
||||
|
||||
// 使用优惠检测服务计算最优组合
|
||||
if (Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
return discountDetectionService.previewOptimalCombination(context);
|
||||
} else {
|
||||
return discountDetectionService.calculateOptimalCombination(context);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算优惠时发生异常", e);
|
||||
|
||||
// 返回失败结果
|
||||
DiscountCombinationResult failureResult = new DiscountCombinationResult();
|
||||
failureResult.setOriginalAmount(currentAmount);
|
||||
failureResult.setFinalAmount(currentAmount);
|
||||
failureResult.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||
failureResult.setSuccess(false);
|
||||
failureResult.setErrorMessage("优惠计算失败: " + e.getMessage());
|
||||
return failureResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用的优惠信息到结果中
|
||||
*/
|
||||
private void setUsedDiscountInfo(PriceCalculationResult result, DiscountCombinationResult discountResult, PriceCalculationRequest request) {
|
||||
if (discountResult.getAppliedDiscounts() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (DiscountResult discountApplied : discountResult.getAppliedDiscounts()) {
|
||||
DiscountInfo discountInfo = discountApplied.getDiscountInfo();
|
||||
|
||||
if ("COUPON".equals(discountInfo.getProviderType()) && discountInfo.getCouponId() != null) {
|
||||
// 构建优惠券信息(这里可能需要重新查询完整信息)
|
||||
CouponInfo couponInfo = new CouponInfo();
|
||||
couponInfo.setCouponId(discountInfo.getCouponId());
|
||||
couponInfo.setCouponName(discountInfo.getDiscountName());
|
||||
couponInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount());
|
||||
result.setUsedCoupon(couponInfo);
|
||||
|
||||
} else if ("VOUCHER".equals(discountInfo.getProviderType()) && discountInfo.getVoucherCode() != null) {
|
||||
// 获取券码信息
|
||||
VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo(
|
||||
discountInfo.getVoucherCode(),
|
||||
request.getFaceId(),
|
||||
request.getScenicId()
|
||||
);
|
||||
if (voucherInfo != null) {
|
||||
voucherInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount());
|
||||
result.setUsedVoucher(voucherInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记优惠为已使用(仅在非预览模式下调用)
|
||||
*/
|
||||
private void markDiscountsAsUsed(PriceCalculationResult result, PriceCalculationRequest request) {
|
||||
try {
|
||||
// 标记券码为已使用
|
||||
if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) {
|
||||
String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount());
|
||||
voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark);
|
||||
log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode());
|
||||
}
|
||||
|
||||
// 优惠券的使用标记由原有的CouponService处理
|
||||
// 这里不需要额外处理
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("标记优惠使用状态时发生异常", e);
|
||||
// 不抛出异常,避免影响主流程
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 价格管理服务实现(用于配置管理,手动处理时间字段)
|
||||
@@ -35,8 +35,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
config.setCreatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setCreatedTime(new Date());
|
||||
config.setUpdatedTime(new Date());
|
||||
productConfigMapper.insertProductConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateProductConfig(PriceProductConfig config) {
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(new Date());
|
||||
return productConfigMapper.updateProductConfig(config) > 0;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
config.getProductType(), config.getMinQuantity(), config.getMaxQuantity());
|
||||
}
|
||||
|
||||
config.setCreatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setCreatedTime(new Date());
|
||||
config.setUpdatedTime(new Date());
|
||||
tierConfigMapper.insertTierConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
@@ -67,15 +67,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateTierConfig(PriceTierConfig config) {
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(new Date());
|
||||
return tierConfigMapper.updateTierConfig(config) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createCouponConfig(PriceCouponConfig config) {
|
||||
config.setCreatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setCreatedTime(new Date());
|
||||
config.setUpdatedTime(new Date());
|
||||
couponConfigMapper.insertCoupon(config);
|
||||
return config.getId();
|
||||
}
|
||||
@@ -83,16 +83,16 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponConfig(PriceCouponConfig config) {
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(new Date());
|
||||
return couponConfigMapper.updateCoupon(config) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||
record.setClaimTime(LocalDateTime.now());
|
||||
record.setCreatedTime(LocalDateTime.now());
|
||||
record.setUpdatedTime(LocalDateTime.now());
|
||||
record.setClaimTime(new Date());
|
||||
record.setCreatedTime(new Date());
|
||||
record.setUpdatedTime(new Date());
|
||||
couponClaimRecordMapper.insertClaimRecord(record);
|
||||
return record.getId();
|
||||
}
|
||||
@@ -100,15 +100,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||
record.setUpdatedTime(LocalDateTime.now());
|
||||
record.setUpdatedTime(new Date());
|
||||
return couponClaimRecordMapper.updateClaimRecord(record) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createBundleConfig(PriceBundleConfig config) {
|
||||
config.setCreatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setCreatedTime(new Date());
|
||||
config.setUpdatedTime(new Date());
|
||||
bundleConfigMapper.insertBundleConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
@@ -116,7 +116,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateBundleConfig(PriceBundleConfig config) {
|
||||
config.setUpdatedTime(LocalDateTime.now());
|
||||
config.setUpdatedTime(new Date());
|
||||
return bundleConfigMapper.updateBundleConfig(config) > 0;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,193 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherBatchServiceImpl implements VoucherBatchService {
|
||||
|
||||
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||
private final VoucherCodeService voucherCodeService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createBatch(VoucherBatchCreateReq req) {
|
||||
if (req.getBatchName() == null || req.getBatchName().trim().isEmpty()) {
|
||||
throw new BizException(400, "券码批次名称不能为空");
|
||||
}
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getDiscountType() == null) {
|
||||
throw new BizException(400, "优惠类型不能为空");
|
||||
}
|
||||
if (req.getTotalCount() == null || req.getTotalCount() < 1) {
|
||||
throw new BizException(400, "券码数量必须大于0");
|
||||
}
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(req.getDiscountType());
|
||||
if (discountType == null) {
|
||||
throw new BizException(400, "无效的优惠类型");
|
||||
}
|
||||
|
||||
if (discountType != VoucherDiscountType.FREE_ALL && req.getDiscountValue() == null) {
|
||||
throw new BizException(400, "优惠金额不能为空");
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||
BeanUtils.copyProperties(req, batch);
|
||||
batch.setUsedCount(0);
|
||||
batch.setClaimedCount(0);
|
||||
batch.setStatus(1);
|
||||
batch.setCreatedTime(new Date());
|
||||
String userIdStr = BaseContextHandler.getUserId();
|
||||
if (userIdStr != null) {
|
||||
batch.setCreateBy(Long.valueOf(userIdStr));
|
||||
}
|
||||
batch.setDeleted(0);
|
||||
|
||||
voucherBatchMapper.insert(batch);
|
||||
|
||||
voucherCodeService.generateVoucherCodes(batch.getId(), req.getScenicId(), req.getTotalCount());
|
||||
|
||||
return batch.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
||||
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||
.eq(req.getScenicId() != null, PriceVoucherBatchConfig::getScenicId, req.getScenicId())
|
||||
.eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId())
|
||||
.eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName())
|
||||
.orderByDesc(PriceVoucherBatchConfig::getCreatedTime);
|
||||
|
||||
Page<PriceVoucherBatchConfig> entityPage = voucherBatchMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherBatchResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList());
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchResp getBatchDetail(Long id) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
return convertToResp(batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchStatsResp getBatchStats(Long id) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
VoucherBatchStatsResp stats = new VoucherBatchStatsResp();
|
||||
stats.setBatchId(batch.getId());
|
||||
stats.setBatchName(batch.getBatchName());
|
||||
stats.setTotalCount(batch.getTotalCount());
|
||||
stats.setClaimedCount(batch.getClaimedCount());
|
||||
stats.setUsedCount(batch.getUsedCount());
|
||||
stats.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
if (batch.getTotalCount() > 0) {
|
||||
stats.setClaimedRate((double) batch.getClaimedCount() / batch.getTotalCount() * 100);
|
||||
stats.setUsedRate((double) batch.getUsedCount() / batch.getTotalCount() * 100);
|
||||
} else {
|
||||
stats.setClaimedRate(0.0);
|
||||
stats.setUsedRate(0.0);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchStatus(Long id, Integer status) {
|
||||
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||
batch.setId(id);
|
||||
batch.setStatus(status);
|
||||
|
||||
int updated = voucherBatchMapper.updateById(batch);
|
||||
if (updated == 0) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchClaimedCount(Long batchId) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setClaimedCount(batch.getClaimedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchUsedCount(Long batchId) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setUsedCount(batch.getUsedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId) {
|
||||
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherBatchConfig::getScenicId, scenicId)
|
||||
.eq(PriceVoucherBatchConfig::getBrokerId, brokerId)
|
||||
.eq(PriceVoucherBatchConfig::getStatus, 1)
|
||||
.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||
.apply("claimed_count < total_count")
|
||||
.orderByDesc(PriceVoucherBatchConfig::getCreatedTime);
|
||||
|
||||
return voucherBatchMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private VoucherBatchResp convertToResp(PriceVoucherBatchConfig batch) {
|
||||
VoucherBatchResp resp = new VoucherBatchResp();
|
||||
BeanUtils.copyProperties(batch, resp);
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
}
|
||||
|
||||
resp.setStatusName(batch.getStatus() == 1 ? "启用" : "禁用");
|
||||
resp.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -0,0 +1,194 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
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.service.VoucherBatchService;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
|
||||
private final PriceVoucherCodeMapper voucherCodeMapper;
|
||||
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||
private final 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.setCreatedTime(new Date());
|
||||
code.setDeleted(0);
|
||||
codes.add(code);
|
||||
}
|
||||
|
||||
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.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
|
||||
if (batch == null) {
|
||||
throw new BizException(400, "暂无可用券码批次");
|
||||
}
|
||||
|
||||
PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
|
||||
if (availableCode == null) {
|
||||
throw new BizException(400, "券码已领完");
|
||||
}
|
||||
|
||||
availableCode.setFaceId(req.getFaceId());
|
||||
availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
availableCode.setClaimedTime(new Date());
|
||||
|
||||
voucherCodeMapper.updateById(availableCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(availableCode, batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
||||
Page<PriceVoucherCode> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
|
||||
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId())
|
||||
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
|
||||
.orderByDesc(PriceVoucherCode::getCreatedTime);
|
||||
|
||||
Page<PriceVoucherCode> entityPage = voucherCodeMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherCodeResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (PriceVoucherCode code : entityPage.getRecords()) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
respPage.setRecords(respList);
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getFaceId, faceId)
|
||||
.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.orderByDesc(PriceVoucherCode::getClaimedTime);
|
||||
|
||||
List<PriceVoucherCode> codes = voucherCodeMapper.selectList(wrapper);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (PriceVoucherCode code : codes) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
|
||||
return respList;
|
||||
}
|
||||
|
||||
@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 (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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
private String generateVoucherCode() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) {
|
||||
VoucherCodeResp resp = new VoucherCodeResp();
|
||||
BeanUtils.copyProperties(code, resp);
|
||||
|
||||
if (batch != null) {
|
||||
resp.setBatchName(batch.getBatchName());
|
||||
resp.setDiscountType(batch.getDiscountType());
|
||||
resp.setDiscountValue(batch.getDiscountValue());
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
resp.setDiscountDescription(discountType.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus());
|
||||
if (status != null) {
|
||||
resp.setStatusName(status.getName());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 券码折扣提供者
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherDiscountProvider implements IDiscountProvider {
|
||||
|
||||
private final IVoucherService voucherService;
|
||||
|
||||
@Override
|
||||
public String getProviderType() {
|
||||
return "VOUCHER";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 100; // 券码优先级最高
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||
List<DiscountInfo> discounts = new ArrayList<>();
|
||||
|
||||
if (context.getFaceId() == null || context.getScenicId() == null) {
|
||||
return discounts;
|
||||
}
|
||||
|
||||
try {
|
||||
VoucherInfo voucherInfo = null;
|
||||
|
||||
// 优先检查用户主动输入的券码
|
||||
if (StringUtils.hasText(context.getVoucherCode())) {
|
||||
voucherInfo = voucherService.validateAndGetVoucherInfo(
|
||||
context.getVoucherCode(),
|
||||
context.getFaceId(),
|
||||
context.getScenicId()
|
||||
);
|
||||
}
|
||||
// 如果没有输入券码且允许自动使用,则查找最优券码
|
||||
else if (Boolean.TRUE.equals(context.getAutoUseVoucher())) {
|
||||
voucherInfo = voucherService.getBestVoucher(
|
||||
context.getFaceId(),
|
||||
context.getScenicId(),
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
if (voucherInfo != null && Boolean.TRUE.equals(voucherInfo.getAvailable())) {
|
||||
// 计算券码优惠金额
|
||||
BigDecimal discountAmount = voucherService.calculateVoucherDiscount(voucherInfo, context);
|
||||
|
||||
if (discountAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
DiscountInfo discountInfo = new DiscountInfo();
|
||||
discountInfo.setDiscountId(voucherInfo.getVoucherId());
|
||||
discountInfo.setDiscountType("VOUCHER");
|
||||
discountInfo.setDiscountName("券码优惠");
|
||||
discountInfo.setDiscountDescription(buildDiscountDescription(voucherInfo));
|
||||
discountInfo.setDiscountAmount(discountAmount);
|
||||
discountInfo.setProviderType(getProviderType());
|
||||
discountInfo.setPriority(getPriority());
|
||||
discountInfo.setStackable(isStackable(voucherInfo)); // 只有全场免费不可叠加
|
||||
discountInfo.setVoucherCode(voucherInfo.getVoucherCode());
|
||||
|
||||
discounts.add(discountInfo);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("检测券码优惠时发生异常", e);
|
||||
}
|
||||
|
||||
return discounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||
DiscountResult result = new DiscountResult();
|
||||
result.setDiscountInfo(discountInfo);
|
||||
|
||||
try {
|
||||
String voucherCode = discountInfo.getVoucherCode();
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
result.setSuccess(false);
|
||||
result.setFailureReason("券码信息丢失");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 重新验证券码
|
||||
VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo(
|
||||
voucherCode,
|
||||
context.getFaceId(),
|
||||
context.getScenicId()
|
||||
);
|
||||
|
||||
if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable())) {
|
||||
result.setSuccess(false);
|
||||
result.setFailureReason("券码无效或不可用");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 计算实际优惠金额
|
||||
BigDecimal actualDiscount = voucherService.calculateVoucherDiscount(voucherInfo, context);
|
||||
BigDecimal finalAmount;
|
||||
|
||||
// 对于全场免费券码,最终金额为0
|
||||
if (voucherInfo.getDiscountType() == VoucherDiscountType.FREE_ALL) {
|
||||
finalAmount = BigDecimal.ZERO;
|
||||
actualDiscount = context.getCurrentAmount();
|
||||
} else {
|
||||
finalAmount = context.getCurrentAmount().subtract(actualDiscount);
|
||||
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
finalAmount = BigDecimal.ZERO;
|
||||
actualDiscount = context.getCurrentAmount();
|
||||
}
|
||||
}
|
||||
|
||||
result.setActualDiscountAmount(actualDiscount);
|
||||
result.setFinalAmount(finalAmount);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("成功应用券码: {}, 优惠金额: {}", voucherCode, actualDiscount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("应用券码失败: " + discountInfo.getVoucherCode(), e);
|
||||
result.setSuccess(false);
|
||||
result.setFailureReason("券码应用失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||
return "VOUCHER".equals(discountInfo.getDiscountType()) &&
|
||||
context.getFaceId() != null &&
|
||||
context.getScenicId() != null &&
|
||||
StringUtils.hasText(discountInfo.getVoucherCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建优惠描述
|
||||
*/
|
||||
private String buildDiscountDescription(VoucherInfo voucherInfo) {
|
||||
if (voucherInfo.getDiscountType() == null) {
|
||||
return "券码优惠";
|
||||
}
|
||||
|
||||
return String.format("券码 %s - %s",
|
||||
voucherInfo.getVoucherCode(),
|
||||
voucherInfo.getDiscountType().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否可以与其他优惠叠加
|
||||
*/
|
||||
private boolean isStackable(VoucherInfo voucherInfo) {
|
||||
// 全场免费券码不可与其他优惠叠加
|
||||
return voucherInfo.getDiscountType() != VoucherDiscountType.FREE_ALL;
|
||||
}
|
||||
}
|
@@ -0,0 +1,223 @@
|
||||
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.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.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.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 券码服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherServiceImpl implements IVoucherService {
|
||||
|
||||
private final PriceVoucherCodeMapper voucherCodeMapper;
|
||||
private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper;
|
||||
|
||||
@Override
|
||||
public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) {
|
||||
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 || batchConfig.getStatus() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证景区匹配
|
||||
if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
|
||||
|
||||
// 检查券码状态和可用性
|
||||
if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||
// 未领取状态,检查是否可以领取
|
||||
if (faceId != null && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())) {
|
||||
voucherInfo.setAvailable(true);
|
||||
} else {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
|
||||
}
|
||||
} else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||
// 已领取未使用,检查是否为当前用户
|
||||
if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) {
|
||||
voucherInfo.setAvailable(true);
|
||||
} else {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已被其他用户领取");
|
||||
}
|
||||
} else {
|
||||
// 已使用
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已使用");
|
||||
}
|
||||
|
||||
return voucherInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoucherInfo> getAvailableVouchers(Long faceId, Long scenicId) {
|
||||
if (faceId == null || scenicId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId);
|
||||
List<VoucherInfo> voucherInfos = new ArrayList<>();
|
||||
|
||||
for (PriceVoucherCode voucherCode : voucherCodes) {
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId());
|
||||
if (batchConfig != null && batchConfig.getDeleted() == 0 && batchConfig.getStatus() == 1) {
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig);
|
||||
voucherInfo.setAvailable(true);
|
||||
voucherInfos.add(voucherInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return voucherInfos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markVoucherAsUsed(String voucherCode, String remark) {
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark);
|
||||
if (result > 0) {
|
||||
// 更新批次统计
|
||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||
if (voucherCodeEntity != null) {
|
||||
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
|
||||
}
|
||||
log.info("券码已标记为使用: {}", voucherCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
if (faceId == null || scenicId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) {
|
||||
if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) ||
|
||||
context.getProducts() == null || context.getProducts().isEmpty()) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
VoucherDiscountType discountType = voucherInfo.getDiscountType();
|
||||
BigDecimal discountValue = voucherInfo.getDiscountValue();
|
||||
|
||||
if (discountType == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return switch (discountType) {
|
||||
case FREE_ALL -> {
|
||||
// 全场免费,返回当前总金额
|
||||
yield context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
|
||||
}
|
||||
case REDUCE_PRICE -> {
|
||||
// 商品降价,每个商品减免固定金额
|
||||
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
yield BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal totalDiscount = BigDecimal.ZERO;
|
||||
for (ProductItem product : context.getProducts()) {
|
||||
BigDecimal productDiscount = discountValue.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
totalDiscount = totalDiscount.add(productDiscount);
|
||||
}
|
||||
yield totalDiscount;
|
||||
}
|
||||
case DISCOUNT -> {
|
||||
// 商品打折,按百分比计算
|
||||
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0 ||
|
||||
discountValue.compareTo(BigDecimal.valueOf(100)) >= 0) {
|
||||
yield BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal currentAmount = context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
|
||||
BigDecimal discountRate = discountValue.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
|
||||
yield currentAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) {
|
||||
List<VoucherInfo> availableVouchers = getAvailableVouchers(faceId, scenicId);
|
||||
if (availableVouchers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算每个券码的优惠金额,选择最优的
|
||||
VoucherInfo bestVoucher = null;
|
||||
BigDecimal maxDiscount = BigDecimal.ZERO;
|
||||
|
||||
for (VoucherInfo voucher : availableVouchers) {
|
||||
BigDecimal discount = calculateVoucherDiscount(voucher, context);
|
||||
voucher.setActualDiscountAmount(discount);
|
||||
|
||||
if (discount.compareTo(maxDiscount) > 0) {
|
||||
maxDiscount = discount;
|
||||
bestVoucher = voucher;
|
||||
}
|
||||
}
|
||||
|
||||
return bestVoucher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建券码信息DTO
|
||||
*/
|
||||
private VoucherInfo buildVoucherInfo(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig) {
|
||||
VoucherInfo voucherInfo = new VoucherInfo();
|
||||
voucherInfo.setVoucherId(voucherCode.getId());
|
||||
voucherInfo.setVoucherCode(voucherCode.getCode());
|
||||
voucherInfo.setBatchId(batchConfig.getId());
|
||||
voucherInfo.setBatchName(batchConfig.getBatchName());
|
||||
voucherInfo.setScenicId(voucherCode.getScenicId());
|
||||
voucherInfo.setBrokerId(batchConfig.getBrokerId());
|
||||
voucherInfo.setDiscountType(VoucherDiscountType.getByCode(batchConfig.getDiscountType()));
|
||||
voucherInfo.setDiscountValue(batchConfig.getDiscountValue());
|
||||
voucherInfo.setStatus(voucherCode.getStatus());
|
||||
voucherInfo.setClaimedTime(voucherCode.getClaimedTime());
|
||||
voucherInfo.setUsedTime(voucherCode.getUsedTime());
|
||||
|
||||
return voucherInfo;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user