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