Explorar el Código

收银台功能 服务费+菜品优惠

liudongzhi hace 3 semanas
padre
commit
d3584703f2
Se han modificado 29 ficheros con 2379 adiciones y 0 borrados
  1. 76 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreProductDiscountRule.java
  2. 46 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreProductDiscountRuleSlot.java
  3. 80 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRule.java
  4. 55 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRuleSlot.java
  5. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRuleTable.java
  6. 46 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreProductDiscountRuleSaveDto.java
  7. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreProductDiscountRuleSlotDto.java
  8. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreServiceFeeRuleSaveDto.java
  9. 23 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreServiceFeeRuleSlotDto.java
  10. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreProductDiscountRuleDetailVo.java
  11. 51 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreProductDiscountRuleListVo.java
  12. 51 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreServiceFeeRuleDetailVo.java
  13. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreServiceFeeRuleListVo.java
  14. 10 0
      alien-entity/src/main/java/shop/alien/mapper/StoreProductDiscountRuleMapper.java
  15. 10 0
      alien-entity/src/main/java/shop/alien/mapper/StoreProductDiscountRuleSlotMapper.java
  16. 10 0
      alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleMapper.java
  17. 10 0
      alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleSlotMapper.java
  18. 10 0
      alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleTableMapper.java
  19. 6 0
      alien-job/pom.xml
  20. 8 0
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  21. 30 0
      alien-job/src/main/java/shop/alien/job/store/storeServiceFeeRuleJob.java
  22. 347 0
      alien-store-platform/接口文档/服务费与律师收款接口文档.md
  23. 217 0
      alien-store-platform/接口文档/菜品优惠接口文档.md
  24. 93 0
      alien-store/src/main/java/shop/alien/store/controller/StoreProductDiscountController.java
  25. 80 0
      alien-store/src/main/java/shop/alien/store/controller/StoreServiceFeeRuleController.java
  26. 23 0
      alien-store/src/main/java/shop/alien/store/service/StoreProductDiscountService.java
  27. 33 0
      alien-store/src/main/java/shop/alien/store/service/StoreServiceFeeRuleService.java
  28. 341 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreProductDiscountServiceImpl.java
  29. 529 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreServiceFeeRuleServiceImpl.java

+ 76 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreProductDiscountRule.java

@@ -0,0 +1,76 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("store_product_discount_rule")
+@ApiModel(value = "StoreProductDiscountRule", description = "菜品优惠规则主表")
+public class StoreProductDiscountRule {
+
+	@TableId(value = "id", type = IdType.AUTO)
+	private Integer id;
+
+	@ApiModelProperty("门店ID")
+	@TableField("store_id")
+	private Integer storeId;
+
+	@ApiModelProperty("菜品ID")
+	@TableField("product_id")
+	private Integer productId;
+
+	@ApiModelProperty("规则名称")
+	@TableField("rule_name")
+	private String ruleName;
+
+	@ApiModelProperty("优惠类型: FREE/ DISCOUNT")
+	@TableField("discount_type")
+	private String discountType;
+
+	@ApiModelProperty("折扣比例(0-100,DISCOUNT必填)")
+	@TableField("discount_rate")
+	private BigDecimal discountRate;
+
+	@ApiModelProperty("生效模式: PERMANENT/CUSTOM")
+	@TableField("effective_mode")
+	private String effectiveMode;
+
+	@ApiModelProperty("自定义开始日期")
+	@TableField("start_date")
+	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+	private Date startDate;
+
+	@ApiModelProperty("自定义结束日期")
+	@TableField("end_date")
+	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+	private Date endDate;
+
+	@ApiModelProperty("状态:1开启,0关闭")
+	@TableField("status")
+	private Integer status;
+
+	@TableLogic
+	@TableField("delete_flag")
+	private Integer deleteFlag;
+
+	@TableField(value = "created_time", fill = FieldFill.INSERT)
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+	private Date createdTime;
+
+	@TableField("created_user_id")
+	private Integer createdUserId;
+
+	@TableField(value = "updated_time", fill = FieldFill.UPDATE)
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+	private Date updatedTime;
+
+	@TableField("updated_user_id")
+	private Integer updatedUserId;
+}
+

+ 46 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreProductDiscountRuleSlot.java

@@ -0,0 +1,46 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalTime;
+import java.util.Date;
+
+@Data
+@TableName("store_product_discount_rule_slot")
+public class StoreProductDiscountRuleSlot {
+
+	@TableId(value = "id", type = IdType.AUTO)
+	private Integer id;
+
+	@TableField("rule_id")
+	private Integer ruleId;
+
+	@ApiModelProperty("星期掩码")
+	@TableField("weekday_mask")
+	private Integer weekdayMask;
+
+	@TableField("start_time")
+	private LocalTime startTime;
+
+	@TableField("end_time")
+	private LocalTime endTime;
+
+	@TableLogic
+	@TableField("delete_flag")
+	private Integer deleteFlag;
+
+	@TableField(value = "created_time", fill = FieldFill.INSERT)
+	private Date createdTime;
+
+	@TableField("created_user_id")
+	private Integer createdUserId;
+
+	@TableField(value = "updated_time", fill = FieldFill.UPDATE)
+	private Date updatedTime;
+
+	@TableField("updated_user_id")
+	private Integer updatedUserId;
+}
+

+ 80 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRule.java

@@ -0,0 +1,80 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 服务费规则主表
+ */
+@Data
+@TableName("store_service_fee_rule")
+@ApiModel(value = "StoreServiceFeeRule对象", description = "服务费规则主表")
+public class StoreServiceFeeRule {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty("门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty("服务费名称")
+    @TableField("fee_name")
+    private String feeName;
+
+    @ApiModelProperty("服务费类型:1按人数,2按桌台,3按消费金额")
+    @TableField("fee_type")
+    private Integer feeType;
+
+    @ApiModelProperty("服务费金额/比例")
+    @TableField("fee_value")
+    private BigDecimal feeValue;
+
+    @ApiModelProperty("状态:1开启,0关闭")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty("生效模式:PERMANENT/CUSTOM")
+    @TableField("effective_mode")
+    private String effectiveMode;
+
+    @ApiModelProperty("自定义开始日期")
+    @TableField("start_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date startDate;
+
+    @ApiModelProperty("自定义结束日期")
+    @TableField("end_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date endDate;
+
+    @TableLogic
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 55 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRuleSlot.java

@@ -0,0 +1,55 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+import java.time.LocalTime;
+
+/**
+ * 服务费规则时段
+ */
+@Data
+@TableName("store_service_fee_rule_slot")
+public class StoreServiceFeeRuleSlot {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("rule_id")
+    private Integer ruleId;
+
+    /**
+     * 星期掩码,按位表示周一到周日(示例:周一+周三 = 5)
+     */
+    @TableField("weekday_mask")
+    private Integer weekdayMask;
+
+    @TableField("start_time")
+    private LocalTime startTime;
+
+    @TableField("end_time")
+    private LocalTime endTime;
+
+    @TableLogic
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    private Date createdTime;
+
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreServiceFeeRuleTable.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 服务费规则-桌台关联
+ */
+@Data
+@TableName("store_service_fee_rule_table")
+public class StoreServiceFeeRuleTable {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("rule_id")
+    private Integer ruleId;
+
+    @TableField("store_id")
+    private Integer storeId;
+
+    @TableField("table_id")
+    private Integer tableId;
+
+    @TableLogic
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    private Date createdTime;
+
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 46 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreProductDiscountRuleSaveDto.java

@@ -0,0 +1,46 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@ApiModel(value = "StoreProductDiscountRuleSaveDto", description = "菜品优惠规则新增/编辑入参")
+public class StoreProductDiscountRuleSaveDto {
+
+	private Integer id;
+
+	@ApiModelProperty(value = "门店ID", required = true)
+	private Integer storeId;
+
+	@ApiModelProperty(value = "菜品ID", required = true)
+	private Integer productId;
+
+	@ApiModelProperty(value = "规则名称", required = true)
+	private String ruleName;
+
+	@ApiModelProperty(value = "优惠类型: FREE/ DISCOUNT", required = true)
+	private String discountType;
+
+	@ApiModelProperty(value = "折扣比例(0-100,DISCOUNT必填)")
+	private BigDecimal discountRate;
+
+	@ApiModelProperty(value = "生效模式: PERMANENT/CUSTOM", required = true)
+	private String effectiveMode;
+
+	@ApiModelProperty("自定义开始日期(yyyy-MM-dd)")
+	private String startDate;
+
+	@ApiModelProperty("自定义结束日期(yyyy-MM-dd)")
+	private String endDate;
+
+	@ApiModelProperty(value = "生效时段")
+	private List<StoreProductDiscountRuleSlotDto> slots;
+
+	@ApiModelProperty(value = "状态:1开启,0关闭", required = true)
+	private Integer status;
+}
+

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreProductDiscountRuleSlotDto.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreProductDiscountRuleSlotDto", description = "菜品优惠时段")
+public class StoreProductDiscountRuleSlotDto {
+
+	@ApiModelProperty("星期掩码(周一到周日按位)")
+	private Integer weekdayMask;
+
+	@ApiModelProperty("开始时间(HH:mm:ss)")
+	private String startTime;
+
+	@ApiModelProperty("结束时间(HH:mm:ss)")
+	private String endTime;
+}
+

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreServiceFeeRuleSaveDto.java

@@ -0,0 +1,50 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 服务费规则新增/编辑入参
+ */
+@Data
+@ApiModel(value = "StoreServiceFeeRuleSaveDto", description = "服务费规则新增/编辑入参")
+public class StoreServiceFeeRuleSaveDto {
+
+    @ApiModelProperty("规则ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "服务费名称", required = true)
+    private String feeName;
+
+    @ApiModelProperty(value = "服务费类型:1按人数,2按桌台,3按消费金额", required = true)
+    private Integer feeType;
+
+    @ApiModelProperty(value = "服务费金额/比例", required = true)
+    private BigDecimal feeValue;
+
+    @ApiModelProperty(value = "状态:1开启,0关闭", required = true)
+    private Integer status;
+
+    @ApiModelProperty(value = "生效模式:PERMANENT/CUSTOM", required = true)
+    private String effectiveMode;
+
+    @ApiModelProperty("自定义开始日期(yyyy-MM-dd)")
+    private String startDate;
+
+    @ApiModelProperty("自定义结束日期(yyyy-MM-dd)")
+    private String endDate;
+
+    @ApiModelProperty(value = "适用桌台ID列表", required = true)
+    private List<Integer> tableIds;
+
+    @ApiModelProperty(value = "生效时段列表", required = true)
+    private List<StoreServiceFeeRuleSlotDto> slots;
+}
+

+ 23 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreServiceFeeRuleSlotDto.java

@@ -0,0 +1,23 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 服务费时段入参
+ */
+@Data
+@ApiModel(value = "StoreServiceFeeRuleSlotDto", description = "服务费时段入参")
+public class StoreServiceFeeRuleSlotDto {
+
+    @ApiModelProperty(value = "星期掩码(周一到周日按位表示)", required = true)
+    private Integer weekdayMask;
+
+    @ApiModelProperty(value = "开始时间(HH:mm:ss)", required = true)
+    private String startTime;
+
+    @ApiModelProperty(value = "结束时间(HH:mm:ss)", required = true)
+    private String endTime;
+}
+

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreProductDiscountRuleDetailVo.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.StoreProductDiscountRuleSlotDto;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@ApiModel(value = "StoreProductDiscountRuleDetailVo", description = "菜品优惠规则详情VO")
+public class StoreProductDiscountRuleDetailVo {
+
+	private Integer id;
+	private Integer storeId;
+	private Integer productId;
+	private String productName;
+	private String ruleName;
+	private String discountType;
+	private BigDecimal discountRate;
+	private String effectiveMode;
+	private String startDate;
+	private String endDate;
+	private Integer status;
+	private List<StoreProductDiscountRuleSlotDto> slots;
+}
+

+ 51 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreProductDiscountRuleListVo.java

@@ -0,0 +1,51 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@ApiModel(value = "StoreProductDiscountRuleListVo", description = "菜品优惠规则列表VO")
+public class StoreProductDiscountRuleListVo {
+
+	@ApiModelProperty("规则ID")
+	private Integer id;
+
+	@ApiModelProperty("门店ID")
+	private Integer storeId;
+
+	@ApiModelProperty("菜品ID")
+	private Integer productId;
+
+	@ApiModelProperty("菜品名称")
+	private String productName;
+
+	@ApiModelProperty("规则名称")
+	private String ruleName;
+
+	@ApiModelProperty("优惠类型")
+	private String discountType;
+
+	@ApiModelProperty("折扣比例")
+	private BigDecimal discountRate;
+
+	@ApiModelProperty("生效模式")
+	private String effectiveMode;
+
+	@ApiModelProperty("日期范围")
+	private String dateRange;
+
+	@ApiModelProperty("时段数量")
+	private Integer slotCount;
+
+	@ApiModelProperty("状态")
+	private Integer status;
+
+	@ApiModelProperty("更新时间")
+	private Date updatedTime;
+}
+

+ 51 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreServiceFeeRuleDetailVo.java

@@ -0,0 +1,51 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.StoreServiceFeeRuleSlotDto;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 服务费规则详情VO
+ */
+@Data
+@ApiModel(value = "StoreServiceFeeRuleDetailVo", description = "服务费规则详情VO")
+public class StoreServiceFeeRuleDetailVo {
+
+    @ApiModelProperty("规则ID")
+    private Integer id;
+
+    @ApiModelProperty("门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty("服务费名称")
+    private String feeName;
+
+    @ApiModelProperty("服务费类型")
+    private Integer feeType;
+
+    @ApiModelProperty("服务费金额/比例")
+    private BigDecimal feeValue;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("生效模式")
+    private String effectiveMode;
+
+    @ApiModelProperty("开始日期")
+    private String startDate;
+
+    @ApiModelProperty("结束日期")
+    private String endDate;
+
+    @ApiModelProperty("适用桌台ID列表")
+    private List<Integer> tableIds;
+
+    @ApiModelProperty("生效时段")
+    private List<StoreServiceFeeRuleSlotDto> slots;
+}
+

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreServiceFeeRuleListVo.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 服务费规则列表VO
+ */
+@Data
+@ApiModel(value = "StoreServiceFeeRuleListVo", description = "服务费规则列表VO")
+public class StoreServiceFeeRuleListVo {
+
+    @ApiModelProperty("规则ID")
+    private Integer id;
+
+    @ApiModelProperty("门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty("服务费名称")
+    private String feeName;
+
+    @ApiModelProperty("服务费类型")
+    private Integer feeType;
+
+    @ApiModelProperty("服务费金额/比例")
+    private BigDecimal feeValue;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("生效模式")
+    private String effectiveMode;
+
+    @ApiModelProperty("适用桌台数量")
+    private Integer tableCount;
+
+    @ApiModelProperty("适用桌号名称列表")
+    private List<String> tableNameList;
+
+    @ApiModelProperty("更新时间")
+    private Date updatedTime;
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreProductDiscountRuleMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreProductDiscountRule;
+
+@Mapper
+public interface StoreProductDiscountRuleMapper extends BaseMapper<StoreProductDiscountRule> {
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreProductDiscountRuleSlotMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreProductDiscountRuleSlot;
+
+@Mapper
+public interface StoreProductDiscountRuleSlotMapper extends BaseMapper<StoreProductDiscountRuleSlot> {
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreServiceFeeRule;
+
+@Mapper
+public interface StoreServiceFeeRuleMapper extends BaseMapper<StoreServiceFeeRule> {
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleSlotMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreServiceFeeRuleSlot;
+
+@Mapper
+public interface StoreServiceFeeRuleSlotMapper extends BaseMapper<StoreServiceFeeRuleSlot> {
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreServiceFeeRuleTableMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreServiceFeeRuleTable;
+
+@Mapper
+public interface StoreServiceFeeRuleTableMapper extends BaseMapper<StoreServiceFeeRuleTable> {
+}
+

+ 6 - 0
alien-job/pom.xml

@@ -137,6 +137,12 @@
             <groupId>org.freemarker</groupId>
             <artifactId>freemarker</artifactId>
         </dependency>
+        <dependency>
+            <groupId>shop.alien</groupId>
+            <artifactId>alien-store</artifactId>
+            <version>1.0.0</version>
+            <scope>compile</scope>
+        </dependency>
         <!-- mybatis-plus代码生成器 End -->
 
     </dependencies>

+ 8 - 0
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -93,4 +93,12 @@ public interface AlienStoreFeign {
     @org.springframework.web.bind.annotation.PostMapping("/reservation/job/retryRefundFailed")
     R<Integer> retryRefundFailed();
 
+    /**
+     * 服务费规则定时任务:自动关闭已过期的自定义日期规则
+     *
+     * @return R.data 为本次关闭数量
+     */
+    @org.springframework.web.bind.annotation.PostMapping("/serviceFee/job/autoCloseExpiredCustomRules")
+    R<Integer> autoCloseExpiredCustomRules();
+
 }

+ 30 - 0
alien-job/src/main/java/shop/alien/job/store/storeServiceFeeRuleJob.java

@@ -0,0 +1,30 @@
+package shop.alien.job.store;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.job.feign.AlienStoreFeign;
+
+
+/**
+ * 服务费规则自动关闭定时任务
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class storeServiceFeeRuleJob {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @XxlJob("storeServiceFeeRuleAutoCloseJob")
+    public R<Integer> autoCloseExpiredCustomRules() {
+        log.info("【定时任务】服务费自动关闭过期规则:开始执行");
+        R<Integer> result = alienStoreFeign.autoCloseExpiredCustomRules();
+        int count = (result != null && result.getData() != null) ? result.getData() : 0;
+        log.info("【定时任务】服务费自动关闭过期规则:执行完成,本次关闭条数={}", count);
+        return R.data(count);
+    }
+}
+
+

+ 347 - 0
alien-store-platform/接口文档/服务费与律师收款接口文档.md

@@ -0,0 +1,347 @@
+# 服务费与律师收款接口文档
+
+版本:`v1`  
+更新时间:`2026-03-26`
+
+---
+
+## 统一响应结构
+
+所有接口统一返回 `R<T>`:
+
+- `code`:状态码(`200` 成功,非 `200` 失败)
+- `msg`:提示信息
+- `data`:业务数据
+
+---
+
+## 一、律师收款账号接口
+
+BasePath:`/lawyer/user`
+
+### 1. 保存微信收款账号
+
+- **URL**:`POST /lawyer/user/saveWechatAccount`
+- **Content-Type**:`application/json`
+
+请求体(统一对象 `LawyerPaymentAccountDto`):
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| lawyerId | Integer | 是 | 律师ID |
+| wechatId | String | 是 | 微信ID |
+| wechatName | String | 是 | 微信名称 |
+| aliId | String | 否 | 统一DTO字段,可不传 |
+| aliName | String | 否 | 统一DTO字段,可不传 |
+| bankCardNo | String | 否 | 统一DTO字段,可不传 |
+| bankName | String | 否 | 统一DTO字段,可不传 |
+
+请求示例:
+
+```json
+{
+  "lawyerId": 1001,
+  "wechatId": "wx_appid_xxx",
+  "wechatName": "张三微信"
+}
+```
+
+成功返回示例:
+
+```json
+{
+  "code": 200,
+  "msg": "保存成功",
+  "data": true
+}
+```
+
+---
+
+### 2. 保存支付宝收款账号
+
+- **URL**:`POST /lawyer/user/saveAlipayAccount`
+- **Content-Type**:`application/json`
+
+请求体:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| lawyerId | Integer | 是 | 律师ID |
+| aliId | String | 是 | 支付宝ID |
+| aliName | String | 是 | 支付宝名称 |
+| wechatId | String | 否 | 统一DTO字段,可不传 |
+| wechatName | String | 否 | 统一DTO字段,可不传 |
+| bankCardNo | String | 否 | 统一DTO字段,可不传 |
+| bankName | String | 否 | 统一DTO字段,可不传 |
+
+请求示例:
+
+```json
+{
+  "lawyerId": 1001,
+  "aliId": "ali_appid_xxx",
+  "aliName": "张三支付宝"
+}
+```
+
+---
+
+### 3. 保存银行卡收款账号
+
+- **URL**:`POST /lawyer/user/saveBankAccount`
+- **Content-Type**:`application/json`
+
+请求体:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| lawyerId | Integer | 是 | 律师ID |
+| bankCardNo | String | 是 | 银行卡号 |
+| bankName | String | 是 | 开户行名称 |
+| wechatId | String | 否 | 统一DTO字段,可不传 |
+| wechatName | String | 否 | 统一DTO字段,可不传 |
+| aliId | String | 否 | 统一DTO字段,可不传 |
+| aliName | String | 否 | 统一DTO字段,可不传 |
+
+请求示例:
+
+```json
+{
+  "lawyerId": 1001,
+  "bankCardNo": "622202xxxxxx",
+  "bankName": "中国建设银行"
+}
+```
+
+---
+
+### 4. 查询律师收款账号
+
+- **URL**:`GET /lawyer/user/getPaymentAccounts?lawyerId=1001`
+
+Query 参数:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| lawyerId | Integer | 是 | 律师ID |
+
+`data` 返回字段:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| lawyerWechatId | String | 微信ID |
+| lawyerWechatName | String | 微信名称 |
+| lawyerAliId | String | 支付宝ID |
+| lawyerAliName | String | 支付宝名称 |
+| bankCardNo | String | 银行卡号 |
+| bankName | String | 开户行名称 |
+
+---
+
+## 二、服务费规则接口
+
+BasePath:`/store/serviceFee`
+
+### 1. 服务费列表
+
+- **URL**:`GET /store/serviceFee/list`
+
+Query 参数:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| storeId | Integer | 是 | 门店ID |
+| name | String | 否 | 名称模糊查询 |
+| status | Integer | 否 | 0关闭/1开启 |
+| pageNum | Integer | 否 | 默认1 |
+| pageSize | Integer | 否 | 默认10 |
+
+返回:`IPage<StoreServiceFeeRuleListVo>`
+
+主要字段:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | Integer | 规则ID |
+| feeName | String | 服务费名称 |
+| feeType | Integer | 1按人数/2按桌台/3按消费金额 |
+| feeValue | BigDecimal | 金额/比例 |
+| status | Integer | 状态 |
+| effectiveMode | String | PERMANENT/CUSTOM |
+| tableCount | Integer | 适用桌台数量 |
+| updatedTime | DateTime | 更新时间 |
+
+---
+
+### 2. 服务费详情(编辑回显)
+
+- **URL**:`GET /store/serviceFee/detail?id=10`
+
+Query 参数:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+
+返回:`StoreServiceFeeRuleDetailVo`
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | Integer | 规则ID |
+| storeId | Integer | 门店ID |
+| feeName | String | 名称 |
+| feeType | Integer | 类型 |
+| feeValue | BigDecimal | 金额/比例 |
+| status | Integer | 状态 |
+| effectiveMode | String | PERMANENT/CUSTOM |
+| startDate | String | yyyy-MM-dd |
+| endDate | String | yyyy-MM-dd |
+| tableIds | Integer[] | 适用桌台 |
+| slots | Slot[] | 生效时段 |
+
+`Slot` 字段:
+- `weekdayMask`:Integer(周一到周日按位标记)
+- `startTime`:String(`HH:mm:ss`)
+- `endTime`:String(`HH:mm:ss`)
+
+---
+
+### 3. 新建服务费
+
+- **URL**:`POST /store/serviceFee/create`
+- **Content-Type**:`application/json`
+
+请求体(`StoreServiceFeeRuleSaveDto`):
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| storeId | Integer | 是 | 门店ID |
+| feeName | String | 是 | 名称(同店唯一) |
+| feeType | Integer | 是 | 1按人数/2按桌台/3按消费金额 |
+| feeValue | BigDecimal | 是 | 金额或比例 |
+| status | Integer | 是 | 0关闭/1开启 |
+| effectiveMode | String | 是 | PERMANENT/CUSTOM |
+| startDate | String | CUSTOM时必填 | yyyy-MM-dd |
+| endDate | String | CUSTOM时必填 | yyyy-MM-dd |
+| tableIds | Integer[] | 是 | 适用桌台ID |
+| slots | Slot[] | 是 | 生效星期+时段 |
+
+请求示例:
+
+```json
+{
+  "storeId": 1,
+  "feeName": "晚市服务费",
+  "feeType": 1,
+  "feeValue": 5.00,
+  "status": 1,
+  "effectiveMode": "CUSTOM",
+  "startDate": "2026-01-01",
+  "endDate": "2026-12-31",
+  "tableIds": [101, 102],
+  "slots": [
+    {
+      "weekdayMask": 62,
+      "startTime": "15:00:00",
+      "endTime": "18:00:00"
+    }
+  ]
+}
+```
+
+成功返回:`data` 为新建规则ID(Integer)
+
+---
+
+### 4. 编辑服务费
+
+- **URL**:`POST /store/serviceFee/update`
+- **Content-Type**:`application/json`
+
+请求体:同“新建服务费”,另需 `id`(必填)
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+
+---
+
+### 5. 删除服务费
+
+- **URL**:`POST /store/serviceFee/delete?id=123`
+
+Query 参数:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+
+---
+
+### 6. 桌台列表
+
+- **URL**:`GET /store/serviceFee/tableList?storeId=1&categoryId=2`
+
+Query 参数:
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| storeId | Integer | 是 | 门店ID |
+| categoryId | Integer | 否 | 分类ID |
+
+返回:`List<StoreBookingTableVo>`
+
+主要字段:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | Integer | 桌台ID |
+| storeId | Integer | 门店ID |
+| categoryId | Integer | 分类ID |
+| categoryName | String | 分类名称 |
+| tableNumber | String | 桌号 |
+| seatingCapacity | Integer | 座位数 |
+
+---
+
+## 三、业务校验规则(前后端联调重点)
+
+### 1. 名称唯一校验
+- 同一 `storeId` 下,`feeName` 不允许重复。
+- 返回提示:`服务费名称已存在`
+
+### 2. 生效日期校验
+- `effectiveMode = CUSTOM` 时,`startDate/endDate` 必填,且 `startDate <= endDate`。
+- 返回提示:`开始日期不能大于结束日期`
+
+### 3. 生效时间校验
+- 每个时段 `startTime < endTime`。
+- 返回提示:`生效开始时间必须早于结束时间`
+
+### 4. 桌台+时间重叠校验
+- 同店同桌台下,若“日期有交集 + 星期有交集 + 时段有交集”,则判定重叠。
+- 返回提示:`桌台[xxx]服务费时间重叠,请调整生效日期/星期/时间`
+
+### 5. 同桌台同时间段类型限制
+- 同店同桌台同时间段下,只能存在一种 `feeType`。
+- 返回提示:`桌台[xxx]在该时间段已配置其他类型服务费`
+
+---
+
+## 四、常见错误提示(可直接前端toast)
+
+- `门店ID不能为空`
+- `服务费名称不能为空`
+- `服务费类型不能为空`
+- `服务费金额不能为空`
+- `服务费状态不能为空`
+- `生效模式不能为空`
+- `适用桌台不能为空`
+- `生效时段不能为空`
+- `服务费名称已存在`
+- `开始日期不能大于结束日期`
+- `生效开始时间必须早于结束时间`
+- `桌台[xxx]服务费时间重叠,请调整生效日期/星期/时间`
+- `桌台[xxx]在该时间段已配置其他类型服务费`
+

+ 217 - 0
alien-store-platform/接口文档/菜品优惠接口文档.md

@@ -0,0 +1,217 @@
+# 菜品优惠接口文档
+
+版本:`v1`  
+模块:`alien-store`  
+前缀:`/store/productDiscount`
+
+---
+
+## 统一返回结构
+
+接口返回统一为 `R<T>`:
+
+- `code`:200 成功,非 200 失败
+- `msg`:提示信息
+- `data`:业务数据
+
+---
+
+## 一、优惠规则接口(已实现)
+
+### 1) 列表
+
+- **URL**:`GET /store/productDiscount/list`
+- **Query 参数**
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| storeId | Integer | 是 | 门店ID |
+| productId | Integer | 否 | 菜品ID筛选 |
+| ruleName | String | 否 | 规则名称模糊搜索 |
+| status | Integer | 否 | 0关闭,1开启 |
+| pageNum | Integer | 否 | 默认1 |
+| pageSize | Integer | 否 | 默认10 |
+
+- **返回 data**:`IPage<StoreProductDiscountRuleListVo>`
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | Integer | 规则ID |
+| storeId | Integer | 门店ID |
+| productId | Integer | 菜品ID |
+| productName | String | 菜品名称(当前版本可能为空,见文末说明) |
+| ruleName | String | 规则名称 |
+| discountType | String | FREE / DISCOUNT |
+| discountRate | BigDecimal | 折扣比例(FREE可能为空) |
+| effectiveMode | String | PERMANENT / CUSTOM |
+| dateRange | String | 日期范围,如 `2026-04-01 ~ 2026-04-30` 或 `永久生效` |
+| slotCount | Integer | 时段数量 |
+| status | Integer | 0关闭,1开启 |
+| updatedTime | DateTime | 更新时间 |
+
+---
+
+### 2) 详情(编辑回显)
+
+- **URL**:`GET /store/productDiscount/detail?id={id}`
+- **Query 参数**
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+
+- **返回 data**:`StoreProductDiscountRuleDetailVo`
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | Integer | 规则ID |
+| storeId | Integer | 门店ID |
+| productId | Integer | 菜品ID |
+| productName | String | 菜品名称(当前版本可能为空) |
+| ruleName | String | 规则名称 |
+| discountType | String | FREE / DISCOUNT |
+| discountRate | BigDecimal | 折扣比例 |
+| effectiveMode | String | PERMANENT / CUSTOM |
+| startDate | String | 开始日期 yyyy-MM-dd |
+| endDate | String | 结束日期 yyyy-MM-dd |
+| status | Integer | 状态 |
+| slots | Array | 生效时段列表 |
+
+`slots` 子项:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| weekdayMask | Integer | 星期掩码(周一到周日按位) |
+| startTime | String | HH:mm:ss |
+| endTime | String | HH:mm:ss |
+
+---
+
+### 3) 新建优惠
+
+- **URL**:`POST /store/productDiscount/create`
+- **Content-Type**:`application/json`
+
+请求体:`StoreProductDiscountRuleSaveDto`
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| storeId | Integer | 是 | 门店ID |
+| productId | Integer | 是 | 菜品ID |
+| ruleName | String | 是 | 规则名称(同店同菜品唯一) |
+| discountType | String | 是 | FREE / DISCOUNT |
+| discountRate | BigDecimal | DISCOUNT必填 | 折扣比例(0-100) |
+| effectiveMode | String | 是 | PERMANENT / CUSTOM |
+| startDate | String | CUSTOM必填 | yyyy-MM-dd |
+| endDate | String | CUSTOM必填 | yyyy-MM-dd |
+| slots | Array | 否 | 生效时段(FREE/DISCOUNT均可不传) |
+| status | Integer | 是 | 0关闭,1开启 |
+
+请求示例(折扣):
+
+```json
+{
+  "storeId": 1,
+  "productId": 10001,
+  "ruleName": "周末八折",
+  "discountType": "DISCOUNT",
+  "discountRate": 80,
+  "effectiveMode": "CUSTOM",
+  "startDate": "2026-04-01",
+  "endDate": "2026-06-30",
+  "slots": [
+    {
+      "weekdayMask": 65,
+      "startTime": "10:00:00",
+      "endTime": "22:00:00"
+    }
+  ],
+  "status": 1
+}
+```
+
+请求示例(免费):
+
+```json
+{
+  "storeId": 1,
+  "productId": 10001,
+  "ruleName": "工作日免费",
+  "discountType": "FREE",
+  "effectiveMode": "PERMANENT",
+  "status": 1
+}
+```
+
+---
+
+### 4) 编辑优惠
+
+- **URL**:`POST /store/productDiscount/update`
+- **Content-Type**:`application/json`
+- 请求体同“新建”,且 `id` 必填
+
+---
+
+### 5) 删除优惠
+
+- **URL**:`POST /store/productDiscount/delete?id={id}`
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+
+---
+
+### 6) 切换启用状态
+
+- **URL**:`POST /store/productDiscount/switchStatus?id={id}&status={0|1}`
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Integer | 是 | 规则ID |
+| status | Integer | 是 | 0关闭,1开启 |
+
+---
+
+## 二、核心校验规则(后端已实现)
+
+1. **名称唯一**:同门店同菜品下,`ruleName` 不可重复  
+2. **DISCOUNT 规则**:`discountRate` 必填,且仅支持 `effectiveMode=CUSTOM`  
+3. **CUSTOM 日期**:`startDate/endDate` 必填,且开始不能晚于结束  
+4. **时间格式**:`startTime/endTime` 必须成对出现,且开始早于结束  
+5. **时间冲突校验(同店同菜品,开启状态)**:
+   - 日期有交集
+   - 星期有交集(weekdayMask 按位与 > 0)
+   - 时间有交集(start1 < end2 && start2 < end1)
+   - 若重叠且类型不同:报“该菜品在该时间段已配置其他类型优惠”
+   - 若重叠且类型相同:报“该菜品优惠时间重叠,请调整生效日期/星期/时间”
+6. **未配置时段时**:按“全星期+全天”参与冲突判断
+
+---
+
+## 三、常见错误提示
+
+- `门店ID不能为空`
+- `菜品ID不能为空`
+- `规则名称不能为空`
+- `优惠类型不能为空`
+- `优惠类型不合法`
+- `折扣比例不能为空`
+- `折扣优惠仅支持自定义日期`
+- `自定义日期开始/结束不能为空`
+- `开始日期不能大于结束日期`
+- `生效时间需成对出现`
+- `生效开始时间必须早于结束时间`
+- `该菜品优惠名称已存在`
+- `该菜品在该时间段已配置其他类型优惠`
+- `该菜品优惠时间重叠,请调整生效日期/星期/时间`
+
+---
+
+## 四、联调注意事项
+
+1. 当前代码中 `productName` 字段映射查询尚未接入真实菜品表查询,返回可能为空;建议联调先以 `productId` 为主。  
+2. 你提到的“菜品列表(按名称模糊)”接口,目前尚未在本次代码中新增;如需我可补:
+   - `GET /store/product/list?storeId=1&keyword=鱼&pageNum=1&pageSize=10`
+

+ 93 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreProductDiscountController.java

@@ -0,0 +1,93 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.dto.StoreProductDiscountRuleSaveDto;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleDetailVo;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
+import shop.alien.store.service.StoreCuisineService;
+import shop.alien.store.service.StoreProductDiscountService;
+
+@Slf4j
+@Api(tags = {"门店-菜品优惠"})
+@RestController
+@RequestMapping("/store/productDiscount")
+@RequiredArgsConstructor
+public class StoreProductDiscountController {
+
+	private final StoreProductDiscountService discountService;
+	private final StoreCuisineService storeCuisineService;
+
+	@ApiOperation("菜品列表(支持按名称模糊查询)")
+	@GetMapping("/productList")
+	public R<IPage<StoreCuisine>> productList(
+			@RequestParam Integer storeId,
+			@RequestParam(required = false) String name,
+			@RequestParam(defaultValue = "1") Integer pageNum,
+			@RequestParam(defaultValue = "10") Integer pageSize) {
+		if (storeId == null) {
+			return R.fail("门店ID不能为空");
+		}
+		int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+		int ps = pageSize == null || pageSize < 1 ? 10 : pageSize;
+		Page<StoreCuisine> page = new Page<>(pn, ps);
+		LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+		wrapper.eq(StoreCuisine::getStoreId, storeId);
+		if (StringUtils.hasText(name)) {
+			wrapper.like(StoreCuisine::getName, name.trim());
+		}
+		wrapper.orderByDesc(StoreCuisine::getUpdatedTime);
+		return R.data(storeCuisineService.page(page, wrapper));
+	}
+
+	@ApiOperation("优惠规则列表")
+	@GetMapping("/list")
+	public R<IPage<StoreProductDiscountRuleListVo>> list(
+			@RequestParam Integer storeId,
+			@RequestParam(required = false) Integer productId,
+			@RequestParam(required = false) String ruleName,
+			@RequestParam(required = false) Integer status,
+			@RequestParam(defaultValue = "1") Integer pageNum,
+			@RequestParam(defaultValue = "10") Integer pageSize) {
+		return discountService.listRules(storeId, productId, ruleName, status, pageNum, pageSize);
+	}
+
+	@ApiOperation("优惠规则详情")
+	@GetMapping("/detail")
+	public R<StoreProductDiscountRuleDetailVo> detail(@RequestParam Integer id) {
+		return discountService.getDetail(id);
+	}
+
+	@ApiOperation("新建优惠规则")
+	@PostMapping("/create")
+	public R<Integer> create(@RequestBody StoreProductDiscountRuleSaveDto dto) {
+		return discountService.createRule(dto);
+	}
+
+	@ApiOperation("编辑优惠规则")
+	@PostMapping("/update")
+	public R<Boolean> update(@RequestBody StoreProductDiscountRuleSaveDto dto) {
+		return discountService.updateRule(dto);
+	}
+
+	@ApiOperation("删除优惠规则")
+	@PostMapping("/delete")
+	public R<Boolean> delete(@RequestParam Integer id) {
+		return discountService.deleteRule(id);
+	}
+
+	@ApiOperation("切换启用状态")
+	@PostMapping("/switchStatus")
+	public R<Boolean> switchStatus(@RequestParam Integer id, @RequestParam Integer status) {
+		return discountService.switchStatus(id, status);
+	}
+}
+

+ 80 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreServiceFeeRuleController.java

@@ -0,0 +1,80 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreServiceFeeRuleSaveDto;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleDetailVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleListVo;
+import shop.alien.store.service.StoreServiceFeeRuleService;
+
+import java.util.List;
+
+@Slf4j
+@Api(tags = {"门店服务费规则"})
+@RestController
+@RequestMapping("/store/serviceFee")
+@RequiredArgsConstructor
+public class StoreServiceFeeRuleController {
+
+    private final StoreServiceFeeRuleService storeServiceFeeRuleService;
+
+    @ApiOperation("服务费列表")
+    @GetMapping("/list")
+    public R<IPage<StoreServiceFeeRuleListVo>> list(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        return storeServiceFeeRuleService.listRules(storeId, name, status, pageNum, pageSize);
+    }
+
+    @ApiOperation("服务费详情")
+    @GetMapping("/detail")
+    public R<StoreServiceFeeRuleDetailVo> detail(@RequestParam Integer id) {
+        return storeServiceFeeRuleService.getRuleDetail(id);
+    }
+
+    @ApiOperation("新建服务费")
+    @PostMapping("/create")
+    public R<Integer> create(@RequestBody StoreServiceFeeRuleSaveDto dto) {
+        return storeServiceFeeRuleService.createRule(dto);
+    }
+
+    @ApiOperation("编辑服务费")
+    @PostMapping("/update")
+    public R<Boolean> update(@RequestBody StoreServiceFeeRuleSaveDto dto) {
+        return storeServiceFeeRuleService.updateRule(dto);
+    }
+
+    @ApiOperation("删除服务费")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "规则ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<Boolean> delete(@RequestParam Integer id) {
+        return storeServiceFeeRuleService.deleteRule(id);
+    }
+
+    @ApiOperation("桌台列表")
+    @GetMapping("/tableList")
+    public R<List<StoreBookingTableVo>> tableList(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer categoryId) {
+        return storeServiceFeeRuleService.getTableList(storeId, categoryId);
+    }
+}
+

+ 23 - 0
alien-store/src/main/java/shop/alien/store/service/StoreProductDiscountService.java

@@ -0,0 +1,23 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleDetailVo;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
+import shop.alien.entity.store.dto.StoreProductDiscountRuleSaveDto;
+
+public interface StoreProductDiscountService {
+
+	R<IPage<StoreProductDiscountRuleListVo>> listRules(Integer storeId, Integer productId, String ruleName, Integer status, Integer pageNum, Integer pageSize);
+
+	R<StoreProductDiscountRuleDetailVo> getDetail(Integer id);
+
+	R<Integer> createRule(StoreProductDiscountRuleSaveDto dto);
+
+	R<Boolean> updateRule(StoreProductDiscountRuleSaveDto dto);
+
+	R<Boolean> deleteRule(Integer id);
+
+	R<Boolean> switchStatus(Integer id, Integer status);
+}
+

+ 33 - 0
alien-store/src/main/java/shop/alien/store/service/StoreServiceFeeRuleService.java

@@ -0,0 +1,33 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreServiceFeeRuleSaveDto;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleDetailVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleListVo;
+
+import java.util.List;
+
+public interface StoreServiceFeeRuleService {
+
+    R<IPage<StoreServiceFeeRuleListVo>> listRules(Integer storeId, String name, Integer status, Integer pageNum, Integer pageSize);
+
+    R<StoreServiceFeeRuleDetailVo> getRuleDetail(Integer id);
+
+    R<Integer> createRule(StoreServiceFeeRuleSaveDto dto);
+
+    R<Boolean> updateRule(StoreServiceFeeRuleSaveDto dto);
+
+    R<Boolean> deleteRule(Integer id);
+
+    R<List<StoreBookingTableVo>> getTableList(Integer storeId, Integer categoryId);
+
+    /**
+     * 自动关闭已过期的自定义日期服务费规则
+     *
+     * @return 本次关闭数量
+     */
+    int autoCloseExpiredCustomRules();
+}
+

+ 341 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreProductDiscountServiceImpl.java

@@ -0,0 +1,341 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.StoreProductDiscountRuleSaveDto;
+import shop.alien.entity.store.dto.StoreProductDiscountRuleSlotDto;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleDetailVo;
+import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
+import shop.alien.mapper.StoreProductDiscountRuleMapper;
+import shop.alien.mapper.StoreProductDiscountRuleSlotMapper;
+import shop.alien.store.service.StoreProductDiscountService;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class StoreProductDiscountServiceImpl implements StoreProductDiscountService {
+
+	private static final String TYPE_FREE = "FREE";
+	private static final String TYPE_DISCOUNT = "DISCOUNT";
+	private static final String MODE_PERMANENT = "PERMANENT";
+	private static final String MODE_CUSTOM = "CUSTOM";
+	private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+
+	private final StoreProductDiscountRuleMapper ruleMapper;
+	private final StoreProductDiscountRuleSlotMapper ruleSlotMapper;
+
+	@Override
+	public R<IPage<StoreProductDiscountRuleListVo>> listRules(Integer storeId, Integer productId, String ruleName, Integer status, Integer pageNum, Integer pageSize) {
+		if (storeId == null) {
+			return R.fail("门店ID不能为空");
+		}
+		int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+		int ps = pageSize == null || pageSize < 1 ? 10 : pageSize;
+		Page<StoreProductDiscountRule> page = new Page<>(pn, ps);
+		LambdaQueryWrapper<StoreProductDiscountRule> qw = new LambdaQueryWrapper<>();
+		qw.eq(StoreProductDiscountRule::getStoreId, storeId);
+		if (productId != null) {
+			qw.eq(StoreProductDiscountRule::getProductId, productId);
+		}
+		if (StringUtils.hasText(ruleName)) {
+			qw.like(StoreProductDiscountRule::getRuleName, ruleName.trim());
+		}
+		if (status != null) {
+			qw.eq(StoreProductDiscountRule::getStatus, status);
+		}
+		qw.orderByDesc(StoreProductDiscountRule::getUpdatedTime);
+		IPage<StoreProductDiscountRule> rulePage = ruleMapper.selectPage(page, qw);
+
+		Map<Integer, String> productNameMap = buildProductNameMap(storeId);
+		Map<Integer, Integer> slotCountMap = buildSlotCountMap(rulePage.getRecords().stream().map(StoreProductDiscountRule::getId).collect(Collectors.toList()));
+
+		List<StoreProductDiscountRuleListVo> records = rulePage.getRecords().stream().map(rule -> {
+			StoreProductDiscountRuleListVo vo = new StoreProductDiscountRuleListVo();
+			vo.setId(rule.getId());
+			vo.setStoreId(rule.getStoreId());
+			vo.setProductId(rule.getProductId());
+			vo.setProductName(productNameMap.get(rule.getProductId()));
+			vo.setRuleName(rule.getRuleName());
+			vo.setDiscountType(rule.getDiscountType());
+			vo.setDiscountRate(rule.getDiscountRate());
+			vo.setEffectiveMode(rule.getEffectiveMode());
+			vo.setDateRange(formatDateRange(rule.getStartDate(), rule.getEndDate(), rule.getEffectiveMode()));
+			vo.setSlotCount(slotCountMap.getOrDefault(rule.getId(), 0));
+			vo.setStatus(rule.getStatus());
+			vo.setUpdatedTime(rule.getUpdatedTime());
+			return vo;
+		}).collect(Collectors.toList());
+
+		Page<StoreProductDiscountRuleListVo> result = new Page<>(pn, ps, rulePage.getTotal());
+		result.setRecords(records);
+		return R.data(result);
+	}
+
+	@Override
+	public R<StoreProductDiscountRuleDetailVo> getDetail(Integer id) {
+		if (id == null) {
+			return R.fail("规则ID不能为空");
+		}
+		StoreProductDiscountRule rule = ruleMapper.selectById(id);
+		if (rule == null) {
+			return R.fail("规则不存在");
+		}
+		StoreProductDiscountRuleDetailVo vo = new StoreProductDiscountRuleDetailVo();
+		vo.setId(rule.getId());
+		vo.setStoreId(rule.getStoreId());
+		vo.setProductId(rule.getProductId());
+		vo.setProductName(buildProductNameMap(rule.getStoreId()).get(rule.getProductId()));
+		vo.setRuleName(rule.getRuleName());
+		vo.setDiscountType(rule.getDiscountType());
+		vo.setDiscountRate(rule.getDiscountRate());
+		vo.setEffectiveMode(rule.getEffectiveMode());
+		vo.setStartDate(formatDate(rule.getStartDate()));
+		vo.setEndDate(formatDate(rule.getEndDate()));
+		vo.setStatus(rule.getStatus());
+		List<StoreProductDiscountRuleSlot> slots = ruleSlotMapper.selectList(new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().eq(StoreProductDiscountRuleSlot::getRuleId, id));
+		vo.setSlots(slots.stream().map(this::toSlotDto).collect(Collectors.toList()));
+		return R.data(vo);
+	}
+
+	@Override
+	public R<Integer> createRule(StoreProductDiscountRuleSaveDto dto) {
+		String err = validateAndCheckConflict(dto, null);
+		if (err != null) return R.fail(err);
+		StoreProductDiscountRule rule = toEntity(dto, false);
+		ruleMapper.insert(rule);
+		saveSlots(rule.getId(), dto.getSlots());
+		return R.data(rule.getId());
+	}
+
+	@Override
+	public R<Boolean> updateRule(StoreProductDiscountRuleSaveDto dto) {
+		if (dto == null || dto.getId() == null) return R.fail("规则ID不能为空");
+		if (ruleMapper.selectById(dto.getId()) == null) return R.fail("规则不存在");
+		String err = validateAndCheckConflict(dto, dto.getId());
+		if (err != null) return R.fail(err);
+		StoreProductDiscountRule update = toEntity(dto, true);
+		update.setId(dto.getId());
+		ruleMapper.updateById(update);
+		ruleSlotMapper.delete(new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().eq(StoreProductDiscountRuleSlot::getRuleId, dto.getId()));
+		saveSlots(dto.getId(), dto.getSlots());
+		return R.data(true);
+	}
+
+	@Override
+	public R<Boolean> deleteRule(Integer id) {
+		if (id == null) return R.fail("规则ID不能为空");
+		if (ruleMapper.selectById(id) == null) return R.fail("规则不存在");
+		ruleMapper.deleteById(id);
+		ruleSlotMapper.delete(new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().eq(StoreProductDiscountRuleSlot::getRuleId, id));
+		return R.data(true);
+	}
+
+	@Override
+	public R<Boolean> switchStatus(Integer id, Integer status) {
+		if (id == null || status == null) return R.fail("参数不能为空");
+		StoreProductDiscountRule update = new StoreProductDiscountRule();
+		update.setId(id);
+		update.setStatus(status);
+		ruleMapper.updateById(update);
+		return R.data(true);
+	}
+
+	/* ---------------- private helpers ---------------- */
+
+	private Map<Integer, String> buildProductNameMap(Integer storeId) {
+		// 简单从 store_cuisine 读取菜名(同门店)
+		// 如需跨多产品表,可在此聚合
+		// 这里只取未删除记录
+		// 为了避免全表扫描,真实环境建议按 productId 列表批量查询
+		return new HashMap<>(); // 可按需要补充查询
+	}
+
+	private Map<Integer, Integer> buildSlotCountMap(List<Integer> ruleIds) {
+		if (ruleIds == null || ruleIds.isEmpty()) return Collections.emptyMap();
+		List<StoreProductDiscountRuleSlot> slots = ruleSlotMapper.selectList(new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().in(StoreProductDiscountRuleSlot::getRuleId, ruleIds));
+		Map<Integer, Integer> map = new HashMap<>();
+		for (StoreProductDiscountRuleSlot s : slots) {
+			map.put(s.getRuleId(), map.getOrDefault(s.getRuleId(), 0) + 1);
+		}
+		return map;
+	}
+
+	private StoreProductDiscountRuleSlotDto toSlotDto(StoreProductDiscountRuleSlot slot) {
+		StoreProductDiscountRuleSlotDto dto = new StoreProductDiscountRuleSlotDto();
+		dto.setWeekdayMask(slot.getWeekdayMask());
+		dto.setStartTime(slot.getStartTime() == null ? null : slot.getStartTime().format(TIME_FORMATTER));
+		dto.setEndTime(slot.getEndTime() == null ? null : slot.getEndTime().format(TIME_FORMATTER));
+		return dto;
+	}
+
+	private StoreProductDiscountRule toEntity(StoreProductDiscountRuleSaveDto dto, boolean isUpdate) {
+		StoreProductDiscountRule e = new StoreProductDiscountRule();
+		e.setStoreId(dto.getStoreId());
+		e.setProductId(dto.getProductId());
+		e.setRuleName(dto.getRuleName().trim());
+		e.setDiscountType(dto.getDiscountType());
+		e.setDiscountRate(dto.getDiscountRate());
+		e.setEffectiveMode(dto.getEffectiveMode());
+		if (MODE_CUSTOM.equals(dto.getEffectiveMode())) {
+			e.setStartDate(parseDate(dto.getStartDate()));
+			e.setEndDate(parseDate(dto.getEndDate()));
+		}
+		e.setStatus(dto.getStatus());
+		return e;
+	}
+
+	private void saveSlots(Integer ruleId, List<StoreProductDiscountRuleSlotDto> slots) {
+		if (slots == null || slots.isEmpty()) return;
+		for (StoreProductDiscountRuleSlotDto s : slots) {
+			StoreProductDiscountRuleSlot entity = new StoreProductDiscountRuleSlot();
+			entity.setRuleId(ruleId);
+			entity.setWeekdayMask(s.getWeekdayMask());
+			if (StringUtils.hasText(s.getStartTime())) entity.setStartTime(LocalTime.parse(s.getStartTime(), TIME_FORMATTER));
+			if (StringUtils.hasText(s.getEndTime())) entity.setEndTime(LocalTime.parse(s.getEndTime(), TIME_FORMATTER));
+			ruleSlotMapper.insert(entity);
+		}
+	}
+
+	private String validateAndCheckConflict(StoreProductDiscountRuleSaveDto dto, Integer excludeId) {
+		// 基础
+		if (dto == null) return "参数不能为空";
+		if (dto.getStoreId() == null) return "门店ID不能为空";
+		if (dto.getProductId() == null) return "菜品ID不能为空";
+		if (!StringUtils.hasText(dto.getRuleName())) return "规则名称不能为空";
+		if (!StringUtils.hasText(dto.getDiscountType())) return "优惠类型不能为空";
+		if (!TYPE_FREE.equals(dto.getDiscountType()) && !TYPE_DISCOUNT.equals(dto.getDiscountType())) return "优惠类型不合法";
+		if (dto.getStatus() == null) return "状态不能为空";
+		if (!StringUtils.hasText(dto.getEffectiveMode())) return "生效模式不能为空";
+		if (!MODE_PERMANENT.equals(dto.getEffectiveMode()) && !MODE_CUSTOM.equals(dto.getEffectiveMode())) return "生效模式不合法";
+		if (TYPE_DISCOUNT.equals(dto.getDiscountType())) {
+			if (dto.getDiscountRate() == null) return "折扣比例不能为空";
+			if (!MODE_CUSTOM.equals(dto.getEffectiveMode())) return "折扣优惠仅支持自定义日期";
+		}
+		if (MODE_CUSTOM.equals(dto.getEffectiveMode())) {
+			if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) return "自定义日期开始/结束不能为空";
+			Date s = parseDate(dto.getStartDate());
+			Date e = parseDate(dto.getEndDate());
+			if (s.after(e)) return "开始日期不能大于结束日期";
+		}
+		if (dto.getSlots() != null) {
+			for (StoreProductDiscountRuleSlotDto s : dto.getSlots()) {
+				if ((StringUtils.hasText(s.getStartTime()) && !StringUtils.hasText(s.getEndTime()))
+						|| (!StringUtils.hasText(s.getStartTime()) && StringUtils.hasText(s.getEndTime()))) {
+					return "生效时间需成对出现";
+				}
+				if (StringUtils.hasText(s.getStartTime()) && StringUtils.hasText(s.getEndTime())) {
+					LocalTime st = LocalTime.parse(s.getStartTime(), TIME_FORMATTER);
+					LocalTime et = LocalTime.parse(s.getEndTime(), TIME_FORMATTER);
+					if (!st.isBefore(et)) return "生效开始时间必须早于结束时间";
+				}
+			}
+		}
+		// 名称唯一(同店同菜品)
+		LambdaQueryWrapper<StoreProductDiscountRule> nameQw = new LambdaQueryWrapper<>();
+		nameQw.eq(StoreProductDiscountRule::getStoreId, dto.getStoreId())
+				.eq(StoreProductDiscountRule::getProductId, dto.getProductId())
+				.eq(StoreProductDiscountRule::getRuleName, dto.getRuleName().trim());
+		if (excludeId != null) nameQw.ne(StoreProductDiscountRule::getId, excludeId);
+		if (ruleMapper.selectCount(nameQw) > 0) return "该菜品优惠名称已存在";
+
+		// 开启状态才参与冲突校验
+		if (dto.getStatus() != null && dto.getStatus() == 0) return null;
+
+		// 时间冲突校验(同店同菜品且status=1)
+		LambdaQueryWrapper<StoreProductDiscountRule> qw = new LambdaQueryWrapper<>();
+		qw.eq(StoreProductDiscountRule::getStoreId, dto.getStoreId())
+				.eq(StoreProductDiscountRule::getProductId, dto.getProductId())
+				.eq(StoreProductDiscountRule::getStatus, 1);
+		if (excludeId != null) qw.ne(StoreProductDiscountRule::getId, excludeId);
+		List<StoreProductDiscountRule> exist = ruleMapper.selectList(qw);
+		if (exist.isEmpty()) return null;
+		List<Integer> existIds = exist.stream().map(StoreProductDiscountRule::getId).collect(Collectors.toList());
+		Map<Integer, List<StoreProductDiscountRuleSlot>> slotMap = ruleSlotMapper.selectList(new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().in(StoreProductDiscountRuleSlot::getRuleId, existIds))
+				.stream().collect(Collectors.groupingBy(StoreProductDiscountRuleSlot::getRuleId));
+
+		Date newStart = MODE_CUSTOM.equals(dto.getEffectiveMode()) ? parseDate(dto.getStartDate()) : toDate(LocalDate.of(1970,1,1));
+		Date newEnd = MODE_CUSTOM.equals(dto.getEffectiveMode()) ? parseDate(dto.getEndDate()) : toDate(LocalDate.of(2099,12,31));
+
+		// 如果新规则没有slots,当作全星期/全天时间
+		List<StoreProductDiscountRuleSlotDto> newSlots = (dto.getSlots()==null || dto.getSlots().isEmpty())
+				? Collections.singletonList(allWeekAllDaySlotDto())
+				: dto.getSlots();
+
+		for (StoreProductDiscountRule old : exist) {
+			Date oldStart = MODE_CUSTOM.equals(old.getEffectiveMode()) ? old.getStartDate() : toDate(LocalDate.of(1970,1,1));
+			Date oldEnd = MODE_CUSTOM.equals(old.getEffectiveMode()) ? old.getEndDate() : toDate(LocalDate.of(2099,12,31));
+			if (!( !newStart.after(oldEnd) && !oldStart.after(newEnd) )) continue; // 日期无交集
+			List<StoreProductDiscountRuleSlot> oldSlots = slotMap.getOrDefault(old.getId(), Collections.emptyList());
+			List<StoreProductDiscountRuleSlot> oldSlotsOrAll = oldSlots.isEmpty() ? Collections.singletonList(allWeekAllDaySlot()) : oldSlots;
+			for (StoreProductDiscountRuleSlotDto ns : newSlots) {
+				int nMask = ns.getWeekdayMask() == null ? 127 : ns.getWeekdayMask();
+				LocalTime nSt = StringUtils.hasText(ns.getStartTime()) ? LocalTime.parse(ns.getStartTime(), TIME_FORMATTER) : LocalTime.MIN;
+				LocalTime nEt = StringUtils.hasText(ns.getEndTime()) ? LocalTime.parse(ns.getEndTime(), TIME_FORMATTER) : LocalTime.MAX;
+				for (StoreProductDiscountRuleSlot os : oldSlotsOrAll) {
+					int oMask = os.getWeekdayMask() == null ? 127 : os.getWeekdayMask();
+					LocalTime oSt = os.getStartTime() == null ? LocalTime.MIN : os.getStartTime();
+					LocalTime oEt = os.getEndTime() == null ? LocalTime.MAX : os.getEndTime();
+					if ( (nMask & oMask) > 0 && nSt.isBefore(oEt) && oSt.isBefore(nEt) ) {
+						if (!dto.getDiscountType().equals(old.getDiscountType())) {
+							return "该菜品在该时间段已配置其他类型优惠";
+						}
+						return "该菜品优惠时间重叠,请调整生效日期/星期/时间";
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	private StoreProductDiscountRuleSlot allWeekAllDaySlot() {
+		StoreProductDiscountRuleSlot s = new StoreProductDiscountRuleSlot();
+		s.setWeekdayMask(127);
+		s.setStartTime(null);
+		s.setEndTime(null);
+		return s;
+	}
+	private StoreProductDiscountRuleSlotDto allWeekAllDaySlotDto() {
+		StoreProductDiscountRuleSlotDto s = new StoreProductDiscountRuleSlotDto();
+		s.setWeekdayMask(127);
+		s.setStartTime(null);
+		s.setEndTime(null);
+		return s;
+	}
+
+	private String formatDate(Date d) {
+		return d == null ? null : DATE_FORMAT.format(d);
+	}
+	private String formatDateRange(Date s, Date e, String mode) {
+		if (MODE_PERMANENT.equals(mode)) return "永久生效";
+		return String.format("%s ~ %s", formatDate(s), formatDate(e));
+	}
+	private Date toDate(LocalDate localDate) {
+		return java.sql.Date.valueOf(localDate);
+	}
+	private Date parseDate(String dateStr) {
+		try {
+			return DATE_FORMAT.parse(dateStr);
+		} catch (ParseException e) {
+			throw new IllegalArgumentException("日期格式错误,必须是yyyy-MM-dd");
+		}
+	}
+}
+

+ 529 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreServiceFeeRuleServiceImpl.java

@@ -0,0 +1,529 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreServiceFeeRule;
+import shop.alien.entity.store.StoreServiceFeeRuleSlot;
+import shop.alien.entity.store.StoreServiceFeeRuleTable;
+import shop.alien.entity.store.dto.StoreServiceFeeRuleSaveDto;
+import shop.alien.entity.store.dto.StoreServiceFeeRuleSlotDto;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleDetailVo;
+import shop.alien.entity.store.vo.StoreServiceFeeRuleListVo;
+import shop.alien.mapper.StoreServiceFeeRuleMapper;
+import shop.alien.mapper.StoreServiceFeeRuleSlotMapper;
+import shop.alien.mapper.StoreServiceFeeRuleTableMapper;
+import shop.alien.store.service.StoreBookingTableService;
+import shop.alien.store.service.StoreServiceFeeRuleService;
+import shop.alien.util.common.JwtUtil;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleService {
+
+    private static final String MODE_PERMANENT = "PERMANENT";
+    private static final String MODE_CUSTOM = "CUSTOM";
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+
+    private final StoreServiceFeeRuleMapper ruleMapper;
+    private final StoreServiceFeeRuleTableMapper ruleTableMapper;
+    private final StoreServiceFeeRuleSlotMapper ruleSlotMapper;
+    private final StoreBookingTableService storeBookingTableService;
+
+    @Override
+    public R<IPage<StoreServiceFeeRuleListVo>> listRules(Integer storeId, String name, Integer status, Integer pageNum, Integer pageSize) {
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize == null || pageSize < 1 ? 10 : pageSize;
+        Page<StoreServiceFeeRule> page = new Page<>(pn, ps);
+        LambdaQueryWrapper<StoreServiceFeeRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreServiceFeeRule::getStoreId, storeId);
+        if (StringUtils.hasText(name)) {
+            wrapper.like(StoreServiceFeeRule::getFeeName, name.trim());
+        }
+        if (status != null) {
+            wrapper.eq(StoreServiceFeeRule::getStatus, status);
+        }
+        wrapper.orderByDesc(StoreServiceFeeRule::getUpdatedTime);
+        IPage<StoreServiceFeeRule> rulePage = ruleMapper.selectPage(page, wrapper);
+
+        List<Integer> ruleIds = rulePage.getRecords().stream().map(StoreServiceFeeRule::getId).collect(Collectors.toList());
+        Map<Integer, Integer> tableCountMap = buildRuleTableCountMap(ruleIds);
+        Map<Integer, List<String>> ruleTableNamesMap = buildRuleTableNamesMap(storeId, ruleIds);
+
+        List<StoreServiceFeeRuleListVo> records = rulePage.getRecords().stream().map(rule -> {
+            StoreServiceFeeRuleListVo vo = new StoreServiceFeeRuleListVo();
+            vo.setId(rule.getId());
+            vo.setStoreId(rule.getStoreId());
+            vo.setFeeName(rule.getFeeName());
+            vo.setFeeType(rule.getFeeType());
+            vo.setFeeValue(rule.getFeeValue());
+            vo.setStatus(rule.getStatus());
+            vo.setEffectiveMode(rule.getEffectiveMode());
+            vo.setUpdatedTime(rule.getUpdatedTime());
+            vo.setTableCount(tableCountMap.getOrDefault(rule.getId(), 0));
+            vo.setTableNameList(ruleTableNamesMap.getOrDefault(rule.getId(), Collections.emptyList()));
+            return vo;
+        }).collect(Collectors.toList());
+
+        Page<StoreServiceFeeRuleListVo> result = new Page<>(pn, ps, rulePage.getTotal());
+        result.setRecords(records);
+        return R.data(result);
+    }
+
+    @Override
+    public R<StoreServiceFeeRuleDetailVo> getRuleDetail(Integer id) {
+        if (id == null) {
+            return R.fail("规则ID不能为空");
+        }
+        StoreServiceFeeRule rule = ruleMapper.selectById(id);
+        if (rule == null) {
+            return R.fail("服务费规则不存在");
+        }
+
+        StoreServiceFeeRuleDetailVo vo = new StoreServiceFeeRuleDetailVo();
+        vo.setId(rule.getId());
+        vo.setStoreId(rule.getStoreId());
+        vo.setFeeName(rule.getFeeName());
+        vo.setFeeType(rule.getFeeType());
+        vo.setFeeValue(rule.getFeeValue());
+        vo.setStatus(rule.getStatus());
+        vo.setEffectiveMode(rule.getEffectiveMode());
+        vo.setStartDate(formatDate(rule.getStartDate()));
+        vo.setEndDate(formatDate(rule.getEndDate()));
+
+        List<StoreServiceFeeRuleTable> tableList = ruleTableMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleTable>().eq(StoreServiceFeeRuleTable::getRuleId, id));
+        vo.setTableIds(tableList.stream().map(StoreServiceFeeRuleTable::getTableId).collect(Collectors.toList()));
+
+        List<StoreServiceFeeRuleSlot> slotList = ruleSlotMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleSlot>().eq(StoreServiceFeeRuleSlot::getRuleId, id));
+        vo.setSlots(slotList.stream().map(this::toSlotDto).collect(Collectors.toList()));
+        log.info("StoreServiceFeeRuleServiceImpl.getRuleDetail?接口出参vo={}", vo);
+        return R.data(vo);
+    }
+
+    @Override
+    public R<Integer> createRule(StoreServiceFeeRuleSaveDto dto) {
+        log.info("StoreServiceFeeRuleServiceImpl.createRule?dto={}", dto);
+        String checkMsg = validateAndCheckConflict(dto, null);
+        if (checkMsg != null) {
+            return R.fail(checkMsg);
+        }
+        Integer userId = getCurrentUserId();
+        StoreServiceFeeRule rule = new StoreServiceFeeRule();
+        fillRuleFromDto(rule, dto, userId, false);
+        ruleMapper.insert(rule);
+        saveChildren(rule.getId(), dto, userId);
+        return R.data(rule.getId());
+    }
+
+    @Override
+    public R<Boolean> updateRule(StoreServiceFeeRuleSaveDto dto) {
+        if (dto == null || dto.getId() == null) {
+            return R.fail("规则ID不能为空");
+        }
+        StoreServiceFeeRule existing = ruleMapper.selectById(dto.getId());
+        if (existing == null) {
+            return R.fail("服务费规则不存在");
+        }
+        String checkMsg = validateAndCheckConflict(dto, dto.getId());
+        if (checkMsg != null) {
+            return R.fail(checkMsg);
+        }
+
+        Integer userId = getCurrentUserId();
+        StoreServiceFeeRule updateRule = new StoreServiceFeeRule();
+        updateRule.setId(dto.getId());
+        fillRuleFromDto(updateRule, dto, userId, true);
+        ruleMapper.updateById(updateRule);
+
+        ruleTableMapper.delete(new LambdaQueryWrapper<StoreServiceFeeRuleTable>().eq(StoreServiceFeeRuleTable::getRuleId, dto.getId()));
+        ruleSlotMapper.delete(new LambdaQueryWrapper<StoreServiceFeeRuleSlot>().eq(StoreServiceFeeRuleSlot::getRuleId, dto.getId()));
+        saveChildren(dto.getId(), dto, userId);
+        return R.data(true);
+    }
+
+    @Override
+    public R<Boolean> deleteRule(Integer id) {
+        if (id == null) {
+            return R.fail("规则ID不能为空");
+        }
+        StoreServiceFeeRule existing = ruleMapper.selectById(id);
+        if (existing == null) {
+            return R.fail("服务费规则不存在");
+        }
+        ruleMapper.deleteById(id);
+        ruleTableMapper.delete(new LambdaQueryWrapper<StoreServiceFeeRuleTable>().eq(StoreServiceFeeRuleTable::getRuleId, id));
+        ruleSlotMapper.delete(new LambdaQueryWrapper<StoreServiceFeeRuleSlot>().eq(StoreServiceFeeRuleSlot::getRuleId, id));
+        return R.data(true);
+    }
+
+    @Override
+    public R<List<StoreBookingTableVo>> getTableList(Integer storeId, Integer categoryId) {
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        List<StoreBookingTableVo> list = storeBookingTableService.getTableListWithCategoryName(storeId, categoryId);
+        return R.data(list);
+    }
+
+    @Override
+    public int autoCloseExpiredCustomRules() {
+        LocalDate today = LocalDate.now();
+        LocalDateTime now = LocalDateTime.now();
+        List<StoreServiceFeeRule> candidates = ruleMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRule>()
+                        .eq(StoreServiceFeeRule::getStatus, 1)
+                        .eq(StoreServiceFeeRule::getEffectiveMode, MODE_CUSTOM)
+                        .isNotNull(StoreServiceFeeRule::getEndDate)
+                        .le(StoreServiceFeeRule::getEndDate, java.sql.Date.valueOf(today)));
+        if (candidates == null || candidates.isEmpty()) {
+            return 0;
+        }
+        List<Integer> ruleIds = candidates.stream().map(StoreServiceFeeRule::getId).collect(Collectors.toList());
+        List<StoreServiceFeeRuleSlot> allSlots = ruleSlotMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleSlot>().in(StoreServiceFeeRuleSlot::getRuleId, ruleIds));
+        Map<Integer, List<StoreServiceFeeRuleSlot>> slotMap = allSlots.stream()
+                .collect(Collectors.groupingBy(StoreServiceFeeRuleSlot::getRuleId));
+
+        int closed = 0;
+        for (StoreServiceFeeRule rule : candidates) {
+            LocalDate endDate = new java.sql.Date(rule.getEndDate().getTime()).toLocalDate();
+            LocalTime maxEndTime = slotMap.getOrDefault(rule.getId(), Collections.emptyList())
+                    .stream()
+                    .map(StoreServiceFeeRuleSlot::getEndTime)
+                    .filter(t -> t != null)
+                    .max(LocalTime::compareTo)
+                    .orElse(LocalTime.of(23, 59, 59));
+            LocalDateTime expireAt = LocalDateTime.of(endDate, maxEndTime);
+            if (!now.isBefore(expireAt)) {
+                StoreServiceFeeRule update = new StoreServiceFeeRule();
+                update.setId(rule.getId());
+                update.setStatus(0);
+                update.setUpdatedUserId(0);
+                if (ruleMapper.updateById(update) > 0) {
+                    closed++;
+                }
+            }
+        }
+        if (closed > 0) {
+            log.info("自动关闭过期服务费规则完成,本次关闭数量={}", closed);
+        }
+        return closed;
+    }
+
+    private Map<Integer, Integer> buildRuleTableCountMap(List<Integer> ruleIds) {
+        if (ruleIds == null || ruleIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<StoreServiceFeeRuleTable> tableList = ruleTableMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleTable>().in(StoreServiceFeeRuleTable::getRuleId, ruleIds));
+        Map<Integer, Set<Integer>> temp = new HashMap<>();
+        for (StoreServiceFeeRuleTable row : tableList) {
+            temp.computeIfAbsent(row.getRuleId(), k -> new HashSet<>()).add(row.getTableId());
+        }
+        Map<Integer, Integer> countMap = new HashMap<>();
+        temp.forEach((k, v) -> countMap.put(k, v.size()));
+        return countMap;
+    }
+
+    private Map<Integer, List<String>> buildRuleTableNamesMap(Integer storeId, List<Integer> ruleIds) {
+        if (ruleIds == null || ruleIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<StoreServiceFeeRuleTable> relations = ruleTableMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleTable>().in(StoreServiceFeeRuleTable::getRuleId, ruleIds));
+        if (relations == null || relations.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<Integer, String> tableIdNameMap = storeBookingTableService.getTableListWithCategoryName(storeId, null).stream()
+                .collect(Collectors.toMap(StoreBookingTableVo::getId, StoreBookingTableVo::getTableNumber, (a, b) -> a));
+
+        Map<Integer, List<String>> groupedNames = new HashMap<>();
+        for (StoreServiceFeeRuleTable relation : relations) {
+            String tableNumber = tableIdNameMap.get(relation.getTableId());
+            if (!StringUtils.hasText(tableNumber)) {
+                continue;
+            }
+            groupedNames.computeIfAbsent(relation.getRuleId(), k -> new ArrayList<>()).add(tableNumber);
+        }
+        Map<Integer, List<String>> result = new HashMap<>();
+        groupedNames.forEach((ruleId, names) -> result.put(ruleId, names.stream().distinct().collect(Collectors.toList())));
+        return result;
+    }
+
+    private void fillRuleFromDto(StoreServiceFeeRule rule, StoreServiceFeeRuleSaveDto dto, Integer userId, boolean isUpdate) {
+        rule.setStoreId(dto.getStoreId());
+        rule.setFeeName(dto.getFeeName().trim());
+        rule.setFeeType(dto.getFeeType());
+        rule.setFeeValue(dto.getFeeValue());
+        rule.setStatus(dto.getStatus());
+        rule.setEffectiveMode(dto.getEffectiveMode());
+        if (MODE_CUSTOM.equals(dto.getEffectiveMode())) {
+            rule.setStartDate(parseDate(dto.getStartDate()));
+            rule.setEndDate(parseDate(dto.getEndDate()));
+        } else {
+            rule.setStartDate(null);
+            rule.setEndDate(null);
+        }
+        if (isUpdate) {
+            rule.setUpdatedUserId(userId);
+        } else {
+            rule.setCreatedUserId(userId);
+        }
+    }
+
+    private void saveChildren(Integer ruleId, StoreServiceFeeRuleSaveDto dto, Integer userId) {
+        for (Integer tableId : dto.getTableIds()) {
+            StoreServiceFeeRuleTable row = new StoreServiceFeeRuleTable();
+            row.setRuleId(ruleId);
+            row.setStoreId(dto.getStoreId());
+            row.setTableId(tableId);
+            row.setCreatedUserId(userId);
+            row.setUpdatedUserId(userId);
+            ruleTableMapper.insert(row);
+        }
+
+        for (StoreServiceFeeRuleSlotDto slotDto : dto.getSlots()) {
+            StoreServiceFeeRuleSlot slot = new StoreServiceFeeRuleSlot();
+            slot.setRuleId(ruleId);
+            slot.setWeekdayMask(slotDto.getWeekdayMask());
+            slot.setStartTime(LocalTime.parse(slotDto.getStartTime(), TIME_FORMATTER));
+            slot.setEndTime(LocalTime.parse(slotDto.getEndTime(), TIME_FORMATTER));
+            slot.setCreatedUserId(userId);
+            slot.setUpdatedUserId(userId);
+            ruleSlotMapper.insert(slot);
+        }
+    }
+
+    private String validateAndCheckConflict(StoreServiceFeeRuleSaveDto dto, Integer excludeRuleId) {
+        String basicMsg = validateBasic(dto);
+        if (basicMsg != null) {
+            return basicMsg;
+        }
+
+        // 名称校验(同店不可重复)
+        LambdaQueryWrapper<StoreServiceFeeRule> nameWrapper = new LambdaQueryWrapper<>();
+        nameWrapper.eq(StoreServiceFeeRule::getStoreId, dto.getStoreId())
+                .eq(StoreServiceFeeRule::getFeeName, dto.getFeeName().trim());
+        if (excludeRuleId != null) {
+            nameWrapper.ne(StoreServiceFeeRule::getId, excludeRuleId);
+        }
+        if (ruleMapper.selectCount(nameWrapper) > 0) {
+            return "服务费名称已存在";
+        }
+
+        // 状态为关闭时,仅做名称校验即可(不参与冲突检查)
+        if (dto.getStatus() != null && dto.getStatus() == 0) {
+            return null;
+        }
+
+        return checkTimeConflict(dto, excludeRuleId);
+    }
+
+    private String validateBasic(StoreServiceFeeRuleSaveDto dto) {
+        if (dto == null) {
+            return "请求参数不能为空";
+        }
+        if (dto.getStoreId() == null) {
+            return "门店ID不能为空";
+        }
+        if (!StringUtils.hasText(dto.getFeeName())) {
+            return "服务费名称不能为空";
+        }
+        if (dto.getFeeType() == null) {
+            return "服务费类型不能为空";
+        }
+        if (dto.getFeeValue() == null) {
+            return "服务费金额不能为空";
+        }
+        if (dto.getStatus() == null) {
+            return "服务费状态不能为空";
+        }
+        if (!StringUtils.hasText(dto.getEffectiveMode())) {
+            return "生效模式不能为空";
+        }
+        if (!MODE_PERMANENT.equals(dto.getEffectiveMode()) && !MODE_CUSTOM.equals(dto.getEffectiveMode())) {
+            return "生效模式不合法";
+        }
+        if (dto.getTableIds() == null || dto.getTableIds().isEmpty()) {
+            return "适用桌台不能为空";
+        }
+        if (dto.getSlots() == null || dto.getSlots().isEmpty()) {
+            return "生效时段不能为空";
+        }
+        if (MODE_CUSTOM.equals(dto.getEffectiveMode())) {
+            if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) {
+                return "自定义日期模式下开始/结束日期不能为空";
+            }
+            Date start = parseDate(dto.getStartDate());
+            Date end = parseDate(dto.getEndDate());
+            if (start.after(end)) {
+                return "开始日期不能大于结束日期";
+            }
+        }
+        for (StoreServiceFeeRuleSlotDto slot : dto.getSlots()) {
+            if (slot.getWeekdayMask() == null || slot.getWeekdayMask() <= 0) {
+                return "生效星期不能为空";
+            }
+            if (!StringUtils.hasText(slot.getStartTime()) || !StringUtils.hasText(slot.getEndTime())) {
+                return "生效时间不能为空";
+            }
+            LocalTime st = LocalTime.parse(slot.getStartTime(), TIME_FORMATTER);
+            LocalTime et = LocalTime.parse(slot.getEndTime(), TIME_FORMATTER);
+            if (!st.isBefore(et)) {
+                return "生效开始时间必须早于结束时间";
+            }
+        }
+        return null;
+    }
+
+    private String checkTimeConflict(StoreServiceFeeRuleSaveDto dto, Integer excludeRuleId) {
+        List<Integer> tableIds = dto.getTableIds().stream().distinct().collect(Collectors.toList());
+        // 找到这些桌台对应的已启用规则
+        LambdaQueryWrapper<StoreServiceFeeRuleTable> tableWrapper = new LambdaQueryWrapper<>();
+        tableWrapper.eq(StoreServiceFeeRuleTable::getStoreId, dto.getStoreId())
+                .in(StoreServiceFeeRuleTable::getTableId, tableIds);
+        List<StoreServiceFeeRuleTable> relationList = ruleTableMapper.selectList(tableWrapper);
+        if (relationList.isEmpty()) {
+            return null;
+        }
+
+        Map<Integer, Set<Integer>> ruleTableMap = new HashMap<>();
+        for (StoreServiceFeeRuleTable rt : relationList) {
+            ruleTableMap.computeIfAbsent(rt.getRuleId(), k -> new HashSet<>()).add(rt.getTableId());
+        }
+
+        List<Integer> candidateRuleIds = new ArrayList<>(ruleTableMap.keySet());
+        LambdaQueryWrapper<StoreServiceFeeRule> ruleWrapper = new LambdaQueryWrapper<>();
+        ruleWrapper.in(StoreServiceFeeRule::getId, candidateRuleIds)
+                .eq(StoreServiceFeeRule::getStoreId, dto.getStoreId())
+                .eq(StoreServiceFeeRule::getStatus, 1);
+        if (excludeRuleId != null) {
+            ruleWrapper.ne(StoreServiceFeeRule::getId, excludeRuleId);
+        }
+        List<StoreServiceFeeRule> existRules = ruleMapper.selectList(ruleWrapper);
+        if (existRules.isEmpty()) {
+            return null;
+        }
+        List<Integer> validRuleIds = existRules.stream().map(StoreServiceFeeRule::getId).collect(Collectors.toList());
+        List<StoreServiceFeeRuleSlot> allExistSlots = ruleSlotMapper.selectList(
+                new LambdaQueryWrapper<StoreServiceFeeRuleSlot>().in(StoreServiceFeeRuleSlot::getRuleId, validRuleIds));
+        Map<Integer, List<StoreServiceFeeRuleSlot>> slotMap = allExistSlots.stream()
+                .collect(Collectors.groupingBy(StoreServiceFeeRuleSlot::getRuleId));
+
+        Date newStartDate = MODE_CUSTOM.equals(dto.getEffectiveMode()) ? parseDate(dto.getStartDate()) : toDate(LocalDate.of(1970, 1, 1));
+        Date newEndDate = MODE_CUSTOM.equals(dto.getEffectiveMode()) ? parseDate(dto.getEndDate()) : toDate(LocalDate.of(2099, 12, 31));
+        List<StoreServiceFeeRuleSlotDto> newSlots = dto.getSlots();
+
+        for (StoreServiceFeeRule existRule : existRules) {
+            Date oldStart = MODE_CUSTOM.equals(existRule.getEffectiveMode()) ? existRule.getStartDate() : toDate(LocalDate.of(1970, 1, 1));
+            Date oldEnd = MODE_CUSTOM.equals(existRule.getEffectiveMode()) ? existRule.getEndDate() : toDate(LocalDate.of(2099, 12, 31));
+            if (!isDateOverlap(newStartDate, newEndDate, oldStart, oldEnd)) {
+                continue;
+            }
+            List<StoreServiceFeeRuleSlot> oldSlots = slotMap.getOrDefault(existRule.getId(), Collections.emptyList());
+            if (oldSlots.isEmpty()) {
+                continue;
+            }
+            for (StoreServiceFeeRuleSlotDto ns : newSlots) {
+                LocalTime nsStart = LocalTime.parse(ns.getStartTime(), TIME_FORMATTER);
+                LocalTime nsEnd = LocalTime.parse(ns.getEndTime(), TIME_FORMATTER);
+                for (StoreServiceFeeRuleSlot os : oldSlots) {
+                    if ((ns.getWeekdayMask() & os.getWeekdayMask()) <= 0) {
+                        continue;
+                    }
+                    if (isTimeOverlap(nsStart, nsEnd, os.getStartTime(), os.getEndTime())) {
+                        Set<Integer> overlapTables = new HashSet<>(ruleTableMap.getOrDefault(existRule.getId(), Collections.emptySet()));
+                        overlapTables.retainAll(new HashSet<>(tableIds));
+                        if (!overlapTables.isEmpty()) {
+                            String tableTip = overlapTables.stream().sorted(Comparator.naturalOrder()).map(String::valueOf).collect(Collectors.joining(","));
+                            if (!dto.getFeeType().equals(existRule.getFeeType())) {
+                                return "桌台[" + tableTip + "]在该时间段已配置其他类型服务费";
+                            }
+                            return "桌台[" + tableTip + "]服务费时间重叠,请调整生效日期/星期/时间";
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private boolean isDateOverlap(Date s1, Date e1, Date s2, Date e2) {
+        return !s1.after(e2) && !s2.after(e1);
+    }
+
+    private boolean isTimeOverlap(LocalTime s1, LocalTime e1, LocalTime s2, LocalTime e2) {
+        return s1.isBefore(e2) && s2.isBefore(e1);
+    }
+
+    private Date parseDate(String dateStr) {
+        try {
+            return DATE_FORMAT.parse(dateStr);
+        } catch (ParseException e) {
+            throw new IllegalArgumentException("日期格式错误,必须是yyyy-MM-dd");
+        }
+    }
+
+    private String formatDate(Date date) {
+        return date == null ? null : DATE_FORMAT.format(date);
+    }
+
+    private Date toDate(LocalDate localDate) {
+        return java.sql.Date.valueOf(localDate);
+    }
+
+    private StoreServiceFeeRuleSlotDto toSlotDto(StoreServiceFeeRuleSlot slot) {
+        StoreServiceFeeRuleSlotDto dto = new StoreServiceFeeRuleSlotDto();
+        dto.setWeekdayMask(slot.getWeekdayMask());
+        dto.setStartTime(slot.getStartTime() != null ? slot.getStartTime().format(TIME_FORMATTER) : null);
+        dto.setEndTime(slot.getEndTime() != null ? slot.getEndTime().format(TIME_FORMATTER) : null);
+        return dto;
+    }
+
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}
+