You've already forked FrameTour-BE
Merge branch 'refs/heads/order_v2'
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package com.ycwl.basic.order.factory;
|
||||
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 重复购买检查策略工厂
|
||||
*
|
||||
* 设计原则:
|
||||
* 1. 自动注册:Spring 自动注入所有策略实现并注册
|
||||
* 2. 类型安全:根据枚举类型查找策略
|
||||
* 3. 失败快速:找不到策略时抛出异常
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DuplicatePurchaseCheckerFactory {
|
||||
|
||||
private final Map<DuplicateCheckStrategy, IDuplicatePurchaseChecker> checkerMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 构造函数:自动注册所有策略实现
|
||||
*/
|
||||
@Autowired
|
||||
public DuplicatePurchaseCheckerFactory(List<IDuplicatePurchaseChecker> checkers) {
|
||||
for (IDuplicatePurchaseChecker checker : checkers) {
|
||||
DuplicateCheckStrategy strategy = checker.getStrategyType();
|
||||
checkerMap.put(strategy, checker);
|
||||
log.info("注册重复购买检查策略: {} -> {}", strategy, checker.getClass().getSimpleName());
|
||||
}
|
||||
log.info("重复购买检查策略工厂初始化完成,共注册 {} 个策略", checkerMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据策略类型获取检查器
|
||||
*
|
||||
* @param strategy 策略类型
|
||||
* @return 对应的检查器实现
|
||||
* @throws IllegalArgumentException 如果找不到对应的策略实现
|
||||
*/
|
||||
public IDuplicatePurchaseChecker getChecker(DuplicateCheckStrategy strategy) {
|
||||
IDuplicatePurchaseChecker checker = checkerMap.get(strategy);
|
||||
if (checker == null) {
|
||||
throw new IllegalArgumentException("未找到重复购买检查策略: " + strategy);
|
||||
}
|
||||
return checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持指定策略
|
||||
*/
|
||||
public boolean supportsStrategy(DuplicateCheckStrategy strategy) {
|
||||
return checkerMap.containsKey(strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的策略类型
|
||||
*/
|
||||
public Map<DuplicateCheckStrategy, IDuplicatePurchaseChecker> getAllCheckers() {
|
||||
return new HashMap<>(checkerMap);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ import com.ycwl.basic.order.mapper.OrderV2Mapper;
|
||||
import com.ycwl.basic.order.mapper.OrderRefundMapper;
|
||||
import com.ycwl.basic.order.service.IOrderService;
|
||||
import com.ycwl.basic.order.event.*;
|
||||
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetail;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
@@ -69,6 +76,8 @@ public class OrderServiceImpl implements IOrderService {
|
||||
private final ICouponService couponService;
|
||||
private final IVoucherService voucherService;
|
||||
private final IProductConfigService productConfigService;
|
||||
private final IProductTypeCapabilityService productTypeCapabilityService;
|
||||
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -767,16 +776,10 @@ public class OrderServiceImpl implements IOrderService {
|
||||
|
||||
/**
|
||||
* 获取商品类型中文名称
|
||||
* 重构: 从配置驱动替代硬编码
|
||||
*/
|
||||
private String getProductTypeName(String productType) {
|
||||
return switch (productType) {
|
||||
case "VLOG_VIDEO" -> "Vlog视频";
|
||||
case "RECORDING_SET" -> "录像集";
|
||||
case "PHOTO_SET" -> "照相集";
|
||||
case "PHOTO_PRINT" -> "照片打印";
|
||||
case "MACHINE_PRINT" -> "一体机打印";
|
||||
default -> "景区商品";
|
||||
};
|
||||
return productTypeCapabilityService.getDisplayName(productType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -850,14 +853,17 @@ public class OrderServiceImpl implements IOrderService {
|
||||
: getProductTypeName(product.getProductType().name());
|
||||
|
||||
// 3. 处理按数量计价的商品类型
|
||||
if (product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.PHOTO_PRINT ||
|
||||
product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.MACHINE_PRINT) {
|
||||
if (product.getQuantity() != null && product.getQuantity() > 0) {
|
||||
unitPrice = unitPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
// 重构: 使用商品类型能力配置替代硬编码判断
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability(productTypeCode);
|
||||
if (capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED) {
|
||||
Integer quantity = product.getQuantity() != null && product.getQuantity() > 0
|
||||
? product.getQuantity() : 1;
|
||||
unitPrice = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(quantity));
|
||||
}
|
||||
log.debug("按数量计价: productType={}, quantity={}, unitPrice={}",
|
||||
productTypeCode, quantity, unitPrice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,7 +900,8 @@ public class OrderServiceImpl implements IOrderService {
|
||||
/**
|
||||
* 检查重复购买
|
||||
* 防止用户重复购买相同内容
|
||||
*
|
||||
* 重构: 使用策略模式替代硬编码的 switch-case
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param faceId 人脸ID
|
||||
* @param scenicId 景区ID
|
||||
@@ -903,117 +910,44 @@ public class OrderServiceImpl implements IOrderService {
|
||||
*/
|
||||
private void checkDuplicatePurchase(Long userId, Long faceId, Long scenicId, List<ProductItem> products) {
|
||||
for (ProductItem product : products) {
|
||||
switch (product.getProductType()) {
|
||||
case VLOG_VIDEO:
|
||||
checkVideoAlreadyPurchased(userId, faceId, scenicId, product.getProductId());
|
||||
break;
|
||||
case RECORDING_SET:
|
||||
case PHOTO_SET:
|
||||
checkSetAlreadyPurchased(userId, faceId, scenicId, product.getProductType());
|
||||
break;
|
||||
case PHOTO_PRINT:
|
||||
case PHOTO_PRINT_MU:
|
||||
case PHOTO_PRINT_FX:
|
||||
case MACHINE_PRINT:
|
||||
// 打印类商品允许重复购买,跳过检查
|
||||
log.debug("跳过打印类商品重复购买检查: productType={}, productId={}",
|
||||
product.getProductType(), product.getProductId());
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||
break;
|
||||
String productType = product.getProductType().getCode();
|
||||
|
||||
// 获取商品类型能力配置
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability(productType);
|
||||
|
||||
// 如果允许重复购买,直接跳过
|
||||
if (Boolean.TRUE.equals(capability.getAllowDuplicatePurchase())) {
|
||||
log.debug("商品类型允许重复购买,跳过检查: productType={}, productId={}",
|
||||
productType, product.getProductId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取检查策略并执行
|
||||
DuplicateCheckStrategy strategy = capability.getDuplicateCheckStrategyEnum();
|
||||
if (strategy != null && strategy != DuplicateCheckStrategy.NO_CHECK) {
|
||||
try {
|
||||
IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy);
|
||||
|
||||
// 构建检查上下文
|
||||
DuplicateCheckContext context = new DuplicateCheckContext();
|
||||
context.setUserId(String.valueOf(userId));
|
||||
context.setScenicId(String.valueOf(scenicId));
|
||||
context.setProductType(productType);
|
||||
context.setProductId(product.getProductId());
|
||||
context.setProducts(products);
|
||||
context.addParam("faceId", faceId);
|
||||
|
||||
// 执行检查
|
||||
checker.check(context);
|
||||
} catch (DuplicatePurchaseException e) {
|
||||
// 重新抛出重复购买异常
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("重复购买检查失败,策略: {}, productType: {}", strategy, productType, e);
|
||||
// 检查失败时为了安全起见,默认拒绝
|
||||
throw new BaseException("重复购买检查失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查视频是否已经购买
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param faceId 人脸ID
|
||||
* @param scenicId 景区ID
|
||||
* @param videoId 视频ID
|
||||
* @throws DuplicatePurchaseException 如果已购买
|
||||
*/
|
||||
private void checkVideoAlreadyPurchased(Long userId, Long faceId, Long scenicId, String videoId) {
|
||||
// 构建查询条件:查找已支付的有效订单中包含该视频的订单
|
||||
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
|
||||
orderQuery.eq("member_id", userId)
|
||||
.eq("face_id", faceId)
|
||||
.eq("scenic_id", scenicId)
|
||||
.eq("payment_status", PaymentStatus.PAID.getCode())
|
||||
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
|
||||
.eq("deleted", 0);
|
||||
|
||||
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
|
||||
|
||||
for (OrderV2 order : existingOrders) {
|
||||
// 检查订单明细中是否包含该视频
|
||||
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
|
||||
itemQuery.eq("order_id", order.getId())
|
||||
.eq("product_type", com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO.name())
|
||||
.eq("product_id", videoId);
|
||||
|
||||
long count = orderItemMapper.selectCount(itemQuery);
|
||||
if (count > 0) {
|
||||
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
|
||||
userId, faceId, scenicId, videoId, order.getId());
|
||||
throw new DuplicatePurchaseException(
|
||||
"您已购买过此视频",
|
||||
order.getId(),
|
||||
order.getOrderNo(),
|
||||
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
|
||||
videoId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
|
||||
userId, faceId, scenicId, videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查套餐(录像集/照相集)是否已经购买
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param faceId 人脸ID
|
||||
* @param scenicId 景区ID
|
||||
* @param productType 商品类型
|
||||
* @throws DuplicatePurchaseException 如果已购买
|
||||
*/
|
||||
private void checkSetAlreadyPurchased(Long userId, Long faceId, Long scenicId,
|
||||
com.ycwl.basic.pricing.enums.ProductType productType) {
|
||||
// 构建查询条件:查找已支付的有效订单中包含该类型套餐的订单
|
||||
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
|
||||
orderQuery.eq("member_id", userId)
|
||||
.eq("face_id", faceId)
|
||||
.eq("scenic_id", scenicId)
|
||||
.eq("payment_status", PaymentStatus.PAID.getCode())
|
||||
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
|
||||
.eq("deleted", 0);
|
||||
|
||||
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
|
||||
|
||||
for (OrderV2 order : existingOrders) {
|
||||
// 检查订单明细中是否包含该类型的套餐
|
||||
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
|
||||
itemQuery.eq("order_id", order.getId())
|
||||
.eq("product_type", productType.name());
|
||||
|
||||
long count = orderItemMapper.selectCount(itemQuery);
|
||||
if (count > 0) {
|
||||
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
|
||||
userId, faceId, scenicId, productType, order.getId());
|
||||
throw new DuplicatePurchaseException(
|
||||
"您已购买过此类型的套餐",
|
||||
order.getId(),
|
||||
order.getOrderNo(),
|
||||
productType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
|
||||
userId, faceId, scenicId, productType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ycwl.basic.order.strategy;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 重复购买检查上下文
|
||||
* 封装检查所需的所有参数
|
||||
*/
|
||||
@Data
|
||||
public class DuplicateCheckContext {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
/**
|
||||
* 商品类型代码
|
||||
*/
|
||||
private String productType;
|
||||
|
||||
/**
|
||||
* 商品ID(视频ID、套餐ID等)
|
||||
*/
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 当前购物车中的所有商品
|
||||
*/
|
||||
private List<ProductItem> products;
|
||||
|
||||
/**
|
||||
* 额外参数(扩展用)
|
||||
*/
|
||||
private Map<String, Object> additionalParams;
|
||||
|
||||
public DuplicateCheckContext() {
|
||||
this.additionalParams = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加额外参数
|
||||
*/
|
||||
public void addParam(String key, Object value) {
|
||||
this.additionalParams.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外参数
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getParam(String key) {
|
||||
return (T) this.additionalParams.get(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ycwl.basic.order.strategy;
|
||||
|
||||
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
|
||||
/**
|
||||
* 重复购买检查策略接口
|
||||
*
|
||||
* 设计原则:
|
||||
* 1. 单一职责: 每个策略只负责一种检查逻辑
|
||||
* 2. 开放扩展: 通过实现接口添加新策略
|
||||
* 3. 自动注册: Spring 自动扫描并注册所有实现类
|
||||
*/
|
||||
public interface IDuplicatePurchaseChecker {
|
||||
|
||||
/**
|
||||
* 获取策略类型
|
||||
* 用于策略工厂的注册和查找
|
||||
*
|
||||
* @return 对应的 DuplicateCheckStrategy 枚举值
|
||||
*/
|
||||
DuplicateCheckStrategy getStrategyType();
|
||||
|
||||
/**
|
||||
* 执行重复购买检查
|
||||
*
|
||||
* @param context 检查上下文,包含用户ID、商品信息等
|
||||
* @throws DuplicatePurchaseException 如果检测到重复购买
|
||||
*/
|
||||
void check(DuplicateCheckContext context) throws DuplicatePurchaseException;
|
||||
|
||||
/**
|
||||
* 是否支持该商品类型
|
||||
* 默认实现:所有策略都支持所有商品类型
|
||||
* 可在具体实现中覆盖以限制适用范围
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return true-支持, false-不支持
|
||||
*/
|
||||
default boolean supports(String productType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ycwl.basic.order.strategy.impl;
|
||||
|
||||
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 不检查重复购买策略
|
||||
* 适用于:打印类商品等允许重复购买的商品类型
|
||||
*
|
||||
* 检查逻辑:
|
||||
* 不执行任何检查,直接通过
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class NoCheckDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||
|
||||
@Override
|
||||
public DuplicateCheckStrategy getStrategyType() {
|
||||
return DuplicateCheckStrategy.NO_CHECK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(DuplicateCheckContext context) {
|
||||
// 不检查,直接通过
|
||||
log.debug("跳过重复购买检查: productType={}, productId={}",
|
||||
context.getProductType(), context.getProductId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.ycwl.basic.order.strategy.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.ycwl.basic.order.entity.OrderItemV2;
|
||||
import com.ycwl.basic.order.entity.OrderV2;
|
||||
import com.ycwl.basic.order.enums.OrderStatus;
|
||||
import com.ycwl.basic.order.enums.PaymentStatus;
|
||||
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
|
||||
import com.ycwl.basic.order.mapper.OrderItemMapper;
|
||||
import com.ycwl.basic.order.mapper.OrderV2Mapper;
|
||||
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 按套餐ID检查重复购买策略
|
||||
* 适用于:RECORDING_SET(录像集)、PHOTO_SET(照相集)商品类型
|
||||
*
|
||||
* 检查逻辑:
|
||||
* 1. 查找用户在该景区已支付的有效订单
|
||||
* 2. 检查订单明细中是否包含相同类型的套餐
|
||||
* 3. 如果存在,抛出重复购买异常
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||
|
||||
@Autowired
|
||||
private OrderV2Mapper orderV2Mapper;
|
||||
|
||||
@Autowired
|
||||
private OrderItemMapper orderItemMapper;
|
||||
|
||||
@Override
|
||||
public DuplicateCheckStrategy getStrategyType() {
|
||||
return DuplicateCheckStrategy.CHECK_BY_SET_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
|
||||
String userId = context.getUserId();
|
||||
String scenicId = context.getScenicId();
|
||||
String productType = context.getProductType();
|
||||
|
||||
// 获取人脸ID(从扩展参数中)
|
||||
Long faceId = context.getParam("faceId");
|
||||
|
||||
log.debug("执行套餐ID重复购买检查: userId={}, faceId={}, scenicId={}, productType={}",
|
||||
userId, faceId, scenicId, productType);
|
||||
|
||||
// 构建查询条件:查找已支付的有效订单
|
||||
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
|
||||
orderQuery.eq("member_id", userId)
|
||||
.eq("scenic_id", scenicId)
|
||||
.eq("payment_status", PaymentStatus.PAID.getCode())
|
||||
.in("order_status",
|
||||
OrderStatus.PAID.getCode(),
|
||||
OrderStatus.PROCESSING.getCode(),
|
||||
OrderStatus.COMPLETED.getCode())
|
||||
.eq("deleted", 0);
|
||||
|
||||
// 如果提供了人脸ID,也作为查询条件
|
||||
if (faceId != null) {
|
||||
orderQuery.eq("face_id", faceId);
|
||||
}
|
||||
|
||||
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
|
||||
|
||||
for (OrderV2 order : existingOrders) {
|
||||
// 检查订单明细中是否包含该类型的套餐
|
||||
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
|
||||
itemQuery.eq("order_id", order.getId())
|
||||
.eq("product_type", productType);
|
||||
|
||||
long count = orderItemMapper.selectCount(itemQuery);
|
||||
if (count > 0) {
|
||||
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
|
||||
userId, faceId, scenicId, productType, order.getId());
|
||||
|
||||
// 为了兼容旧代码,需要转换为 ProductType 枚举
|
||||
com.ycwl.basic.pricing.enums.ProductType productTypeEnum =
|
||||
com.ycwl.basic.pricing.enums.ProductType.fromCode(productType);
|
||||
|
||||
throw new DuplicatePurchaseException(
|
||||
"您已购买过此类型的套餐",
|
||||
order.getId(),
|
||||
order.getOrderNo(),
|
||||
productTypeEnum
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
|
||||
userId, faceId, scenicId, productType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ycwl.basic.order.strategy.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.ycwl.basic.order.entity.OrderItemV2;
|
||||
import com.ycwl.basic.order.entity.OrderV2;
|
||||
import com.ycwl.basic.order.enums.OrderStatus;
|
||||
import com.ycwl.basic.order.enums.PaymentStatus;
|
||||
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
|
||||
import com.ycwl.basic.order.mapper.OrderItemMapper;
|
||||
import com.ycwl.basic.order.mapper.OrderV2Mapper;
|
||||
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 按视频ID检查重复购买策略
|
||||
* 适用于:VLOG_VIDEO 商品类型
|
||||
*
|
||||
* 检查逻辑:
|
||||
* 1. 查找用户在该景区已支付的有效订单
|
||||
* 2. 检查订单明细中是否包含相同的视频ID
|
||||
* 3. 如果存在,抛出重复购买异常
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||
|
||||
@Autowired
|
||||
private OrderV2Mapper orderV2Mapper;
|
||||
|
||||
@Autowired
|
||||
private OrderItemMapper orderItemMapper;
|
||||
|
||||
@Override
|
||||
public DuplicateCheckStrategy getStrategyType() {
|
||||
return DuplicateCheckStrategy.CHECK_BY_VIDEO_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
|
||||
String userId = context.getUserId();
|
||||
String scenicId = context.getScenicId();
|
||||
String productId = context.getProductId();
|
||||
|
||||
// 获取人脸ID(从扩展参数中)
|
||||
Long faceId = context.getParam("faceId");
|
||||
|
||||
log.debug("执行视频ID重复购买检查: userId={}, faceId={}, scenicId={}, videoId={}",
|
||||
userId, faceId, scenicId, productId);
|
||||
|
||||
// 构建查询条件:查找已支付的有效订单
|
||||
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
|
||||
orderQuery.eq("member_id", userId)
|
||||
.eq("scenic_id", scenicId)
|
||||
.eq("payment_status", PaymentStatus.PAID.getCode())
|
||||
.in("order_status",
|
||||
OrderStatus.PAID.getCode(),
|
||||
OrderStatus.PROCESSING.getCode(),
|
||||
OrderStatus.COMPLETED.getCode())
|
||||
.eq("deleted", 0);
|
||||
|
||||
// 如果提供了人脸ID,也作为查询条件
|
||||
if (faceId != null) {
|
||||
orderQuery.eq("face_id", faceId);
|
||||
}
|
||||
|
||||
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
|
||||
|
||||
for (OrderV2 order : existingOrders) {
|
||||
// 检查订单明细中是否包含该视频
|
||||
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
|
||||
itemQuery.eq("order_id", order.getId())
|
||||
.eq("product_type", "VLOG_VIDEO")
|
||||
.eq("product_id", productId);
|
||||
|
||||
long count = orderItemMapper.selectCount(itemQuery);
|
||||
if (count > 0) {
|
||||
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
|
||||
userId, faceId, scenicId, productId, order.getId());
|
||||
|
||||
// 为了兼容旧代码,需要转换为 ProductType 枚举
|
||||
throw new DuplicatePurchaseException(
|
||||
"您已购买过此视频",
|
||||
order.getId(),
|
||||
order.getOrderNo(),
|
||||
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
|
||||
productId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
|
||||
userId, faceId, scenicId, productId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ycwl.basic.pricing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 商品分类枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductCategory {
|
||||
|
||||
VLOG("VLOG", "Vlog类"),
|
||||
PHOTO("PHOTO", "照片类"),
|
||||
VIDEO("VIDEO", "视频类"),
|
||||
PRINT("PRINT", "打印类"),
|
||||
OTHER("OTHER", "其他");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举
|
||||
*/
|
||||
public static ProductCategory fromCode(String code) {
|
||||
for (ProductCategory category : values()) {
|
||||
if (category.code.equals(code)) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown product category code: " + code);
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,29 @@ import lombok.Getter;
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductType {
|
||||
|
||||
VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"),
|
||||
RECORDING_SET("RECORDING_SET", "录像集"),
|
||||
PHOTO_SET("PHOTO_SET", "照相集"),
|
||||
PHOTO_LOG("PHOTO_LOG", "pLog图"),
|
||||
PHOTO_VLOG("PHOTO_VLOG", "pLog视频"),
|
||||
PHOTO_PRINT("PHOTO_PRINT", "照片打印"),
|
||||
PHOTO_PRINT_MU("PHOTO_PRINT_MU", "手机照片打印"),
|
||||
PHOTO_PRINT_FX("PHOTO_PRINT_FX", "特效照片打印"),
|
||||
MACHINE_PRINT("MACHINE_PRINT", "一体机打印");
|
||||
|
||||
|
||||
// VLOG类
|
||||
VLOG_VIDEO("VLOG_VIDEO", "Vlog视频", ProductCategory.VLOG),
|
||||
PHOTO_VLOG("PHOTO_VLOG", "pLog视频", ProductCategory.VLOG),
|
||||
|
||||
// 照片类
|
||||
PHOTO("PHOTO", "照片", ProductCategory.PHOTO),
|
||||
PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO),
|
||||
PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO),
|
||||
|
||||
// 视频类(素材视频)
|
||||
RECORDING_SET("RECORDING_SET", "录像集", ProductCategory.VIDEO),
|
||||
|
||||
// 其他类(打印类等)
|
||||
PHOTO_PRINT("PHOTO_PRINT", "照片打印", ProductCategory.PRINT),
|
||||
PHOTO_PRINT_MU("PHOTO_PRINT_MU", "手机照片打印", ProductCategory.PRINT),
|
||||
PHOTO_PRINT_FX("PHOTO_PRINT_FX", "特效照片打印", ProductCategory.PRINT),
|
||||
MACHINE_PRINT("MACHINE_PRINT", "一体机打印", ProductCategory.PRINT);
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
private final ProductCategory category;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举
|
||||
*/
|
||||
@@ -34,4 +43,18 @@ public enum ProductType {
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown product type code: " + code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类代码
|
||||
*/
|
||||
public String getCategoryCode() {
|
||||
return category.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类描述
|
||||
*/
|
||||
public String getCategoryDescription() {
|
||||
return category.getDescription();
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,14 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1")
|
||||
PriceProductConfig selectByProductTypeAndId(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和景区ID查询配置(支持景区维度)
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND scenic_id = #{scenicId} AND is_active = 1")
|
||||
PriceProductConfig selectByProductTypeIdAndScenic(@Param("productType") String productType,
|
||||
@Param("productId") String productId,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 检查是否存在default配置(包含禁用的)
|
||||
|
||||
@@ -26,6 +26,18 @@ public interface PriceTierConfigMapper extends BaseMapper<PriceTierConfig> {
|
||||
PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType,
|
||||
@Param("productId") String productId,
|
||||
@Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度)
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND product_id = #{productId} AND scenic_id = #{scenicId} " +
|
||||
"AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " +
|
||||
"AND is_active = 1 ORDER BY sort_order ASC LIMIT 1")
|
||||
PriceTierConfig selectByProductTypeQuantityAndScenic(@Param("productType") String productType,
|
||||
@Param("productId") String productId,
|
||||
@Param("quantity") Integer quantity,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 根据商品类型查询所有阶梯配置
|
||||
|
||||
@@ -23,22 +23,55 @@ public interface IProductConfigService {
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取精确配置
|
||||
*
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @return 商品配置
|
||||
*/
|
||||
PriceProductConfig getProductConfig(String productType, String productId);
|
||||
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和景区ID获取精确配置(支持景区维度的优惠策略控制)
|
||||
*
|
||||
* 查询优先级:
|
||||
* 1. 景区+商品ID: (productType, productId, scenicId)
|
||||
* 2. 景区+默认: (productType, "default", scenicId)
|
||||
* 3. 全局+商品ID: (productType, productId, null)
|
||||
* 4. 全局+默认: (productType, "default", null)
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 商品配置(包含优惠策略控制字段)
|
||||
*/
|
||||
PriceProductConfig getProductConfig(String productType, String productId, Long scenicId);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和数量获取阶梯价格配置
|
||||
*
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @param quantity 数量
|
||||
* @return 阶梯价格配置
|
||||
*/
|
||||
PriceTierConfig getTierConfig(String productType, String productId, Integer quantity);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID、数量和景区ID获取阶梯价格配置(支持景区维度)
|
||||
*
|
||||
* 查询优先级:
|
||||
* 1. 景区+商品ID: (productType, productId, quantity, scenicId)
|
||||
* 2. 景区+默认: (productType, "default", quantity, scenicId)
|
||||
* 3. 全局+商品ID: (productType, productId, quantity, null)
|
||||
* 4. 全局+默认: (productType, "default", quantity, null)
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @param quantity 数量
|
||||
* @param scenicId 景区ID
|
||||
* @return 阶梯价格配置
|
||||
*/
|
||||
PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId);
|
||||
|
||||
/**
|
||||
* 获取所有启用的商品配置
|
||||
|
||||
@@ -138,18 +138,18 @@ public class CouponDiscountProvider implements IDiscountProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查单个商品的优惠券使用开关
|
||||
// 检查单个商品的优惠券使用开关(使用景区维度配置)
|
||||
for (ProductItem product : context.getProducts()) {
|
||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||
|
||||
|
||||
try {
|
||||
PriceProductConfig productConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), productId);
|
||||
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
|
||||
if (productConfig != null) {
|
||||
if (!Boolean.TRUE.equals(productConfig.getCanUseCoupon())) {
|
||||
log.debug("商品配置不允许使用优惠券: productType={}, productId={}",
|
||||
product.getProductType().getCode(), productId);
|
||||
log.debug("商品配置不允许使用优惠券: productType={}, productId={}, scenicId={}",
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -157,18 +157,18 @@ public class CouponDiscountProvider implements IDiscountProvider {
|
||||
// 如果获取具体商品配置失败,尝试获取default配置
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), "default");
|
||||
|
||||
product.getProductType().getCode(), "default", context.getScenicId());
|
||||
|
||||
if (defaultConfig != null) {
|
||||
if (!Boolean.TRUE.equals(defaultConfig.getCanUseCoupon())) {
|
||||
log.debug("商品默认配置不允许使用优惠券: productType={}",
|
||||
product.getProductType().getCode());
|
||||
log.debug("商品默认配置不允许使用优惠券: productType={}, scenicId={}",
|
||||
product.getProductType().getCode(), context.getScenicId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("获取商品配置失败,默认允许使用优惠券: productType={}, productId={}",
|
||||
product.getProductType().getCode(), productId);
|
||||
log.warn("获取商品配置失败,默认允许使用优惠券: productType={}, productId={}, scenicId={}",
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
||||
return discounts;
|
||||
}
|
||||
|
||||
// 检查商品是否支持一口价优惠
|
||||
if (!areAllProductsSupportOnePrice(context.getProducts())) {
|
||||
// 检查商品是否支持一口价优惠(使用景区维度配置)
|
||||
if (!areAllProductsSupportOnePrice(context.getProducts(), context.getScenicId())) {
|
||||
log.debug("存在不支持一口价优惠的商品,跳过一口价检测");
|
||||
return discounts;
|
||||
}
|
||||
@@ -182,49 +182,48 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车中的所有商品是否都支持一口价优惠
|
||||
* 检查购物车中的所有商品是否都支持一口价优惠(使用景区维度配置)
|
||||
*/
|
||||
private boolean areAllProductsSupportOnePrice(List<ProductItem> products) {
|
||||
private boolean areAllProductsSupportOnePrice(List<ProductItem> products, Long scenicId) {
|
||||
if (products == null || products.isEmpty()) {
|
||||
return true; // 空购物车时默认支持
|
||||
}
|
||||
|
||||
for (ProductItem product : products) {
|
||||
try {
|
||||
// 查询商品配置
|
||||
// 查询商品配置(使用景区维度)
|
||||
PriceProductConfig productConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), product.getProductId());
|
||||
product.getProductType().getCode(), product.getProductId(), scenicId);
|
||||
|
||||
if (productConfig != null) {
|
||||
// 检查商品是否支持一口价优惠
|
||||
if (Boolean.FALSE.equals(productConfig.getCanUseOnePrice())) {
|
||||
log.debug("商品 {}({}) 不支持一口价优惠",
|
||||
product.getProductType().getCode(), product.getProductId());
|
||||
log.debug("商品 {}({}) 在景区 {} 不支持一口价优惠",
|
||||
product.getProductType().getCode(), product.getProductId(), scenicId);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 如果找不到具体商品配置,尝试查询 default 配置
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), "default");
|
||||
product.getProductType().getCode(), "default", scenicId);
|
||||
|
||||
if (defaultConfig != null) {
|
||||
if (Boolean.FALSE.equals(defaultConfig.getCanUseOnePrice())) {
|
||||
log.debug("商品类型 {} 的默认配置不支持一口价优惠",
|
||||
product.getProductType().getCode());
|
||||
log.debug("商品类型 {} 在景区 {} 的默认配置不支持一口价优惠",
|
||||
product.getProductType().getCode(), scenicId);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 如果既没有具体配置也没有默认配置,默认不支持一口价优惠
|
||||
log.debug("商品 {}({}) 未找到价格配置,默认不支持一口价优惠",
|
||||
product.getProductType().getCode(), product.getProductId());
|
||||
return false;
|
||||
// 如果既没有具体配置也没有默认配置,默认支持(保持向后兼容)
|
||||
log.debug("商品 {}({}) 在景区 {} 未找到价格配置,默认支持一口价优惠",
|
||||
product.getProductType().getCode(), product.getProductId(), scenicId);
|
||||
// 改为默认支持,避免配置缺失导致一口价功能不可用
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("检查商品 {}({}) 一口价优惠支持情况时发生异常,默认不支持",
|
||||
product.getProductType().getCode(), product.getProductId(), e);
|
||||
// 异常情况下默认不支持,避免出现意外情况
|
||||
return false;
|
||||
log.warn("检查商品 {}({}) 在景区 {} 一口价优惠支持情况时发生异常,默认支持",
|
||||
product.getProductType().getCode(), product.getProductId(), scenicId, e);
|
||||
// 异常情况下默认支持,确保业务流程不受影响
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ 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.*;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,16 +32,18 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
private final IPriceBundleService bundleService;
|
||||
private final IDiscountDetectionService discountDetectionService;
|
||||
private final IVoucherService voucherService;
|
||||
private final IProductTypeCapabilityService productTypeCapabilityService;
|
||||
|
||||
/**
|
||||
* 判断是否为打印类商品
|
||||
* 打印类商品的价格计算方式为:单价 × 数量
|
||||
* 判断是否为按数量计价的商品
|
||||
* 重构: 使用商品类型能力配置替代硬编码
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return true-按数量计价, false-固定价格
|
||||
*/
|
||||
private boolean isPrintProduct(ProductType productType) {
|
||||
return productType == ProductType.PHOTO_PRINT
|
||||
|| productType == ProductType.PHOTO_PRINT_MU
|
||||
|| productType == ProductType.PHOTO_PRINT_FX
|
||||
|| productType == ProductType.MACHINE_PRINT;
|
||||
private boolean isQuantityBasedPricing(String productType) {
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability(productType);
|
||||
return capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -61,8 +66,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
product.setQuantity(1);
|
||||
}
|
||||
});
|
||||
// 计算商品价格和原价
|
||||
PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts());
|
||||
// 计算商品价格和原价(传入景区ID以支持景区级优惠策略控制)
|
||||
PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts(), request.getScenicId());
|
||||
BigDecimal totalAmount = priceDetails.getTotalAmount();
|
||||
BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount();
|
||||
|
||||
@@ -156,27 +161,27 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
|
||||
return totalAmount.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private PriceDetails calculateProductsPriceWithOriginal(List<ProductItem> products) {
|
||||
|
||||
private PriceDetails calculateProductsPriceWithOriginal(List<ProductItem> products, Long scenicId) {
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
BigDecimal originalTotalAmount = BigDecimal.ZERO;
|
||||
|
||||
|
||||
for (ProductItem product : products) {
|
||||
// 计算实际价格和原价
|
||||
ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product);
|
||||
|
||||
// 计算实际价格和原价(传入景区ID)
|
||||
ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product, scenicId);
|
||||
|
||||
product.setUnitPrice(priceInfo.getActualPrice());
|
||||
product.setOriginalPrice(priceInfo.getOriginalPrice());
|
||||
|
||||
|
||||
BigDecimal subtotal = priceInfo.getActualPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount()));
|
||||
BigDecimal originalSubtotal = priceInfo.getOriginalPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount()));
|
||||
|
||||
|
||||
product.setSubtotal(subtotal);
|
||||
|
||||
|
||||
totalAmount = totalAmount.add(subtotal);
|
||||
originalTotalAmount = originalTotalAmount.add(originalSubtotal);
|
||||
}
|
||||
|
||||
|
||||
return new PriceDetails(
|
||||
totalAmount.setScale(2, RoundingMode.HALF_UP),
|
||||
originalTotalAmount.setScale(2, RoundingMode.HALF_UP)
|
||||
@@ -201,7 +206,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
try {
|
||||
PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId);
|
||||
if (baseConfig != null) {
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return baseConfig.getBasePrice();
|
||||
@@ -216,7 +221,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default");
|
||||
if (defaultConfig != null) {
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
return defaultConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return defaultConfig.getBasePrice();
|
||||
@@ -230,7 +235,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return baseConfig.getBasePrice();
|
||||
@@ -239,32 +244,32 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
|
||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||
}
|
||||
|
||||
private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) {
|
||||
|
||||
private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product, Long scenicId) {
|
||||
ProductType productType = product.getProductType();
|
||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||
|
||||
|
||||
BigDecimal actualPrice;
|
||||
BigDecimal originalPrice = null;
|
||||
|
||||
// 优先使用基于product_id的阶梯定价
|
||||
|
||||
// 优先使用基于product_id的阶梯定价(带景区ID)
|
||||
PriceTierConfig tierConfig = productConfigService.getTierConfig(
|
||||
productType.getCode(), productId, product.getQuantity());
|
||||
|
||||
productType.getCode(), productId, product.getQuantity(), scenicId);
|
||||
|
||||
if (tierConfig != null) {
|
||||
actualPrice = tierConfig.getPrice();
|
||||
originalPrice = tierConfig.getOriginalPrice();
|
||||
log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}",
|
||||
productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice);
|
||||
log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, scenicId={}, price={}, originalPrice={}",
|
||||
productType.getCode(), productId, product.getQuantity(), scenicId, actualPrice, originalPrice);
|
||||
} else {
|
||||
// 使用基于product_id的基础配置
|
||||
// 使用基于product_id的基础配置(带景区ID)
|
||||
try {
|
||||
PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId);
|
||||
PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId, scenicId);
|
||||
if (baseConfig != null) {
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
@@ -274,17 +279,17 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
throw new PriceCalculationException("无法找到具体商品配置");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置",
|
||||
productType, productId);
|
||||
|
||||
// 兜底:使用default配置
|
||||
log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置",
|
||||
productType, productId, scenicId);
|
||||
|
||||
// 兜底:使用default配置(带景区ID)
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default");
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default", scenicId);
|
||||
if (defaultConfig != null) {
|
||||
actualPrice = defaultConfig.getBasePrice();
|
||||
originalPrice = defaultConfig.getOriginalPrice();
|
||||
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
@@ -294,8 +299,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
throw new PriceCalculationException("无法找到default配置");
|
||||
}
|
||||
} catch (Exception defaultEx) {
|
||||
log.warn("未找到default配置: productType={}", productType.getCode());
|
||||
|
||||
log.warn("未找到default配置: productType={}, scenicId={}", productType.getCode(), scenicId);
|
||||
|
||||
// 最后兜底:使用通用配置(向后兼容)
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
@@ -303,7 +308,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (isPrintProduct(productType)) {
|
||||
if (isQuantityBasedPricing(productType.getCode())) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
@@ -315,7 +320,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new ProductPriceInfo(actualPrice, originalPrice);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,107 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
productType, productId, quantity, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "product-config", key = "#productType + '_' + #productId + '_' + #scenicId")
|
||||
public PriceProductConfig getProductConfig(String productType, String productId, Long scenicId) {
|
||||
if (scenicId == null) {
|
||||
// 如果没有景区ID,使用原有逻辑
|
||||
return getProductConfig(productType, productId);
|
||||
}
|
||||
|
||||
// 查询优先级:
|
||||
// 1. 景区+商品ID
|
||||
PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic(
|
||||
productType, productId, scenicId.toString());
|
||||
if (config != null) {
|
||||
log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}",
|
||||
productType, productId, scenicId);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 2. 景区+默认
|
||||
if (!"default".equals(productId)) {
|
||||
config = productConfigMapper.selectByProductTypeIdAndScenic(
|
||||
productType, "default", scenicId.toString());
|
||||
if (config != null) {
|
||||
log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 全局+商品ID (兜底)
|
||||
try {
|
||||
config = productConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||
if (config != null) {
|
||||
log.debug("使用全局商品配置: productType={}, productId={}", productType, productId);
|
||||
return config;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId);
|
||||
}
|
||||
|
||||
// 4. 全局+默认 (最后兜底)
|
||||
config = productConfigMapper.selectByProductTypeAndId(productType, "default");
|
||||
if (config != null) {
|
||||
log.debug("使用全局默认配置: productType={}", productType);
|
||||
return config;
|
||||
}
|
||||
|
||||
throw new ProductConfigNotFoundException(
|
||||
String.format("商品配置未找到: productType=%s, productId=%s, scenicId=%s",
|
||||
productType, productId, scenicId));
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity + '_' + #scenicId")
|
||||
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId) {
|
||||
if (quantity == null || quantity <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scenicId == null) {
|
||||
// 如果没有景区ID,使用原有逻辑
|
||||
return getTierConfig(productType, productId, quantity);
|
||||
}
|
||||
|
||||
// 查询优先级:
|
||||
// 1. 景区+商品ID
|
||||
PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
|
||||
productType, productId, quantity, scenicId.toString());
|
||||
if (config != null) {
|
||||
log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}",
|
||||
productType, productId, quantity, scenicId);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 2. 景区+默认
|
||||
if (!"default".equals(productId)) {
|
||||
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
|
||||
productType, "default", quantity, scenicId.toString());
|
||||
if (config != null) {
|
||||
log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}",
|
||||
productType, quantity, scenicId);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 全局+商品ID (兜底)
|
||||
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity);
|
||||
if (config != null) {
|
||||
log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}",
|
||||
productType, productId, quantity);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 4. 全局+默认 (最后兜底)
|
||||
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity);
|
||||
if (config != null) {
|
||||
log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "active-product-configs")
|
||||
|
||||
@@ -224,18 +224,18 @@ public class VoucherDiscountProvider implements IDiscountProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查单个商品的券码使用开关
|
||||
// 检查单个商品的券码使用开关(使用景区维度配置)
|
||||
for (ProductItem product : context.getProducts()) {
|
||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||
|
||||
|
||||
try {
|
||||
PriceProductConfig productConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), productId);
|
||||
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
|
||||
if (productConfig != null) {
|
||||
if (!Boolean.TRUE.equals(productConfig.getCanUseVoucher())) {
|
||||
log.info("商品配置不允许使用券码: productType={}, productId={}",
|
||||
product.getProductType().getCode(), productId);
|
||||
log.info("商品配置不允许使用券码: productType={}, productId={}, scenicId={}",
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -243,18 +243,18 @@ public class VoucherDiscountProvider implements IDiscountProvider {
|
||||
// 如果获取具体商品配置失败,尝试获取default配置
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
|
||||
product.getProductType().getCode(), "default");
|
||||
|
||||
product.getProductType().getCode(), "default", context.getScenicId());
|
||||
|
||||
if (defaultConfig != null) {
|
||||
if (!Boolean.TRUE.equals(defaultConfig.getCanUseVoucher())) {
|
||||
log.debug("商品默认配置不允许使用券码: productType={}",
|
||||
product.getProductType().getCode());
|
||||
log.debug("商品默认配置不允许使用券码: productType={}, scenicId={}",
|
||||
product.getProductType().getCode(), context.getScenicId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("获取商品配置失败,默认允许使用券码: productType={}, productId={}",
|
||||
product.getProductType().getCode(), productId);
|
||||
log.warn("获取商品配置失败,默认允许使用券码: productType={}, productId={}, scenicId={}",
|
||||
product.getProductType().getCode(), productId, context.getScenicId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ycwl.basic.product.capability;
|
||||
|
||||
/**
|
||||
* 重复购买检查策略枚举
|
||||
* 定义不同商品类型的重复购买检查规则
|
||||
*/
|
||||
public enum DuplicateCheckStrategy {
|
||||
|
||||
/**
|
||||
* 不检查(如打印类商品)
|
||||
* 允许重复购买
|
||||
*/
|
||||
NO_CHECK("NO_CHECK", "不检查"),
|
||||
|
||||
/**
|
||||
* 按视频ID检查(VLOG_VIDEO)
|
||||
* 同一个视频不允许重复购买
|
||||
*/
|
||||
CHECK_BY_VIDEO_ID("CHECK_BY_VIDEO_ID", "按视频ID检查"),
|
||||
|
||||
/**
|
||||
* 按套餐ID检查(RECORDING_SET, PHOTO_SET)
|
||||
* 同一个套餐不允许重复购买
|
||||
*/
|
||||
CHECK_BY_SET_ID("CHECK_BY_SET_ID", "按套餐ID检查"),
|
||||
|
||||
/**
|
||||
* 自定义策略(通过扩展实现)
|
||||
* 预留扩展点,用于未来更复杂的检查逻辑
|
||||
*/
|
||||
CUSTOM("CUSTOM", "自定义策略");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
DuplicateCheckStrategy(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举值
|
||||
*/
|
||||
public static DuplicateCheckStrategy fromCode(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (DuplicateCheckStrategy strategy : DuplicateCheckStrategy.values()) {
|
||||
if (strategy.code.equals(code)) {
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知的重复检查策略: " + code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ycwl.basic.product.capability;
|
||||
|
||||
/**
|
||||
* 定价模式枚举
|
||||
* 定义商品的价格计算方式
|
||||
*/
|
||||
public enum PricingMode {
|
||||
|
||||
/**
|
||||
* 固定价格(不受数量影响)
|
||||
* 适用于:视频类、照片集类商品
|
||||
*/
|
||||
FIXED("FIXED", "固定价格"),
|
||||
|
||||
/**
|
||||
* 基于数量(价格 = 单价 × 数量)
|
||||
* 适用于:打印类商品
|
||||
*/
|
||||
QUANTITY_BASED("QUANTITY_BASED", "基于数量"),
|
||||
|
||||
/**
|
||||
* 分层定价(根据数量区间)
|
||||
* 适用于:支持阶梯定价的商品
|
||||
*/
|
||||
TIERED("TIERED", "分层定价");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
PricingMode(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举值
|
||||
*/
|
||||
public static PricingMode fromCode(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (PricingMode mode : PricingMode.values()) {
|
||||
if (mode.code.equals(code)) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知的定价模式: " + code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.ycwl.basic.product.capability;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置实体
|
||||
* 统一管理商品类型的各项能力和行为特征
|
||||
*
|
||||
* 设计目标:
|
||||
* 1. 消除代码中的硬编码,将商品类型相关的业务规则配置化
|
||||
* 2. 提供统一的商品类型能力查询接口
|
||||
* 3. 支持通过配置快速扩展新商品类型
|
||||
*/
|
||||
@Data
|
||||
@TableName(value = "product_type_capability", autoResultMap = true)
|
||||
public class ProductTypeCapability {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品类型代码(唯一)
|
||||
* 如:VLOG_VIDEO, PHOTO_PRINT 等
|
||||
*/
|
||||
private String productType;
|
||||
|
||||
/**
|
||||
* 显示名称
|
||||
* 用于前端展示和支付页面
|
||||
*/
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* 商品分类
|
||||
* 如:VIDEO(视频类), PRINT(打印类), PHOTO_SET(照片集类)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
// ========== 定价相关 ==========
|
||||
|
||||
/**
|
||||
* 定价模式
|
||||
* FIXED: 固定价格
|
||||
* QUANTITY_BASED: 基于数量(价格 = 单价 × 数量)
|
||||
* TIERED: 分层定价
|
||||
*/
|
||||
private String pricingMode;
|
||||
|
||||
/**
|
||||
* 是否支持阶梯定价
|
||||
*/
|
||||
private Boolean supportsTierPricing;
|
||||
|
||||
// ========== 购买限制 ==========
|
||||
|
||||
/**
|
||||
* 是否允许重复购买
|
||||
*/
|
||||
private Boolean allowDuplicatePurchase;
|
||||
|
||||
/**
|
||||
* 重复购买检查策略
|
||||
* NO_CHECK: 不检查
|
||||
* CHECK_BY_VIDEO_ID: 按视频ID检查
|
||||
* CHECK_BY_SET_ID: 按套餐ID检查
|
||||
* CUSTOM: 自定义策略
|
||||
*/
|
||||
private String duplicateCheckStrategy;
|
||||
|
||||
// ========== 优惠能力 ==========
|
||||
|
||||
/**
|
||||
* 是否可使用优惠券
|
||||
*/
|
||||
private Boolean canUseCoupon;
|
||||
|
||||
/**
|
||||
* 是否可使用券码
|
||||
*/
|
||||
private Boolean canUseVoucher;
|
||||
|
||||
/**
|
||||
* 是否可使用一口价优惠
|
||||
*/
|
||||
private Boolean canUseOnePrice;
|
||||
|
||||
/**
|
||||
* 是否可参与打包优惠
|
||||
*/
|
||||
private Boolean canUseBundle;
|
||||
|
||||
// ========== 扩展属性 ==========
|
||||
|
||||
/**
|
||||
* 扩展属性(JSON 格式)
|
||||
* 用于存储特定商品类型的额外配置
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// ========== 便捷方法 ==========
|
||||
|
||||
/**
|
||||
* 获取定价模式枚举
|
||||
*/
|
||||
public PricingMode getPricingModeEnum() {
|
||||
return PricingMode.fromCode(this.pricingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重复检查策略枚举
|
||||
*/
|
||||
public DuplicateCheckStrategy getDuplicateCheckStrategyEnum() {
|
||||
return DuplicateCheckStrategy.fromCode(this.duplicateCheckStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置定价模式枚举
|
||||
*/
|
||||
public void setPricingModeEnum(PricingMode mode) {
|
||||
this.pricingMode = mode != null ? mode.getCode() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置重复检查策略枚举
|
||||
*/
|
||||
public void setDuplicateCheckStrategyEnum(DuplicateCheckStrategy strategy) {
|
||||
this.duplicateCheckStrategy = strategy != null ? strategy.getCode() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.ycwl.basic.product.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.pricing.enums.ProductCategory;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.dto.EnumOptionResponse;
|
||||
import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest;
|
||||
import com.ycwl.basic.product.dto.ProductTypeCapabilityResponse;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置管理控制器
|
||||
* 提供管理端的配置管理功能
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/product/admin/capability")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductTypeCapabilityController {
|
||||
|
||||
private final IProductTypeCapabilityManagementService managementService;
|
||||
private final IProductTypeCapabilityService capabilityService;
|
||||
|
||||
/**
|
||||
* 分页查询商品类型能力配置
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public ApiResponse<Page<ProductTypeCapabilityResponse>> queryByPage(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String productType,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) Boolean isActive) {
|
||||
|
||||
Page<ProductTypeCapability> page = managementService.queryByPage(
|
||||
pageNum, pageSize, productType, category, isActive);
|
||||
|
||||
// 转换为响应DTO
|
||||
Page<ProductTypeCapabilityResponse> responsePage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
List<ProductTypeCapabilityResponse> responseList = page.getRecords().stream()
|
||||
.map(ProductTypeCapabilityResponse::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
responsePage.setRecords(responseList);
|
||||
|
||||
return ApiResponse.success(responsePage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有商品类型能力配置
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<List<ProductTypeCapabilityResponse>> queryAll(
|
||||
@RequestParam(defaultValue = "false") boolean includeInactive) {
|
||||
|
||||
List<ProductTypeCapability> capabilities = managementService.queryAll(includeInactive);
|
||||
List<ProductTypeCapabilityResponse> responseList = capabilities.stream()
|
||||
.map(ProductTypeCapabilityResponse::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ApiResponse.success(responseList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类查询商品类型能力配置
|
||||
*/
|
||||
@GetMapping("/category/{category}")
|
||||
public ApiResponse<List<ProductTypeCapabilityResponse>> queryByCategory(
|
||||
@PathVariable String category) {
|
||||
|
||||
List<ProductTypeCapability> capabilities = managementService.queryByCategory(category);
|
||||
List<ProductTypeCapabilityResponse> responseList = capabilities.stream()
|
||||
.map(ProductTypeCapabilityResponse::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ApiResponse.success(responseList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询配置详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<ProductTypeCapabilityResponse> getById(
|
||||
@PathVariable Long id) {
|
||||
|
||||
ProductTypeCapability capability = managementService.getById(id);
|
||||
return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商品类型代码查询配置详情
|
||||
*/
|
||||
@GetMapping("/product-type/{productType}")
|
||||
public ApiResponse<ProductTypeCapabilityResponse> getByProductType(
|
||||
@PathVariable String productType) {
|
||||
|
||||
ProductTypeCapability capability = managementService.getByProductType(productType);
|
||||
return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建商品类型能力配置
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<ProductTypeCapabilityResponse> create(
|
||||
@RequestBody ProductTypeCapabilityRequest request) {
|
||||
|
||||
ProductTypeCapability capability = managementService.create(request);
|
||||
return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品类型能力配置
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<ProductTypeCapabilityResponse> update(
|
||||
@PathVariable Long id,
|
||||
@RequestBody ProductTypeCapabilityRequest request) {
|
||||
|
||||
ProductTypeCapability capability = managementService.update(id, request);
|
||||
return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品类型能力配置
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> delete(
|
||||
@PathVariable Long id) {
|
||||
|
||||
managementService.delete(id);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用商品类型能力配置
|
||||
*/
|
||||
@PutMapping("/{id}/status")
|
||||
public ApiResponse<Void> updateStatus(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Boolean isActive) {
|
||||
|
||||
managementService.updateStatus(id, isActive);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量初始化商品类型能力配置
|
||||
* 为所有已定义的商品类型创建默认配置
|
||||
*/
|
||||
@PostMapping("/init-defaults")
|
||||
public ApiResponse<Integer> initializeDefaultCapabilities() {
|
||||
|
||||
int count = managementService.initializeDefaultCapabilities();
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有缓存
|
||||
*/
|
||||
@PostMapping("/refresh-cache")
|
||||
public ApiResponse<Void> refreshCache() {
|
||||
|
||||
capabilityService.refreshCache();
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新指定商品类型的缓存
|
||||
*/
|
||||
@PostMapping("/refresh-cache/{productType}")
|
||||
public ApiResponse<Void> refreshCacheByProductType(
|
||||
@PathVariable String productType) {
|
||||
|
||||
capabilityService.refreshCache(productType);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
// ========== 枚举选项接口 ==========
|
||||
|
||||
/**
|
||||
* 获取定价模式枚举选项
|
||||
*/
|
||||
@GetMapping("/enums/pricing-modes")
|
||||
public ApiResponse<List<EnumOptionResponse>> getPricingModes() {
|
||||
|
||||
List<EnumOptionResponse> options = Arrays.stream(PricingMode.values())
|
||||
.map(mode -> new EnumOptionResponse(mode.getCode(), mode.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ApiResponse.success(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重复检查策略枚举选项
|
||||
*/
|
||||
@GetMapping("/enums/duplicate-check-strategies")
|
||||
public ApiResponse<List<EnumOptionResponse>> getDuplicateCheckStrategies() {
|
||||
|
||||
List<EnumOptionResponse> options = Arrays.stream(DuplicateCheckStrategy.values())
|
||||
.map(strategy -> new EnumOptionResponse(strategy.getCode(), strategy.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ApiResponse.success(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品分类枚举选项
|
||||
*/
|
||||
@GetMapping("/enums/categories")
|
||||
public ApiResponse<List<EnumOptionResponse>> getCategories() {
|
||||
|
||||
List<EnumOptionResponse> options = Arrays.stream(ProductCategory.values())
|
||||
.map(category -> new EnumOptionResponse(category.getCode(), category.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ApiResponse.success(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ycwl.basic.product.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 枚举选项响应DTO
|
||||
* 用于前端下拉框等场景
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EnumOptionResponse {
|
||||
|
||||
/**
|
||||
* 枚举代码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 枚举描述
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ycwl.basic.product.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class ProductTypeCapabilityRequest {
|
||||
|
||||
/**
|
||||
* 商品类型代码(唯一)
|
||||
* 如:VLOG_VIDEO, PHOTO_PRINT 等
|
||||
*/
|
||||
private String productType;
|
||||
|
||||
/**
|
||||
* 显示名称
|
||||
*/
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* 商品分类
|
||||
* VLOG, PHOTO, VIDEO, PRINT, OTHER
|
||||
*/
|
||||
private String category;
|
||||
|
||||
// ========== 定价相关 ==========
|
||||
|
||||
/**
|
||||
* 定价模式
|
||||
* FIXED: 固定价格
|
||||
* QUANTITY_BASED: 基于数量
|
||||
* TIERED: 分层定价
|
||||
*/
|
||||
private String pricingMode;
|
||||
|
||||
/**
|
||||
* 是否支持阶梯定价
|
||||
*/
|
||||
private Boolean supportsTierPricing;
|
||||
|
||||
// ========== 购买限制 ==========
|
||||
|
||||
/**
|
||||
* 是否允许重复购买
|
||||
*/
|
||||
private Boolean allowDuplicatePurchase;
|
||||
|
||||
/**
|
||||
* 重复购买检查策略
|
||||
* NO_CHECK, CHECK_BY_VIDEO_ID, CHECK_BY_SET_ID, CUSTOM
|
||||
*/
|
||||
private String duplicateCheckStrategy;
|
||||
|
||||
// ========== 优惠能力 ==========
|
||||
|
||||
/**
|
||||
* 是否可使用优惠券
|
||||
*/
|
||||
private Boolean canUseCoupon;
|
||||
|
||||
/**
|
||||
* 是否可使用券码
|
||||
*/
|
||||
private Boolean canUseVoucher;
|
||||
|
||||
/**
|
||||
* 是否可使用一口价优惠
|
||||
*/
|
||||
private Boolean canUseOnePrice;
|
||||
|
||||
/**
|
||||
* 是否可参与打包优惠
|
||||
*/
|
||||
private Boolean canUseBundle;
|
||||
|
||||
// ========== 扩展属性 ==========
|
||||
|
||||
/**
|
||||
* 扩展属性(JSON 格式)
|
||||
*/
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.ycwl.basic.product.dto;
|
||||
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class ProductTypeCapabilityResponse {
|
||||
|
||||
private Long id;
|
||||
private String productType;
|
||||
private String displayName;
|
||||
private String category;
|
||||
private String categoryDescription;
|
||||
|
||||
// 定价相关
|
||||
private String pricingMode;
|
||||
private String pricingModeDescription;
|
||||
private Boolean supportsTierPricing;
|
||||
|
||||
// 购买限制
|
||||
private Boolean allowDuplicatePurchase;
|
||||
private String duplicateCheckStrategy;
|
||||
private String duplicateCheckStrategyDescription;
|
||||
|
||||
// 优惠能力
|
||||
private Boolean canUseCoupon;
|
||||
private Boolean canUseVoucher;
|
||||
private Boolean canUseOnePrice;
|
||||
private Boolean canUseBundle;
|
||||
|
||||
// 扩展属性
|
||||
private Map<String, Object> metadata;
|
||||
private Boolean isActive;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 从实体转换为响应DTO
|
||||
*/
|
||||
public static ProductTypeCapabilityResponse fromEntity(ProductTypeCapability entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ProductTypeCapabilityResponse response = new ProductTypeCapabilityResponse();
|
||||
response.setId(entity.getId());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setDisplayName(entity.getDisplayName());
|
||||
response.setCategory(entity.getCategory());
|
||||
|
||||
// 定价相关
|
||||
response.setPricingMode(entity.getPricingMode());
|
||||
if (entity.getPricingModeEnum() != null) {
|
||||
response.setPricingModeDescription(entity.getPricingModeEnum().getDescription());
|
||||
}
|
||||
response.setSupportsTierPricing(entity.getSupportsTierPricing());
|
||||
|
||||
// 购买限制
|
||||
response.setAllowDuplicatePurchase(entity.getAllowDuplicatePurchase());
|
||||
response.setDuplicateCheckStrategy(entity.getDuplicateCheckStrategy());
|
||||
if (entity.getDuplicateCheckStrategyEnum() != null) {
|
||||
response.setDuplicateCheckStrategyDescription(entity.getDuplicateCheckStrategyEnum().getDescription());
|
||||
}
|
||||
|
||||
// 优惠能力
|
||||
response.setCanUseCoupon(entity.getCanUseCoupon());
|
||||
response.setCanUseVoucher(entity.getCanUseVoucher());
|
||||
response.setCanUseOnePrice(entity.getCanUseOnePrice());
|
||||
response.setCanUseBundle(entity.getCanUseBundle());
|
||||
|
||||
// 扩展属性
|
||||
response.setMetadata(entity.getMetadata());
|
||||
response.setIsActive(entity.getIsActive());
|
||||
response.setCreatedAt(entity.getCreatedAt());
|
||||
response.setUpdatedAt(entity.getUpdatedAt());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ycwl.basic.product.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProductTypeCapabilityMapper extends BaseMapper<ProductTypeCapability> {
|
||||
|
||||
/**
|
||||
* 根据商品类型代码查询能力配置
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return 能力配置,不存在时返回 null
|
||||
*/
|
||||
ProductTypeCapability selectByProductType(@Param("productType") String productType);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.ycwl.basic.product.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置管理服务接口
|
||||
* 用于管理端的配置管理功能
|
||||
*/
|
||||
public interface IProductTypeCapabilityManagementService {
|
||||
|
||||
/**
|
||||
* 分页查询商品类型能力配置
|
||||
*
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页大小
|
||||
* @param productType 商品类型代码(可选,支持模糊查询)
|
||||
* @param category 商品分类(可选)
|
||||
* @param isActive 是否启用(可选)
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<ProductTypeCapability> queryByPage(int pageNum, int pageSize,
|
||||
String productType, String category, Boolean isActive);
|
||||
|
||||
/**
|
||||
* 查询所有商品类型能力配置
|
||||
*
|
||||
* @param includeInactive 是否包含禁用的配置
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<ProductTypeCapability> queryAll(boolean includeInactive);
|
||||
|
||||
/**
|
||||
* 根据分类查询商品类型能力配置
|
||||
*
|
||||
* @param category 商品分类
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<ProductTypeCapability> queryByCategory(String category);
|
||||
|
||||
/**
|
||||
* 根据ID查询配置详情
|
||||
*
|
||||
* @param id 配置ID
|
||||
* @return 配置详情
|
||||
*/
|
||||
ProductTypeCapability getById(Long id);
|
||||
|
||||
/**
|
||||
* 根据商品类型代码查询配置详情
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return 配置详情
|
||||
*/
|
||||
ProductTypeCapability getByProductType(String productType);
|
||||
|
||||
/**
|
||||
* 创建商品类型能力配置
|
||||
*
|
||||
* @param request 请求参数
|
||||
* @return 创建的配置
|
||||
*/
|
||||
ProductTypeCapability create(ProductTypeCapabilityRequest request);
|
||||
|
||||
/**
|
||||
* 更新商品类型能力配置
|
||||
*
|
||||
* @param id 配置ID
|
||||
* @param request 请求参数
|
||||
* @return 更新后的配置
|
||||
*/
|
||||
ProductTypeCapability update(Long id, ProductTypeCapabilityRequest request);
|
||||
|
||||
/**
|
||||
* 删除商品类型能力配置
|
||||
*
|
||||
* @param id 配置ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用商品类型能力配置
|
||||
*
|
||||
* @param id 配置ID
|
||||
* @param isActive 是否启用
|
||||
*/
|
||||
void updateStatus(Long id, Boolean isActive);
|
||||
|
||||
/**
|
||||
* 批量初始化商品类型能力配置
|
||||
* 为所有已定义的商品类型创建默认配置
|
||||
*
|
||||
* @return 初始化的配置数量
|
||||
*/
|
||||
int initializeDefaultCapabilities();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.product.service;
|
||||
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
|
||||
/**
|
||||
* 商品类型能力服务接口
|
||||
* 提供商品类型能力配置的查询和管理功能
|
||||
*
|
||||
* 设计原则:
|
||||
* 1. 缓存优先:所有查询都支持缓存,提高性能
|
||||
* 2. 降级兜底:查询失败时返回安全的默认配置
|
||||
* 3. 接口简洁:提供便捷方法,简化调用方代码
|
||||
*/
|
||||
public interface IProductTypeCapabilityService {
|
||||
|
||||
/**
|
||||
* 获取商品类型能力配置(支持缓存)
|
||||
*
|
||||
* @param productType 商品类型代码(如 VLOG_VIDEO)
|
||||
* @return 商品类型能力配置,查询失败时返回默认配置
|
||||
*/
|
||||
ProductTypeCapability getCapability(String productType);
|
||||
|
||||
/**
|
||||
* 获取商品显示名称
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return 显示名称,查询失败时返回"景区商品"
|
||||
*/
|
||||
String getDisplayName(String productType);
|
||||
|
||||
/**
|
||||
* 判断是否允许重复购买
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return true-允许重复购买, false-不允许
|
||||
*/
|
||||
boolean allowDuplicatePurchase(String productType);
|
||||
|
||||
/**
|
||||
* 获取定价模式
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return 定价模式枚举
|
||||
*/
|
||||
PricingMode getPricingMode(String productType);
|
||||
|
||||
/**
|
||||
* 获取重复检查策略
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
* @return 重复检查策略枚举
|
||||
*/
|
||||
DuplicateCheckStrategy getDuplicateCheckStrategy(String productType);
|
||||
|
||||
/**
|
||||
* 刷新缓存
|
||||
* 用于配置更新后手动触发缓存刷新
|
||||
*/
|
||||
void refreshCache();
|
||||
|
||||
/**
|
||||
* 刷新指定商品类型的缓存
|
||||
*
|
||||
* @param productType 商品类型代码
|
||||
*/
|
||||
void refreshCache(String productType);
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package com.ycwl.basic.product.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.pricing.enums.ProductCategory;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest;
|
||||
import com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品类型能力配置管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProductTypeCapabilityManagementServiceImpl implements IProductTypeCapabilityManagementService {
|
||||
|
||||
private final ProductTypeCapabilityMapper mapper;
|
||||
private final IProductTypeCapabilityService capabilityService;
|
||||
|
||||
@Override
|
||||
public Page<ProductTypeCapability> queryByPage(int pageNum, int pageSize,
|
||||
String productType, String category, Boolean isActive) {
|
||||
Page<ProductTypeCapability> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<ProductTypeCapability> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (productType != null && !productType.trim().isEmpty()) {
|
||||
queryWrapper.like(ProductTypeCapability::getProductType, productType);
|
||||
}
|
||||
|
||||
if (category != null && !category.trim().isEmpty()) {
|
||||
queryWrapper.eq(ProductTypeCapability::getCategory, category);
|
||||
}
|
||||
|
||||
if (isActive != null) {
|
||||
queryWrapper.eq(ProductTypeCapability::getIsActive, isActive);
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc(ProductTypeCapability::getCreatedAt);
|
||||
|
||||
return mapper.selectPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductTypeCapability> queryAll(boolean includeInactive) {
|
||||
LambdaQueryWrapper<ProductTypeCapability> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (!includeInactive) {
|
||||
queryWrapper.eq(ProductTypeCapability::getIsActive, true);
|
||||
}
|
||||
|
||||
queryWrapper.orderBy(true, true, ProductTypeCapability::getCategory, ProductTypeCapability::getProductType);
|
||||
|
||||
return mapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductTypeCapability> queryByCategory(String category) {
|
||||
if (category == null || category.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<ProductTypeCapability> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(ProductTypeCapability::getCategory, category);
|
||||
queryWrapper.eq(ProductTypeCapability::getIsActive, true);
|
||||
queryWrapper.orderByAsc(ProductTypeCapability::getProductType);
|
||||
|
||||
return mapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProductTypeCapability getById(Long id) {
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("配置ID不能为空");
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = mapper.selectById(id);
|
||||
if (capability == null) {
|
||||
throw new IllegalArgumentException("配置不存在: " + id);
|
||||
}
|
||||
|
||||
return capability;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProductTypeCapability getByProductType(String productType) {
|
||||
if (productType == null || productType.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("商品类型代码不能为空");
|
||||
}
|
||||
|
||||
return mapper.selectByProductType(productType);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public ProductTypeCapability create(ProductTypeCapabilityRequest request) {
|
||||
validateRequest(request, true);
|
||||
|
||||
// 检查商品类型代码是否已存在
|
||||
ProductTypeCapability existing = mapper.selectByProductType(request.getProductType());
|
||||
if (existing != null) {
|
||||
throw new IllegalArgumentException("商品类型代码已存在: " + request.getProductType());
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = convertToEntity(request);
|
||||
capability.setCreatedAt(LocalDateTime.now());
|
||||
capability.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
mapper.insert(capability);
|
||||
|
||||
// 刷新缓存
|
||||
capabilityService.refreshCache(capability.getProductType());
|
||||
|
||||
log.info("创建商品类型能力配置成功: {}", capability.getProductType());
|
||||
return capability;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public ProductTypeCapability update(Long id, ProductTypeCapabilityRequest request) {
|
||||
validateRequest(request, false);
|
||||
|
||||
ProductTypeCapability capability = getById(id);
|
||||
|
||||
// 如果修改了商品类型代码,检查新代码是否已被使用
|
||||
if (request.getProductType() != null &&
|
||||
!request.getProductType().equals(capability.getProductType())) {
|
||||
ProductTypeCapability existing = mapper.selectByProductType(request.getProductType());
|
||||
if (existing != null && !existing.getId().equals(id)) {
|
||||
throw new IllegalArgumentException("商品类型代码已被使用: " + request.getProductType());
|
||||
}
|
||||
}
|
||||
|
||||
String oldProductType = capability.getProductType();
|
||||
updateEntityFromRequest(capability, request);
|
||||
capability.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
mapper.updateById(capability);
|
||||
|
||||
// 刷新缓存(如果商品类型代码改变了,需要刷新两个缓存)
|
||||
capabilityService.refreshCache(capability.getProductType());
|
||||
if (!oldProductType.equals(capability.getProductType())) {
|
||||
capabilityService.refreshCache(oldProductType);
|
||||
}
|
||||
|
||||
log.info("更新商品类型能力配置成功: {}", capability.getProductType());
|
||||
return capability;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void delete(Long id) {
|
||||
ProductTypeCapability capability = getById(id);
|
||||
|
||||
mapper.deleteById(id);
|
||||
|
||||
// 刷新缓存
|
||||
capabilityService.refreshCache(capability.getProductType());
|
||||
|
||||
log.info("删除商品类型能力配置成功: {}", capability.getProductType());
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void updateStatus(Long id, Boolean isActive) {
|
||||
if (isActive == null) {
|
||||
throw new IllegalArgumentException("状态不能为空");
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = getById(id);
|
||||
capability.setIsActive(isActive);
|
||||
capability.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
mapper.updateById(capability);
|
||||
|
||||
// 刷新缓存
|
||||
capabilityService.refreshCache(capability.getProductType());
|
||||
|
||||
log.info("更新商品类型能力配置状态成功: {}, isActive={}", capability.getProductType(), isActive);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public int initializeDefaultCapabilities() {
|
||||
int count = 0;
|
||||
|
||||
for (ProductType productType : ProductType.values()) {
|
||||
// 检查是否已存在配置
|
||||
ProductTypeCapability existing = mapper.selectByProductType(productType.getCode());
|
||||
if (existing != null) {
|
||||
log.debug("商品类型 {} 已存在配置,跳过初始化", productType.getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建默认配置
|
||||
ProductTypeCapability capability = createDefaultCapability(productType);
|
||||
mapper.insert(capability);
|
||||
count++;
|
||||
|
||||
log.info("初始化商品类型能力配置: {}", productType.getCode());
|
||||
}
|
||||
|
||||
// 刷新所有缓存
|
||||
capabilityService.refreshCache();
|
||||
|
||||
log.info("商品类型能力配置批量初始化完成,共初始化 {} 个配置", count);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
*/
|
||||
private ProductTypeCapability createDefaultCapability(ProductType productType) {
|
||||
ProductTypeCapability capability = new ProductTypeCapability();
|
||||
capability.setProductType(productType.getCode());
|
||||
capability.setDisplayName(productType.getDescription());
|
||||
capability.setCategory(productType.getCategoryCode());
|
||||
|
||||
// 根据分类设置默认的定价模式
|
||||
if (ProductCategory.PRINT == productType.getCategory()) {
|
||||
capability.setPricingMode(PricingMode.QUANTITY_BASED.getCode());
|
||||
capability.setSupportsTierPricing(true);
|
||||
capability.setAllowDuplicatePurchase(true);
|
||||
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode());
|
||||
} else {
|
||||
capability.setPricingMode(PricingMode.FIXED.getCode());
|
||||
capability.setSupportsTierPricing(false);
|
||||
capability.setAllowDuplicatePurchase(false);
|
||||
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.CHECK_BY_SET_ID.getCode());
|
||||
}
|
||||
|
||||
// 优惠能力默认全部开启
|
||||
capability.setCanUseCoupon(true);
|
||||
capability.setCanUseVoucher(true);
|
||||
capability.setCanUseOnePrice(true);
|
||||
capability.setCanUseBundle(true);
|
||||
|
||||
capability.setIsActive(true);
|
||||
capability.setCreatedAt(LocalDateTime.now());
|
||||
capability.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return capability;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求参数
|
||||
*/
|
||||
private void validateRequest(ProductTypeCapabilityRequest request, boolean isCreate) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("请求参数不能为空");
|
||||
}
|
||||
|
||||
if (isCreate || request.getProductType() != null) {
|
||||
if (request.getProductType() == null || request.getProductType().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("商品类型代码不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.getDisplayName() != null && request.getDisplayName().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("显示名称不能为空字符串");
|
||||
}
|
||||
|
||||
// 验证枚举值有效性
|
||||
if (request.getPricingMode() != null) {
|
||||
try {
|
||||
PricingMode.fromCode(request.getPricingMode());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("无效的定价模式: " + request.getPricingMode());
|
||||
}
|
||||
}
|
||||
|
||||
if (request.getDuplicateCheckStrategy() != null) {
|
||||
try {
|
||||
DuplicateCheckStrategy.fromCode(request.getDuplicateCheckStrategy());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("无效的重复检查策略: " + request.getDuplicateCheckStrategy());
|
||||
}
|
||||
}
|
||||
|
||||
if (request.getCategory() != null) {
|
||||
try {
|
||||
ProductCategory.fromCode(request.getCategory());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("无效的商品分类: " + request.getCategory());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求转换为实体
|
||||
*/
|
||||
private ProductTypeCapability convertToEntity(ProductTypeCapabilityRequest request) {
|
||||
ProductTypeCapability capability = new ProductTypeCapability();
|
||||
updateEntityFromRequest(capability, request);
|
||||
return capability;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求更新实体
|
||||
*/
|
||||
private void updateEntityFromRequest(ProductTypeCapability capability, ProductTypeCapabilityRequest request) {
|
||||
if (request.getProductType() != null) {
|
||||
capability.setProductType(request.getProductType());
|
||||
}
|
||||
if (request.getDisplayName() != null) {
|
||||
capability.setDisplayName(request.getDisplayName());
|
||||
}
|
||||
if (request.getCategory() != null) {
|
||||
capability.setCategory(request.getCategory());
|
||||
}
|
||||
if (request.getPricingMode() != null) {
|
||||
capability.setPricingMode(request.getPricingMode());
|
||||
}
|
||||
if (request.getSupportsTierPricing() != null) {
|
||||
capability.setSupportsTierPricing(request.getSupportsTierPricing());
|
||||
}
|
||||
if (request.getAllowDuplicatePurchase() != null) {
|
||||
capability.setAllowDuplicatePurchase(request.getAllowDuplicatePurchase());
|
||||
}
|
||||
if (request.getDuplicateCheckStrategy() != null) {
|
||||
capability.setDuplicateCheckStrategy(request.getDuplicateCheckStrategy());
|
||||
}
|
||||
if (request.getCanUseCoupon() != null) {
|
||||
capability.setCanUseCoupon(request.getCanUseCoupon());
|
||||
}
|
||||
if (request.getCanUseVoucher() != null) {
|
||||
capability.setCanUseVoucher(request.getCanUseVoucher());
|
||||
}
|
||||
if (request.getCanUseOnePrice() != null) {
|
||||
capability.setCanUseOnePrice(request.getCanUseOnePrice());
|
||||
}
|
||||
if (request.getCanUseBundle() != null) {
|
||||
capability.setCanUseBundle(request.getCanUseBundle());
|
||||
}
|
||||
if (request.getMetadata() != null) {
|
||||
capability.setMetadata(request.getMetadata());
|
||||
}
|
||||
if (request.getIsActive() != null) {
|
||||
capability.setIsActive(request.getIsActive());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.ycwl.basic.product.service.impl;
|
||||
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper;
|
||||
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 商品类型能力服务实现
|
||||
*
|
||||
* 缓存策略:
|
||||
* - 使用 Spring Cache 进行缓存
|
||||
* - 缓存名称:productTypeCapability
|
||||
* - 缓存key:商品类型代码
|
||||
* - 缓存失效:手动调用 refreshCache 方法
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@CacheConfig(cacheNames = "productTypeCapability")
|
||||
public class ProductTypeCapabilityServiceImpl implements IProductTypeCapabilityService {
|
||||
|
||||
@Autowired
|
||||
private ProductTypeCapabilityMapper mapper;
|
||||
|
||||
@Cacheable(key = "#productType")
|
||||
@Override
|
||||
public ProductTypeCapability getCapability(String productType) {
|
||||
if (productType == null || productType.trim().isEmpty()) {
|
||||
log.warn("商品类型代码为空,返回默认配置");
|
||||
return getDefaultCapability(null);
|
||||
}
|
||||
|
||||
try {
|
||||
ProductTypeCapability capability = mapper.selectByProductType(productType);
|
||||
if (capability == null) {
|
||||
log.warn("未找到商品类型能力配置: {}, 使用默认配置", productType);
|
||||
return getDefaultCapability(productType);
|
||||
}
|
||||
|
||||
// 验证配置完整性
|
||||
if (!isValidCapability(capability)) {
|
||||
log.warn("商品类型能力配置不完整: {}, 使用默认配置", productType);
|
||||
return getDefaultCapability(productType);
|
||||
}
|
||||
|
||||
return capability;
|
||||
} catch (Exception e) {
|
||||
log.error("查询商品类型能力配置失败: {}, 降级到默认配置", productType, e);
|
||||
return getDefaultCapability(productType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName(String productType) {
|
||||
ProductTypeCapability capability = getCapability(productType);
|
||||
return capability.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowDuplicatePurchase(String productType) {
|
||||
ProductTypeCapability capability = getCapability(productType);
|
||||
return Boolean.TRUE.equals(capability.getAllowDuplicatePurchase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PricingMode getPricingMode(String productType) {
|
||||
ProductTypeCapability capability = getCapability(productType);
|
||||
return capability.getPricingModeEnum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplicateCheckStrategy getDuplicateCheckStrategy(String productType) {
|
||||
ProductTypeCapability capability = getCapability(productType);
|
||||
return capability.getDuplicateCheckStrategyEnum();
|
||||
}
|
||||
|
||||
@CacheEvict(allEntries = true)
|
||||
@Override
|
||||
public void refreshCache() {
|
||||
log.info("刷新所有商品类型能力缓存");
|
||||
}
|
||||
|
||||
@CacheEvict(key = "#productType")
|
||||
@Override
|
||||
public void refreshCache(String productType) {
|
||||
log.info("刷新商品类型能力缓存: {}", productType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认能力配置(兜底方案)
|
||||
* 设计原则:安全第一,宁可限制也不放开
|
||||
*
|
||||
* @param productType 商品类型代码(可为null)
|
||||
* @return 安全的默认配置
|
||||
*/
|
||||
private ProductTypeCapability getDefaultCapability(String productType) {
|
||||
ProductTypeCapability defaultCap = new ProductTypeCapability();
|
||||
defaultCap.setProductType(productType);
|
||||
defaultCap.setDisplayName("景区商品");
|
||||
defaultCap.setCategory("GENERAL");
|
||||
|
||||
// 定价相关:默认固定价格
|
||||
defaultCap.setPricingMode(PricingMode.FIXED.getCode());
|
||||
defaultCap.setSupportsTierPricing(false);
|
||||
|
||||
// 购买限制:默认不允许重复购买(安全策略)
|
||||
defaultCap.setAllowDuplicatePurchase(false);
|
||||
defaultCap.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode());
|
||||
|
||||
// 优惠能力:默认全部允许
|
||||
defaultCap.setCanUseCoupon(true);
|
||||
defaultCap.setCanUseVoucher(true);
|
||||
defaultCap.setCanUseOnePrice(true);
|
||||
defaultCap.setCanUseBundle(true);
|
||||
|
||||
defaultCap.setIsActive(true);
|
||||
|
||||
return defaultCap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置完整性
|
||||
*/
|
||||
private boolean isValidCapability(ProductTypeCapability capability) {
|
||||
if (capability == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 必填字段检查
|
||||
if (capability.getProductType() == null || capability.getProductType().trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (capability.getDisplayName() == null || capability.getDisplayName().trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (capability.getPricingMode() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证枚举值有效性
|
||||
try {
|
||||
PricingMode.fromCode(capability.getPricingMode());
|
||||
if (capability.getDuplicateCheckStrategy() != null) {
|
||||
DuplicateCheckStrategy.fromCode(capability.getDuplicateCheckStrategy());
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("商品类型能力配置包含无效的枚举值: {}", capability.getProductType(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
47
src/main/resources/mapper/ProductTypeCapabilityMapper.xml
Normal file
47
src/main/resources/mapper/ProductTypeCapabilityMapper.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.product.capability.ProductTypeCapability">
|
||||
<id column="id" property="id" jdbcType="BIGINT"/>
|
||||
<result column="product_type" property="productType" jdbcType="VARCHAR"/>
|
||||
<result column="display_name" property="displayName" jdbcType="VARCHAR"/>
|
||||
<result column="category" property="category" jdbcType="VARCHAR"/>
|
||||
<result column="pricing_mode" property="pricingMode" jdbcType="VARCHAR"/>
|
||||
<result column="supports_tier_pricing" property="supportsTierPricing" jdbcType="BOOLEAN"/>
|
||||
<result column="allow_duplicate_purchase" property="allowDuplicatePurchase" jdbcType="BOOLEAN"/>
|
||||
<result column="duplicate_check_strategy" property="duplicateCheckStrategy" jdbcType="VARCHAR"/>
|
||||
<result column="can_use_coupon" property="canUseCoupon" jdbcType="BOOLEAN"/>
|
||||
<result column="can_use_voucher" property="canUseVoucher" jdbcType="BOOLEAN"/>
|
||||
<result column="can_use_one_price" property="canUseOnePrice" jdbcType="BOOLEAN"/>
|
||||
<result column="can_use_bundle" property="canUseBundle" jdbcType="BOOLEAN"/>
|
||||
<result column="metadata" property="metadata" jdbcType="VARCHAR"
|
||||
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="is_active" property="isActive" jdbcType="BOOLEAN"/>
|
||||
<result column="created_at" property="createdAt" jdbcType="TIMESTAMP"/>
|
||||
<result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, product_type, display_name, category,
|
||||
pricing_mode, supports_tier_pricing,
|
||||
allow_duplicate_purchase, duplicate_check_strategy,
|
||||
can_use_coupon, can_use_voucher, can_use_one_price, can_use_bundle,
|
||||
metadata, is_active, created_at, updated_at
|
||||
</sql>
|
||||
|
||||
<!-- 根据商品类型代码查询 -->
|
||||
<select id="selectByProductType" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List"/>
|
||||
FROM product_type_capability
|
||||
WHERE product_type = #{productType}
|
||||
AND is_active = TRUE
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.ycwl.basic.product.service;
|
||||
|
||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||
import com.ycwl.basic.product.capability.PricingMode;
|
||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 商品类型能力服务测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class ProductTypeCapabilityServiceTest {
|
||||
|
||||
@Autowired(required = false)
|
||||
private IProductTypeCapabilityService productTypeCapabilityService;
|
||||
|
||||
@Test
|
||||
public void testGetCapability_VLOG_VIDEO() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability("VLOG_VIDEO");
|
||||
|
||||
assertNotNull(capability);
|
||||
assertEquals("VLOG_VIDEO", capability.getProductType());
|
||||
assertEquals("Vlog视频", capability.getDisplayName());
|
||||
assertEquals("VIDEO", capability.getCategory());
|
||||
assertEquals(PricingMode.FIXED, capability.getPricingModeEnum());
|
||||
assertEquals(false, capability.getAllowDuplicatePurchase());
|
||||
assertEquals(DuplicateCheckStrategy.CHECK_BY_VIDEO_ID, capability.getDuplicateCheckStrategyEnum());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCapability_PHOTO_PRINT() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability("PHOTO_PRINT");
|
||||
|
||||
assertNotNull(capability);
|
||||
assertEquals("PHOTO_PRINT", capability.getProductType());
|
||||
assertEquals("照片打印", capability.getDisplayName());
|
||||
assertEquals("PRINT", capability.getCategory());
|
||||
assertEquals(PricingMode.QUANTITY_BASED, capability.getPricingModeEnum());
|
||||
assertEquals(true, capability.getAllowDuplicatePurchase());
|
||||
assertEquals(DuplicateCheckStrategy.NO_CHECK, capability.getDuplicateCheckStrategyEnum());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDisplayName() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
String displayName = productTypeCapabilityService.getDisplayName("RECORDING_SET");
|
||||
assertEquals("录像集", displayName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowDuplicatePurchase() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 视频类不允许重复购买
|
||||
assertFalse(productTypeCapabilityService.allowDuplicatePurchase("VLOG_VIDEO"));
|
||||
|
||||
// 打印类允许重复购买
|
||||
assertTrue(productTypeCapabilityService.allowDuplicatePurchase("PHOTO_PRINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPricingMode() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 视频类固定价格
|
||||
assertEquals(PricingMode.FIXED, productTypeCapabilityService.getPricingMode("VLOG_VIDEO"));
|
||||
|
||||
// 打印类基于数量
|
||||
assertEquals(PricingMode.QUANTITY_BASED, productTypeCapabilityService.getPricingMode("PHOTO_PRINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCapability_NotFound_ShouldReturnDefault() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductTypeCapability capability = productTypeCapabilityService.getCapability("NOT_EXIST_TYPE");
|
||||
|
||||
// 应该返回默认配置而不是 null
|
||||
assertNotNull(capability);
|
||||
assertEquals("NOT_EXIST_TYPE", capability.getProductType());
|
||||
assertEquals("景区商品", capability.getDisplayName());
|
||||
assertEquals(PricingMode.FIXED, capability.getPricingModeEnum());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCapability_NullOrEmpty_ShouldReturnDefault() {
|
||||
if (productTypeCapabilityService == null) {
|
||||
System.out.println("服务未注入,跳过测试(可能数据库未迁移)");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductTypeCapability capability1 = productTypeCapabilityService.getCapability(null);
|
||||
assertNotNull(capability1);
|
||||
assertEquals("景区商品", capability1.getDisplayName());
|
||||
|
||||
ProductTypeCapability capability2 = productTypeCapabilityService.getCapability("");
|
||||
assertNotNull(capability2);
|
||||
assertEquals("景区商品", capability2.getDisplayName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user