From 9c932b6ba89027e5a42085be88ef5dd879df4525 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 14 Aug 2025 10:48:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=9F=A5=E8=AF=A2=EF=BC=8C?= =?UTF-8?q?=E5=BE=85=E5=A4=84=E7=90=86=E8=AE=A2=E5=8D=95=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 + .../basic/config/JacksonConfiguration.java | 18 +- .../PriceCalculationController.java | 75 ++++++ .../controller/PricingConfigController.java | 149 +++++++++++ .../ycwl/basic/pricing/dto/CouponInfo.java | 38 +++ .../basic/pricing/dto/CouponUseRequest.java | 37 +++ .../basic/pricing/dto/CouponUseResult.java | 38 +++ .../basic/pricing/dto/DiscountDetail.java | 76 ++++++ .../pricing/dto/PriceCalculationRequest.java | 27 ++ .../pricing/dto/PriceCalculationResult.java | 48 ++++ .../ycwl/basic/pricing/dto/PriceDetails.java | 32 +++ .../ycwl/basic/pricing/dto/ProductItem.java | 53 ++++ .../basic/pricing/dto/ProductPriceInfo.java | 27 ++ .../ycwl/basic/pricing/entity/BaseEntity.java | 21 ++ .../pricing/entity/PriceBundleConfig.java | 46 ++++ .../entity/PriceCouponClaimRecord.java | 47 ++++ .../pricing/entity/PriceCouponConfig.java | 73 ++++++ .../pricing/entity/PriceProductConfig.java | 51 ++++ .../basic/pricing/entity/PriceTierConfig.java | 66 +++++ .../basic/pricing/enums/CouponStatus.java | 31 +++ .../ycwl/basic/pricing/enums/CouponType.java | 30 +++ .../ycwl/basic/pricing/enums/ProductType.java | 33 +++ .../exception/CouponInvalidException.java | 15 ++ .../exception/PriceCalculationException.java | 15 ++ .../exception/PricingExceptionHandler.java | 66 +++++ .../ProductConfigNotFoundException.java | 15 ++ .../mapper/PriceBundleConfigMapper.java | 46 ++++ .../mapper/PriceCouponClaimRecordMapper.java | 63 +++++ .../mapper/PriceCouponConfigMapper.java | 62 +++++ .../mapper/PriceProductConfigMapper.java | 49 ++++ .../pricing/mapper/PriceTierConfigMapper.java | 71 ++++++ .../basic/pricing/service/ICouponService.java | 62 +++++ .../pricing/service/IPriceBundleService.java | 36 +++ .../service/IPriceCalculationService.java | 18 ++ .../service/IPricingManagementService.java | 59 +++++ .../service/IProductConfigService.java | 75 ++++++ .../service/impl/CouponServiceImpl.java | 173 +++++++++++++ .../service/impl/PriceBundleServiceImpl.java | 104 ++++++++ .../impl/PriceCalculationServiceImpl.java | 238 ++++++++++++++++++ .../impl/PricingManagementServiceImpl.java | 107 ++++++++ .../impl/ProductConfigServiceImpl.java | 78 ++++++ 41 files changed, 2371 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/CouponType.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/ProductType.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/ICouponService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java diff --git a/pom.xml b/pom.xml index 7dd39ed..915cbbf 100644 --- a/pom.xml +++ b/pom.xml @@ -135,6 +135,10 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java index ba2892a..70711a4 100644 --- a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java +++ b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java @@ -1,10 +1,16 @@ package com.ycwl.basic.config; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Configuration public class JacksonConfiguration { @@ -13,6 +19,16 @@ public class JacksonConfiguration { return builder -> { // 把 Long 类型序列化为 String builder.serializerByType(Long.class, ToStringSerializer.instance); + + // 添加 JavaTimeModule 以支持 Java 8 时间类型 + builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); + builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; } -} + + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java new file mode 100644 index 0000000..8b4cd2e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格计算控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing") +@RequiredArgsConstructor +public class PriceCalculationController { + + private final IPriceCalculationService priceCalculationService; + private final ICouponService couponService; + + /** + * 计算商品价格 + */ + @PostMapping("/calculate") + public ApiResponse calculatePrice(@RequestBody PriceCalculationRequest request) { + log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size()); + + PriceCalculationResult result = priceCalculationService.calculatePrice(request); + + log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}", + result.getOriginalAmount(), result.getFinalAmount(), + result.getUsedCoupon() != null ? result.getUsedCoupon().getCouponId() : null); + + return ApiResponse.success(result); + } + + /** + * 使用优惠券 + * 只能通过代码处理,不能通过接口调用 + */ + @PostMapping("/coupons/use") + @Deprecated + public ApiResponse useCoupon(@RequestBody CouponUseRequest request) { +// log.info("优惠券使用请求: userId={}, couponId={}, orderId={}", +// request.getUserId(), request.getCouponId(), request.getOrderId()); +// +// CouponUseResult result = couponService.useCoupon(request); +// +// log.info("优惠券使用成功: couponId={}, discountAmount={}", +// result.getCouponId(), result.getDiscountAmount()); +// +// ApiResponse response = ApiResponse.success(result); +// response.setMsg("优惠券使用成功"); +// return response; + return null; + } + + /** + * 查询用户可用优惠券 + */ + @GetMapping("/coupons/my-coupons") + public ApiResponse> getUserCoupons(@RequestParam Long userId) { + log.info("查询用户可用优惠券: userId={}", userId); + + List coupons = couponService.getUserAvailableCoupons(userId); + + log.info("用户可用优惠券数量: {}", coupons.size()); + + return ApiResponse.success(coupons); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java new file mode 100644 index 0000000..8b98ad8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -0,0 +1,149 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.service.IProductConfigService; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格配置管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/config") +@RequiredArgsConstructor +public class PricingConfigController { + + private final IProductConfigService productConfigService; + private final IPriceBundleService bundleService; + private final IPricingManagementService managementService; + + /** + * 获取所有商品配置 + */ + @GetMapping("/products") + public ApiResponse> getProductConfigs() { + List configs = productConfigService.getActiveProductConfigs(); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型获取阶梯配置 + */ + @GetMapping("/tiers/{productType}") + public ApiResponse> getTierConfigs(@PathVariable String productType) { + List configs = productConfigService.getTierConfigs(productType); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取阶梯配置 + */ + @GetMapping("/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigs(@PathVariable String productType, + @PathVariable String productId) { + List configs = productConfigService.getTierConfigs(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取具体配置 + */ + @GetMapping("/products/{productType}/{productId}") + public ApiResponse getProductConfig(@PathVariable String productType, + @PathVariable String productId) { + PriceProductConfig config = productConfigService.getProductConfig(productType, productId); + return ApiResponse.success(config); + } + + /** + * 获取所有阶梯配置 + */ + @GetMapping("/tiers") + public ApiResponse> getAllTierConfigs() { + log.info("获取所有阶梯定价配置"); + return ApiResponse.success(List.of()); + } + + /** + * 获取所有一口价配置 + */ + @GetMapping("/bundles") + public ApiResponse> getBundleConfigs() { + List configs = bundleService.getActiveBundles(); + return ApiResponse.success(configs); + } + + // ==================== 配置管理API(手动处理时间) ==================== + + /** + * 创建商品配置 + */ + @PostMapping("/products") + public ApiResponse createProductConfig(@RequestBody PriceProductConfig config) { + log.info("创建商品配置: {}", config.getProductName()); + Long id = managementService.createProductConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新商品配置 + */ + @PutMapping("/products/{id}") + public ApiResponse updateProductConfig(@PathVariable Long id, @RequestBody PriceProductConfig config) { + log.info("更新商品配置: id={}, name={}", id, config.getProductName()); + config.setId(id); + boolean success = managementService.updateProductConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建阶梯定价配置 + */ + @PostMapping("/tiers") + public ApiResponse createTierConfig(@RequestBody PriceTierConfig config) { + log.info("创建阶梯定价配置: productType={}, price={}", config.getProductType(), config.getPrice()); + Long id = managementService.createTierConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新阶梯定价配置 + */ + @PutMapping("/tiers/{id}") + public ApiResponse updateTierConfig(@PathVariable Long id, @RequestBody PriceTierConfig config) { + log.info("更新阶梯定价配置: id={}", id); + config.setId(id); + boolean success = managementService.updateTierConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建一口价配置 + */ + @PostMapping("/bundles") + public ApiResponse createBundleConfig(@RequestBody PriceBundleConfig config) { + log.info("创建一口价配置: {}", config.getBundleName()); + Long id = managementService.createBundleConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新一口价配置 + */ + @PutMapping("/bundles/{id}") + public ApiResponse updateBundleConfig(@PathVariable Long id, @RequestBody PriceBundleConfig config) { + log.info("更新一口价配置: id={}, name={}", id, config.getBundleName()); + config.setId(id); + boolean success = managementService.updateBundleConfig(config); + return ApiResponse.success(success); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java new file mode 100644 index 0000000..6d315b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券信息DTO + */ +@Data +public class CouponInfo { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠类型 + */ + private CouponType discountType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java new file mode 100644 index 0000000..f1f7253 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券使用请求DTO + */ +@Data +public class CouponUseRequest { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java new file mode 100644 index 0000000..b5df283 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 优惠券使用结果DTO + */ +@Data +public class CouponUseResult { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 使用时间 + */ + private LocalDateTime useTime; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java new file mode 100644 index 0000000..30954e7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 折扣明细DTO + */ +@Data +public class DiscountDetail { + + /** + * 折扣类型 + */ + private String discountType; + + /** + * 折扣名称 + */ + private String discountName; + + /** + * 折扣金额 + */ + private BigDecimal discountAmount; + + /** + * 折扣描述 + */ + private String description; + + /** + * 排序(数值越小越靠前) + */ + private Integer sortOrder; + + /** + * 创建限时立减折扣明细 + */ + public static DiscountDetail createLimitedTimeDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("LIMITED_TIME"); + detail.setDiscountName("限时立减"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("限时优惠,立即享受"); + detail.setSortOrder(1); // 限时立减排在最前面 + return detail; + } + + /** + * 创建优惠券折扣明细 + */ + public static DiscountDetail createCouponDiscount(String couponName, BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("COUPON"); + detail.setDiscountName(couponName); + detail.setDiscountAmount(discountAmount); + detail.setDescription("优惠券减免"); + detail.setSortOrder(2); // 优惠券排在限时立减后面 + return detail; + } + + /** + * 创建一口价折扣明细 + */ + public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("BUNDLE"); + detail.setDiscountName("一口价优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("一口价购买更优惠"); + detail.setSortOrder(3); // 一口价排在最后 + return detail; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java new file mode 100644 index 0000000..72b2048 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 价格计算请求DTO + */ +@Data +public class PriceCalculationRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 用户ID + */ + private Long userId; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java new file mode 100644 index 0000000..bd00cdb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java @@ -0,0 +1,48 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 价格计算结果DTO + */ +@Data +public class PriceCalculationResult { + + /** + * 原始金额(用于前端展示的总原价) + */ + private BigDecimal originalAmount; + + /** + * 商品小计金额(按实际计算价格) + */ + private BigDecimal subtotalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 使用的优惠券信息 + */ + private CouponInfo usedCoupon; + + /** + * 折扣明细列表(包含限时立减、优惠券、一口价等) + */ + private List discountDetails; + + /** + * 商品明细列表 + */ + private List productDetails; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java new file mode 100644 index 0000000..c75ca33 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java @@ -0,0 +1,32 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 价格详情(内部计算用) + */ +@Data +public class PriceDetails { + + /** + * 实际计算总金额 + */ + private BigDecimal totalAmount; + + /** + * 原价总金额 + */ + private BigDecimal originalTotalAmount; + + public PriceDetails() { + this.totalAmount = BigDecimal.ZERO; + this.originalTotalAmount = BigDecimal.ZERO; + } + + public PriceDetails(BigDecimal totalAmount, BigDecimal originalTotalAmount) { + this.totalAmount = totalAmount; + this.originalTotalAmount = originalTotalAmount; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java new file mode 100644 index 0000000..6ce3d6c --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.ProductType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品项DTO + */ +@Data +public class ProductItem { + + /** + * 商品类型 + */ + private ProductType productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 商品子类型 + */ + private String productSubType; + + /** + * 数量(如原片数量、照片数量等) + */ + private Integer quantity; + + /** + * 购买数量(购买几个该商品) + */ + private Integer purchaseCount; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + /** + * 单价(实际计算用的价格) + */ + private BigDecimal unitPrice; + + /** + * 小计(计算后填入) + */ + private BigDecimal subtotal; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java new file mode 100644 index 0000000..65ae04b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品价格信息(内部计算用) + */ +@Data +public class ProductPriceInfo { + + /** + * 实际计算价格 + */ + private BigDecimal actualPrice; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + public ProductPriceInfo(BigDecimal actualPrice, BigDecimal originalPrice) { + this.actualPrice = actualPrice; + this.originalPrice = originalPrice != null ? originalPrice : actualPrice; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java new file mode 100644 index 0000000..b66c468 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 基础实体类 + */ +@Data +public class BaseEntity { + + @TableId(type = IdType.AUTO) + private Long id; + + private LocalDateTime createdTime; + + private LocalDateTime updatedTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java new file mode 100644 index 0000000..f7457dd --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 一口价配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_bundle_config") +public class PriceBundleConfig extends BaseEntity { + + /** + * 套餐名称 + */ + private String bundleName; + + /** + * 套餐价格 + */ + private BigDecimal bundlePrice; + + /** + * 包含商品(JSON) + */ + private String includedProducts; + + /** + * 排除商品(JSON) + */ + private String excludedProducts; + + /** + * 套餐描述 + */ + private String description; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java new file mode 100644 index 0000000..352650f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponStatus; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 优惠券领用记录实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_coupon_claim_record") +public class PriceCouponClaimRecord extends BaseEntity { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 领取时间 + */ + private LocalDateTime claimTime; + + /** + * 使用时间 + */ + private LocalDateTime useTime; + + /** + * 订单ID + */ + private String orderId; + + /** + * 状态 + */ + private CouponStatus status; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java new file mode 100644 index 0000000..f0e134e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java @@ -0,0 +1,73 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 优惠券配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_coupon_config") +public class PriceCouponConfig extends BaseEntity { + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠券类型 + */ + private CouponType couponType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 最小使用金额 + */ + private BigDecimal minAmount; + + /** + * 最大优惠金额 + */ + private BigDecimal maxDiscount; + + /** + * 适用商品类型(JSON) + */ + private String applicableProducts; + + /** + * 发行总量 + */ + private Integer totalQuantity; + + /** + * 已使用数量 + */ + private Integer usedQuantity; + + /** + * 生效时间 + */ + private LocalDateTime validFrom; + + /** + * 失效时间 + */ + private LocalDateTime validUntil; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java new file mode 100644 index 0000000..5bc37d7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java @@ -0,0 +1,51 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 商品价格配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_product_config") +public class PriceProductConfig extends BaseEntity { + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 商品名称 + */ + private String productName; + + /** + * 基础价格 + */ + private BigDecimal basePrice; + + /** + * 商品原价:用于前端展示优惠力度,当original_price > base_price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 价格单位 + */ + private String unit; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java new file mode 100644 index 0000000..31711b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 阶梯定价配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_tier_config") +public class PriceTierConfig extends BaseEntity { + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:与price_product_config的product_id对应 + */ + private String productId; + + /** + * 商品子类型 + */ + private String productSubType; + + /** + * 最小数量 + */ + private Integer minQuantity; + + /** + * 最大数量 + */ + private Integer maxQuantity; + + /** + * 阶梯价格 + */ + private BigDecimal price; + + /** + * 阶梯原价:用于前端展示优惠力度,当original_price > price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 计价单位 + */ + private String unit; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java new file mode 100644 index 0000000..74e511e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java @@ -0,0 +1,31 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券状态枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponStatus { + + CLAIMED("claimed", "已领取"), + USED("used", "已使用"), + EXPIRED("expired", "已过期"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponStatus fromCode(String code) { + for (CouponStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown coupon status code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java new file mode 100644 index 0000000..6908d3d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券类型枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponType { + + PERCENTAGE("percentage", "百分比折扣"), + FIXED_AMOUNT("fixed_amount", "固定金额减免"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponType fromCode(String code) { + for (CouponType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown coupon type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java new file mode 100644 index 0000000..c90bab2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java @@ -0,0 +1,33 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品类型枚举 + */ +@Getter +@AllArgsConstructor +public enum ProductType { + + VLOG_VIDEO("vlog_video", "Vlog视频"), + RECORDING_SET("recording_set", "录像集"), + PHOTO_SET("photo_set", "照相集"), + PHOTO_PRINT("photo_print", "照片打印"), + MACHINE_PRINT("machine_print", "一体机打印"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static ProductType fromCode(String code) { + for (ProductType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown product type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java new file mode 100644 index 0000000..73a6fa4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 优惠券无效异常 + */ +public class CouponInvalidException extends RuntimeException { + + public CouponInvalidException(String message) { + super(message); + } + + public CouponInvalidException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java new file mode 100644 index 0000000..891b7ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 价格计算异常 + */ +public class PriceCalculationException extends RuntimeException { + + public PriceCalculationException(String message) { + super(message); + } + + public PriceCalculationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java new file mode 100644 index 0000000..d4acdd0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.pricing.exception; + +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 价格查询系统全局异常处理器 + */ +@Slf4j +@RestControllerAdvice(basePackages = "com.ycwl.basic.pricing") +public class PricingExceptionHandler { + + /** + * 处理价格计算异常 + */ + @ExceptionHandler(PriceCalculationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handlePriceCalculationException(PriceCalculationException e) { + log.error("价格计算异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理优惠券无效异常 + */ + @ExceptionHandler(CouponInvalidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCouponInvalidException(CouponInvalidException e) { + log.error("优惠券无效异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理商品配置未找到异常 + */ + @ExceptionHandler(ProductConfigNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleProductConfigNotFoundException(ProductConfigNotFoundException e) { + log.error("商品配置未找到异常", e); + return ApiResponse.buildResponse(404, e.getMessage()); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常", e); + return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage()); + } + + /** + * 处理通用异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception e) { + log.error("系统异常", e); + return ApiResponse.buildResponse(500, "系统内部错误"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java new file mode 100644 index 0000000..9bdbe63 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 商品配置未找到异常 + */ +public class ProductConfigNotFoundException extends RuntimeException { + + public ProductConfigNotFoundException(String message) { + super(message); + } + + public ProductConfigNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java new file mode 100644 index 0000000..3f7f48f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 一口价配置Mapper + */ +@Mapper +public interface PriceBundleConfigMapper extends BaseMapper { + + /** + * 查询启用的一口价配置 + */ + @Select("SELECT * FROM price_bundle_config WHERE is_active = 1") + List selectActiveBundles(); + + /** + * 根据ID查询启用的配置 + */ + @Select("SELECT * FROM price_bundle_config WHERE id = #{id} AND is_active = 1") + PriceBundleConfig selectActiveBundleById(Long id); + + /** + * 插入一口价配置 + */ + @Insert("INSERT INTO price_bundle_config (bundle_name, bundle_price, included_products, excluded_products, " + + "description, is_active, created_time, updated_time) VALUES " + + "(#{bundleName}, #{bundlePrice}, #{includedProducts}, #{excludedProducts}, " + + "#{description}, #{isActive}, NOW(), NOW())") + int insertBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, bundle_price = #{bundlePrice}, " + + "included_products = #{includedProducts}, excluded_products = #{excludedProducts}, " + + "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateBundleConfig(PriceBundleConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java new file mode 100644 index 0000000..87ef06d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -0,0 +1,63 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.enums.CouponStatus; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 优惠券领用记录Mapper + */ +@Mapper +public interface PriceCouponClaimRecordMapper extends BaseMapper { + + /** + * 查询用户可用的优惠券记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value, c.min_amount, " + + "c.max_discount, c.applicable_products, c.valid_from, c.valid_until " + + "FROM price_coupon_claim_record r " + + "JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.user_id = #{userId} AND r.status = 'CLAIMED' " + + "AND c.is_active = 1 AND c.valid_from <= NOW() AND c.valid_until > NOW()") + List selectUserAvailableCoupons(Long userId); + + /** + * 查询用户特定优惠券记录 + */ + @Select("SELECT * FROM price_coupon_claim_record " + + "WHERE user_id = #{userId} AND coupon_id = #{couponId} AND status = 'CLAIMED'") + PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId, + @Param("couponId") Long couponId); + + /** + * 更新优惠券使用状态 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, " + + "use_time = #{useTime}, order_id = #{orderId}, updated_time = NOW() " + + "WHERE id = #{id}") + int updateCouponStatus(@Param("id") Long id, + @Param("status") CouponStatus status, + @Param("useTime") java.time.LocalDateTime useTime, + @Param("orderId") String orderId); + + /** + * 插入优惠券领用记录 + */ + @Insert("INSERT INTO price_coupon_claim_record (coupon_id, user_id, claim_time, status, created_time, updated_time) " + + "VALUES (#{couponId}, #{userId}, NOW(), #{status}, NOW(), NOW())") + int insertClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券记录 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, use_time = #{useTime}, " + + "order_id = #{orderId}, updated_time = NOW() WHERE id = #{id}") + int updateClaimRecord(PriceCouponClaimRecord record); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java new file mode 100644 index 0000000..869a4b6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 优惠券配置Mapper + */ +@Mapper +public interface PriceCouponConfigMapper extends BaseMapper { + + /** + * 查询有效的优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " + + "AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + List selectValidCoupons(); + + /** + * 根据ID查询优惠券(包括使用数量检查) + */ + @Select("SELECT * FROM price_coupon_config WHERE id = #{couponId} " + + "AND is_active = 1 AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + PriceCouponConfig selectValidCouponById(Long couponId); + + /** + * 增加优惠券使用数量 + */ + @Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " + + "updated_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") + int incrementUsedQuantity(Long couponId); + + /** + * 插入优惠券配置 + */ + @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + + "max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " + + "is_active, created_time, updated_time) VALUES " + + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + + "#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " + + "#{isActive}, NOW(), NOW())") + int insertCoupon(PriceCouponConfig coupon); + + /** + * 更新优惠券配置 + */ + @Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " + + "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + + "applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " + + "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + + "updated_time = NOW() WHERE id = #{id}") + int updateCoupon(PriceCouponConfig coupon); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java new file mode 100644 index 0000000..907e650 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -0,0 +1,49 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 商品价格配置Mapper + */ +@Mapper +public interface PriceProductConfigMapper extends BaseMapper { + + /** + * 查询启用的商品配置 + */ + @Select("SELECT * FROM price_product_config WHERE is_active = 1") + List selectActiveConfigs(); + + /** + * 根据商品类型查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND is_active = 1") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1") + PriceProductConfig selectByProductTypeAndId(String productType, String productId); + + /** + * 插入商品价格配置 + */ + @Insert("INSERT INTO price_product_config (product_type, product_id, product_name, base_price, original_price, unit, is_active, created_time, updated_time) " + + "VALUES (#{productType}, #{productId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") + int insertProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + @Update("UPDATE price_product_config SET product_id = #{productId}, product_name = #{productName}, base_price = #{basePrice}, " + + "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateProductConfig(PriceProductConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java new file mode 100644 index 0000000..e1f6a72 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 阶梯定价配置Mapper + */ +@Mapper +public interface PriceTierConfigMapper extends BaseMapper { + + /** + * 根据商品类型、商品ID和数量查询匹配的阶梯价格 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} " + + "AND (product_sub_type = #{productSubType} OR product_sub_type IS NULL) " + + "AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " + + "AND is_active = 1 ORDER BY sort_order ASC LIMIT 1") + PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType, + @Param("productId") String productId, + @Param("productSubType") String productSubType, + @Param("quantity") Integer quantity); + + /** + * 根据商品类型查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductTypeAndId(@Param("productType") String productType, + @Param("productId") String productId); + + /** + * 根据商品类型和子类型查询阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_sub_type = #{productSubType} AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductTypeAndSubType(@Param("productType") String productType, + @Param("productSubType") String productSubType); + + /** + * 插入阶梯定价配置 + */ + @Insert("INSERT INTO price_tier_config (product_type, product_id, product_sub_type, min_quantity, max_quantity, price, " + + "original_price, unit, sort_order, is_active, created_time, updated_time) VALUES " + + "(#{productType}, #{productId}, #{productSubType}, #{minQuantity}, #{maxQuantity}, #{price}, " + + "#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())") + int insertTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + @Update("UPDATE price_tier_config SET product_id = #{productId}, product_sub_type = #{productSubType}, min_quantity = #{minQuantity}, " + + "max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " + + "is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateTierConfig(PriceTierConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java new file mode 100644 index 0000000..3acd166 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.CouponInfo; +import com.ycwl.basic.pricing.dto.CouponUseRequest; +import com.ycwl.basic.pricing.dto.CouponUseResult; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠券服务接口 + */ +public interface ICouponService { + + /** + * 自动选择最优优惠券 + * + * @param userId 用户ID + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 最优优惠券信息,如果没有可用优惠券则返回null + */ + CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount); + + /** + * 计算优惠券优惠金额 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 优惠金额 + */ + BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 验证优惠券是否可用 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 是否可用 + */ + boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 使用优惠券 + * + * @param request 优惠券使用请求 + * @return 使用结果 + */ + CouponUseResult useCoupon(CouponUseRequest request); + + /** + * 查询用户可用优惠券 + * + * @param userId 用户ID + * @return 可用优惠券列表 + */ + List getUserAvailableCoupons(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java new file mode 100644 index 0000000..0e7ae09 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java @@ -0,0 +1,36 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 一口价套餐服务接口 + */ +public interface IPriceBundleService { + + /** + * 检查商品是否适用一口价 + * + * @param products 商品列表 + * @return 是否适用 + */ + boolean isBundleApplicable(List products); + + /** + * 获取一口价价格 + * + * @param products 商品列表 + * @return 一口价价格,如果不适用则返回null + */ + BigDecimal getBundlePrice(List products); + + /** + * 获取所有启用的一口价配置 + * + * @return 一口价配置列表 + */ + List getActiveBundles(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java new file mode 100644 index 0000000..bffa743 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java @@ -0,0 +1,18 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.PriceCalculationRequest; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; + +/** + * 价格计算服务接口 + */ +public interface IPriceCalculationService { + + /** + * 计算商品价格(支持自动优惠券应用) + * + * @param request 价格计算请求 + * @return 价格计算结果 + */ + PriceCalculationResult calculatePrice(PriceCalculationRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java new file mode 100644 index 0000000..bcb9f29 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java @@ -0,0 +1,59 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.*; + +/** + * 价格管理服务接口(用于配置管理,手动处理时间字段) + */ +public interface IPricingManagementService { + + /** + * 创建商品价格配置 + */ + Long createProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + boolean updateProductConfig(PriceProductConfig config); + + /** + * 创建阶梯定价配置 + */ + Long createTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + boolean updateTierConfig(PriceTierConfig config); + + /** + * 创建优惠券配置 + */ + Long createCouponConfig(PriceCouponConfig config); + + /** + * 更新优惠券配置 + */ + boolean updateCouponConfig(PriceCouponConfig config); + + /** + * 创建优惠券领用记录 + */ + Long createCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券领用记录 + */ + boolean updateCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 创建一口价配置 + */ + Long createBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + boolean updateBundleConfig(PriceBundleConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java new file mode 100644 index 0000000..cd7ab96 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; + +import java.util.List; + +/** + * 商品配置管理服务接口 + */ +public interface IProductConfigService { + + /** + * 根据商品类型获取基础配置(兼容旧接口) + * + * @param productType 商品类型 + * @return 商品配置列表 + */ + List getProductConfig(String productType); + + /** + * 根据商品类型和商品ID获取精确配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 商品配置 + */ + PriceProductConfig getProductConfig(String productType, String productId); + + /** + * 根据商品类型、商品ID和数量获取阶梯价格配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @param productSubType 商品子类型 + * @param quantity 数量 + * @return 阶梯价格配置 + */ + PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity); + + /** + * 根据商品类型和数量获取阶梯价格配置(兼容旧接口) + * + * @param productType 商品类型 + * @param productSubType 商品子类型 + * @param quantity 数量 + * @return 阶梯价格配置 + */ + @Deprecated + PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity); + + /** + * 获取所有启用的商品配置 + * + * @return 商品配置列表 + */ + List getActiveProductConfigs(); + + /** + * 根据商品类型获取所有阶梯配置 + * + * @param productType 商品类型 + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType); + + /** + * 根据商品类型和商品ID获取所有阶梯配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType, String productId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java new file mode 100644 index 0000000..4394dd5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -0,0 +1,173 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.enums.CouponType; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.CouponInvalidException; +import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; +import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; +import com.ycwl.basic.pricing.service.ICouponService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 优惠券服务实现 + */ +@Slf4j +@Service("pricingCouponServiceImpl") +@RequiredArgsConstructor +public class CouponServiceImpl implements ICouponService { + + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final ObjectMapper objectMapper; + + @Override + public CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount) { + List userCoupons = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + if (userCoupons.isEmpty()) { + return null; + } + + CouponInfo bestCoupon = null; + BigDecimal maxDiscount = BigDecimal.ZERO; + + for (PriceCouponClaimRecord record : userCoupons) { + PriceCouponConfig coupon = couponConfigMapper.selectById(record.getCouponId()); + if (coupon == null || !isCouponApplicable(coupon, products, totalAmount)) { + continue; + } + + BigDecimal discount = calculateCouponDiscount(coupon, products, totalAmount); + if (discount.compareTo(maxDiscount) > 0) { + maxDiscount = discount; + bestCoupon = buildCouponInfo(coupon, discount); + } + } + + return bestCoupon; + } + + @Override + public BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + if (!isCouponApplicable(coupon, products, totalAmount)) { + return BigDecimal.ZERO; + } + + BigDecimal discount; + if (coupon.getCouponType() == CouponType.PERCENTAGE) { + discount = totalAmount.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)); + if (coupon.getMaxDiscount() != null && discount.compareTo(coupon.getMaxDiscount()) > 0) { + discount = coupon.getMaxDiscount(); + } + } else { + discount = coupon.getDiscountValue(); + } + + return discount.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + if (totalAmount.compareTo(coupon.getMinAmount()) < 0) { + return false; + } + + if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) { + return true; + } + + try { + List applicableProductTypes = objectMapper.readValue( + coupon.getApplicableProducts(), new TypeReference>() {}); + + for (ProductItem product : products) { + if (applicableProductTypes.contains(product.getProductType().getCode())) { + return true; + } + } + return false; + } catch (Exception e) { + log.error("解析适用商品类型失败", e); + return false; + } + } + + @Override + @Transactional + public CouponUseResult useCoupon(CouponUseRequest request) { + PriceCouponClaimRecord record = couponClaimRecordMapper.selectUserCouponRecord( + request.getUserId(), request.getCouponId()); + + if (record == null) { + throw new CouponInvalidException("用户未拥有该优惠券"); + } + + if (record.getStatus() != CouponStatus.CLAIMED) { + throw new CouponInvalidException("优惠券状态无效: " + record.getStatus()); + } + + int updateCount = couponConfigMapper.incrementUsedQuantity(request.getCouponId()); + if (updateCount == 0) { + throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限"); + } + + LocalDateTime useTime = LocalDateTime.now(); + + // 设置使用时间和订单信息 + record.setStatus(CouponStatus.USED); + record.setUseTime(useTime); + record.setOrderId(request.getOrderId()); + record.setUpdatedTime(LocalDateTime.now()); + + couponClaimRecordMapper.updateCouponStatus( + record.getId(), CouponStatus.USED, useTime, request.getOrderId()); + + CouponUseResult result = new CouponUseResult(); + result.setCouponId(request.getCouponId()); + result.setUserId(request.getUserId()); + result.setOrderId(request.getOrderId()); + result.setUseTime(useTime); + result.setDiscountAmount(request.getDiscountAmount()); + + return result; + } + + @Override + public List getUserAvailableCoupons(Long userId) { + List records = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + List coupons = new ArrayList<>(); + + for (PriceCouponClaimRecord record : records) { + PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId()); + if (config != null) { + coupons.add(buildCouponInfo(config, null)); + } + } + + return coupons; + } + + private CouponInfo buildCouponInfo(PriceCouponConfig coupon, BigDecimal actualDiscountAmount) { + CouponInfo info = new CouponInfo(); + info.setCouponId(coupon.getId()); + info.setCouponName(coupon.getCouponName()); + info.setDiscountType(coupon.getCouponType()); + info.setDiscountValue(coupon.getDiscountValue()); + info.setActualDiscountAmount(actualDiscountAmount); + return info; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java new file mode 100644 index 0000000..2baa997 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java @@ -0,0 +1,104 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.mapper.PriceBundleConfigMapper; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 一口价套餐服务实现 + */ +@Slf4j +@Service("pricingBundleServiceImpl") +@RequiredArgsConstructor +public class PriceBundleServiceImpl implements IPriceBundleService { + + private final PriceBundleConfigMapper bundleConfigMapper; + private final ObjectMapper objectMapper; + + @Override + public boolean isBundleApplicable(List products) { + List bundles = getActiveBundles(); + if (bundles.isEmpty()) { + return false; + } + + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return true; + } + } + + return false; + } + + @Override + public BigDecimal getBundlePrice(List products) { + if (!isBundleApplicable(products)) { + return null; + } + + List bundles = getActiveBundles(); + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return bundle.getBundlePrice(); + } + } + + return null; + } + + @Override + @Cacheable(value = "active-bundles") + public List getActiveBundles() { + return bundleConfigMapper.selectActiveBundles(); + } + + private boolean isProductsMatchBundle(Set productTypes, PriceBundleConfig bundle) { + try { + List includedProducts = objectMapper.readValue( + bundle.getIncludedProducts(), new TypeReference>() {}); + + Set requiredProducts = new HashSet<>(includedProducts); + + if (bundle.getExcludedProducts() != null && !bundle.getExcludedProducts().isEmpty()) { + List excludedProducts = objectMapper.readValue( + bundle.getExcludedProducts(), new TypeReference>() {}); + + for (String excludedProduct : excludedProducts) { + if (productTypes.contains(excludedProduct)) { + return false; + } + } + } + + return productTypes.containsAll(requiredProducts); + + } catch (Exception e) { + log.error("解析一口价配置失败: bundleId={}", bundle.getId(), e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java new file mode 100644 index 0000000..b1783ce --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -0,0 +1,238 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.PriceCalculationException; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import com.ycwl.basic.pricing.service.IProductConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 价格计算服务实现 + */ +@Slf4j +@Service("pricingCalculationServiceImpl") +@RequiredArgsConstructor +public class PriceCalculationServiceImpl implements IPriceCalculationService { + + private final IProductConfigService productConfigService; + private final ICouponService couponService; + private final IPriceBundleService bundleService; + + @Override + public PriceCalculationResult calculatePrice(PriceCalculationRequest request) { + if (request.getProducts() == null || request.getProducts().isEmpty()) { + throw new PriceCalculationException("商品列表不能为空"); + } + + // 计算商品价格和原价 + PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts()); + BigDecimal totalAmount = priceDetails.getTotalAmount(); + BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount(); + + List discountDetails = new ArrayList<>(); + + // 添加限时立减折扣(如果原价 > 实际价格) + BigDecimal limitedTimeDiscount = originalTotalAmount.subtract(totalAmount); + if (limitedTimeDiscount.compareTo(BigDecimal.ZERO) > 0) { + discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount)); + } + + // 检查一口价优惠 + BigDecimal bundlePrice = bundleService.getBundlePrice(request.getProducts()); + if (bundlePrice != null && bundlePrice.compareTo(totalAmount) < 0) { + BigDecimal bundleDiscount = totalAmount.subtract(bundlePrice); + discountDetails.add(DiscountDetail.createBundleDiscount(bundleDiscount)); + totalAmount = bundlePrice; + log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount); + } + + PriceCalculationResult result = new PriceCalculationResult(); + result.setOriginalAmount(originalTotalAmount); // 原总价 + result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计 + result.setProductDetails(request.getProducts()); + + // 处理优惠券 + BigDecimal couponDiscountAmount = BigDecimal.ZERO; + if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) { + CouponInfo bestCoupon = couponService.selectBestCoupon( + request.getUserId(), request.getProducts(), totalAmount); + + if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + result.setUsedCoupon(bestCoupon); + couponDiscountAmount = bestCoupon.getActualDiscountAmount(); + discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount)); + } + } + + // 计算总优惠金额 + BigDecimal totalDiscountAmount = discountDetails.stream() + .map(DiscountDetail::getDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 按排序排列折扣明细 + discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(discountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + + return result; + } + + private BigDecimal calculateProductsPrice(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + BigDecimal itemPrice = calculateSingleProductPrice(product); + product.setUnitPrice(itemPrice); + + BigDecimal subtotal = itemPrice.multiply(BigDecimal.valueOf(product.getPurchaseCount())); + product.setSubtotal(subtotal); + + totalAmount = totalAmount.add(subtotal); + } + + return totalAmount.setScale(2, RoundingMode.HALF_UP); + } + + private PriceDetails calculateProductsPriceWithOriginal(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + BigDecimal originalTotalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + // 计算实际价格和原价 + ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product); + + 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) + ); + } + + private BigDecimal calculateSingleProductPrice(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + + if (tierConfig != null) { + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}", + productType.getCode(), productId, product.getQuantity(), tierConfig.getPrice()); + return tierConfig.getPrice(); + } + + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + } + + // 兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认 + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + + private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + BigDecimal actualPrice; + BigDecimal originalPrice = null; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + + if (tierConfig != null) { + actualPrice = tierConfig.getPrice(); + originalPrice = tierConfig.getOriginalPrice(); + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}", + productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice); + } else { + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法找到具体商品配置"); + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + + // 兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + } + } + + return new ProductPriceInfo(actualPrice, originalPrice); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java new file mode 100644 index 0000000..948f908 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -0,0 +1,107 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.*; +import com.ycwl.basic.pricing.mapper.*; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 价格管理服务实现(用于配置管理,手动处理时间字段) + */ +@Slf4j +@Service("pricingManagementServiceImpl") +@RequiredArgsConstructor +public class PricingManagementServiceImpl implements IPricingManagementService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final PriceBundleConfigMapper bundleConfigMapper; + + @Override + @Transactional + public Long createProductConfig(PriceProductConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + productConfigMapper.insertProductConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateProductConfig(PriceProductConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return productConfigMapper.updateProductConfig(config) > 0; + } + + @Override + @Transactional + public Long createTierConfig(PriceTierConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + tierConfigMapper.insertTierConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateTierConfig(PriceTierConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return tierConfigMapper.updateTierConfig(config) > 0; + } + + @Override + @Transactional + public Long createCouponConfig(PriceCouponConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + couponConfigMapper.insertCoupon(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateCouponConfig(PriceCouponConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return couponConfigMapper.updateCoupon(config) > 0; + } + + @Override + @Transactional + public Long createCouponClaimRecord(PriceCouponClaimRecord record) { + record.setClaimTime(LocalDateTime.now()); + record.setCreatedTime(LocalDateTime.now()); + record.setUpdatedTime(LocalDateTime.now()); + couponClaimRecordMapper.insertClaimRecord(record); + return record.getId(); + } + + @Override + @Transactional + public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) { + record.setUpdatedTime(LocalDateTime.now()); + return couponClaimRecordMapper.updateClaimRecord(record) > 0; + } + + @Override + @Transactional + public Long createBundleConfig(PriceBundleConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + bundleConfigMapper.insertBundleConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateBundleConfig(PriceBundleConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return bundleConfigMapper.updateBundleConfig(config) > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java new file mode 100644 index 0000000..acf74ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java @@ -0,0 +1,78 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.exception.ProductConfigNotFoundException; +import com.ycwl.basic.pricing.mapper.PriceProductConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceTierConfigMapper; +import com.ycwl.basic.pricing.service.IProductConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 商品配置管理服务实现 + */ +@Slf4j +@Service("pricingProductConfigServiceImpl") +@RequiredArgsConstructor +public class ProductConfigServiceImpl implements IProductConfigService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + + @Override + @Cacheable(value = "product-config", key = "#productType") + public List getProductConfig(String productType) { + return productConfigMapper.selectByProductType(productType); + } + + @Override + @Cacheable(value = "product-config", key = "#productType + '_' + #productId") + public PriceProductConfig getProductConfig(String productType, String productId) { + PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId); + if (config == null) { + throw new ProductConfigNotFoundException("商品配置未找到: " + productType + ", productId: " + productId); + } + return config; + } + + @Override + @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + (#productSubType ?: 'default') + '_' + #quantity") + public PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity) { + PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, productSubType, quantity); + if (config == null) { + log.warn("阶梯定价配置未找到: productType={}, productId={}, productSubType={}, quantity={}", + productType, productId, productSubType, quantity); + } + return config; + } + + @Override + @Deprecated + public PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity) { + // 兼容旧接口,使用默认productId + return getTierConfig(productType, "default", productSubType, quantity); + } + + @Override + @Cacheable(value = "active-product-configs") + public List getActiveProductConfigs() { + return productConfigMapper.selectActiveConfigs(); + } + + @Override + @Cacheable(value = "tier-configs", key = "#productType") + public List getTierConfigs(String productType) { + return tierConfigMapper.selectByProductType(productType); + } + + @Override + @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId") + public List getTierConfigs(String productType, String productId) { + return tierConfigMapper.selectByProductTypeAndId(productType, productId); + } +} \ No newline at end of file