فهرست منبع

Merge remote-tracking branch 'origin/sit-new-checkstand' into sit-new-checkstand

刘云鑫 2 هفته پیش
والد
کامیت
3c4ce2c3c7

+ 17 - 1
alien-entity/src/main/java/shop/alien/entity/store/StorePrice.java

@@ -35,10 +35,26 @@ public class StorePrice {
     @TableField("name")
     private String name;
 
-    @ApiModelProperty(value = "分类ids(JSON数组,如:[1,2,3])")
+    @ApiModelProperty(value = "菜品分类id(varchar500,如 JSON 数组:[1,2,3])")
     @TableField("category_ids")
     private String categoryIds;
 
+    @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+    @TableField("is_homepage_display")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty(value = "标签(JSON数组,如:[\"XXX\",\"推荐\"])")
+    @TableField("tags")
+    private String tags;
+
+    @ApiModelProperty(value = "短评")
+    @TableField("dish_review")
+    private String dishReview;
+
+    @ApiModelProperty(value = "描述")
+    @TableField("description")
+    private String description;
+
     @ApiModelProperty(value = "总价")
     @TableField("total_price")
     private BigDecimal totalPrice;

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

@@ -7,6 +7,7 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.math.BigDecimal;
+import java.time.LocalTime;
 import java.util.Date;
 
 @Data
@@ -72,5 +73,17 @@ public class StoreProductDiscountRule {
 
 	@TableField("updated_user_id")
 	private Integer updatedUserId;
+
+	@ApiModelProperty("星期掩码,按位表示周一到周日")
+	@TableField("weekday_mask")
+	private Integer weekdayMask;
+
+	@ApiModelProperty("开始时间")
+	@TableField("start_time")
+	private LocalTime startTime;
+
+	@ApiModelProperty("结束时间")
+	@TableField("end_time")
+	private LocalTime endTime;
 }
 

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

@@ -12,6 +12,7 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.math.BigDecimal;
+import java.time.LocalTime;
 import java.util.Date;
 
 /**
@@ -59,6 +60,22 @@ public class StoreServiceFeeRule {
     @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
     private Date endDate;
 
+    @ApiModelProperty("桌台ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty("星期掩码,按位表示周一到周日(示例:周一+周三 = 5)")
+    @TableField("weekday_mask")
+    private Integer weekdayMask;
+
+    @ApiModelProperty("开始时间")
+    @TableField("start_time")
+    private LocalTime startTime;
+
+    @ApiModelProperty("结束时间")
+    @TableField("end_time")
+    private LocalTime endTime;
+
     @TableLogic
     @TableField("delete_flag")
     private Integer deleteFlag;

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PriceListVo.java

@@ -73,6 +73,18 @@ public class PriceListVo {
     @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
     private String categoryIds;
 
+    @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty(value = "标签(JSON数组)")
+    private String tags;
+
+    @ApiModelProperty(value = "短评")
+    private String dishReview;
+
+    @ApiModelProperty(value = "描述")
+    private String description;
+
     @ApiModelProperty(value = "菜品分类名称(JSON数组,如:[\"热菜\",\"凉菜\"])")
     private String categoryNames;
 

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

@@ -24,5 +24,8 @@ public class StoreProductDiscountRuleDetailVo {
 	private String endDate;
 	private Integer status;
 	private List<StoreProductDiscountRuleSlotDto> slots;
+
+	@ApiModelProperty("菜品原价")
+	private java.math.BigDecimal originalPrice;
 }
 

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

@@ -24,6 +24,9 @@ public class StoreProductDiscountRuleListVo {
 	@ApiModelProperty("菜品名称")
 	private String productName;
 
+	@ApiModelProperty("菜品图片URL")
+	private String productImageUrl;
+
 	@ApiModelProperty("规则名称")
 	private String ruleName;
 

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreProductSelectVo.java

@@ -37,5 +37,17 @@ public class StoreProductSelectVo {
 
     @ApiModelProperty("分类名称列表")
     private List<String> categoryNames;
+
+    @ApiModelProperty("首页展示(0:否, 1:是)")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty("标签(JSON数组)")
+    private String tags;
+
+    @ApiModelProperty("短评")
+    private String dishReview;
+
+    @ApiModelProperty("描述")
+    private String description;
 }
 

+ 1 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java

@@ -569,6 +569,7 @@ public class StoreCuisineController {
                     PriceListVo vo = new PriceListVo();
 
                     BeanUtils.copyProperties(storePrice, vo);
+                    vo.setCategoryNames(storeCuisineService.getCategoryNames(storePrice.getCategoryIds()));
                     priceListVo.add(vo);
                 }
             }

+ 18 - 0
alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java

@@ -162,6 +162,15 @@ public class StorePriceController {
             if (StringUtils.isNotEmpty(storePrice.getUsageRule())) {
                 textContent.append(storePrice.getUsageRule()).append(" ");
             }
+            if (StringUtils.isNotEmpty(storePrice.getDescription())) {
+                textContent.append(storePrice.getDescription()).append(" ");
+            }
+            if (StringUtils.isNotEmpty(storePrice.getDishReview())) {
+                textContent.append(storePrice.getDishReview()).append(" ");
+            }
+            if (StringUtils.isNotEmpty(storePrice.getTags())) {
+                textContent.append(storePrice.getTags()).append(" ");
+            }
 
             List<String> imageUrls = new ArrayList<>();
 
@@ -291,6 +300,15 @@ public class StorePriceController {
             if (StringUtils.isNotEmpty(storePrice.getUsageRule())) {
                 textContent.append(storePrice.getUsageRule()).append(" ");
             }
+            if (StringUtils.isNotEmpty(storePrice.getDescription())) {
+                textContent.append(storePrice.getDescription()).append(" ");
+            }
+            if (StringUtils.isNotEmpty(storePrice.getDishReview())) {
+                textContent.append(storePrice.getDishReview()).append(" ");
+            }
+            if (StringUtils.isNotEmpty(storePrice.getTags())) {
+                textContent.append(storePrice.getTags()).append(" ");
+            }
 
             List<String> imageUrls = new ArrayList<>();
 

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

@@ -162,6 +162,10 @@ public class StoreProductDiscountController {
 		List<Integer> categoryIdList = parseCategoryIds(item.getCategoryIds());
 		vo.setCategoryIdList(categoryIdList);
 		vo.setCategoryNames(categoryIdList.stream().map(categoryNameMap::get).filter(StringUtils::hasText).collect(Collectors.toList()));
+		vo.setIsHomepageDisplay(item.getIsHomepageDisplay());
+		vo.setTags(item.getTags());
+		vo.setDishReview(item.getDishReview());
+		vo.setDescription(item.getDescription());
 		return vo;
 	}
 

+ 152 - 93
alien-store/src/main/java/shop/alien/store/service/impl/StoreProductDiscountServiceImpl.java

@@ -15,8 +15,10 @@ 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 shop.alien.store.service.StoreCuisineService;
+import shop.alien.store.service.StorePriceService;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -41,7 +43,8 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
 
 	private final StoreProductDiscountRuleMapper ruleMapper;
-	private final StoreProductDiscountRuleSlotMapper ruleSlotMapper;
+	private final StoreCuisineService storeCuisineService;
+	private final StorePriceService storePriceService;
 
 	@Override
 	public R<IPage<StoreProductDiscountRuleListVo>> listRules(Integer storeId, Integer productId, String ruleName, Integer status, Integer pageNum, Integer pageSize) {
@@ -65,27 +68,33 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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()));
+		Map<Integer, ProductInfo> productInfoMap = buildProductInfoMap(storeId, rulePage.getRecords().stream().map(StoreProductDiscountRule::getProductId).collect(Collectors.toList()));
 
-		List<StoreProductDiscountRuleListVo> records = rulePage.getRecords().stream().map(rule -> {
+		// 单表:按“商品+规则头”分组聚合,slotCount 为该组的行数
+		Map<String, List<StoreProductDiscountRule>> grouped = rulePage.getRecords().stream()
+				.collect(Collectors.groupingBy(this::groupKey));
+		List<StoreProductDiscountRuleListVo> records = new ArrayList<>();
+		for (List<StoreProductDiscountRule> group : grouped.values()) {
+			StoreProductDiscountRule head = group.get(0);
 			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());
+			vo.setId(group.stream().map(StoreProductDiscountRule::getId).min(Integer::compareTo).orElse(head.getId()));
+			vo.setStoreId(head.getStoreId());
+			vo.setProductId(head.getProductId());
+			ProductInfo info = productInfoMap.get(head.getProductId());
+			vo.setProductName(info == null ? null : info.name);
+			vo.setProductImageUrl(info == null ? null : info.imageUrl);
+			vo.setRuleName(head.getRuleName());
+			vo.setDiscountType(head.getDiscountType());
+			vo.setDiscountRate(head.getDiscountRate());
+			vo.setEffectiveMode(head.getEffectiveMode());
+			vo.setDateRange(formatDateRange(head.getStartDate(), head.getEndDate(), head.getEffectiveMode()));
+			vo.setSlotCount(group.size());
+			vo.setStatus(head.getStatus());
+			vo.setUpdatedTime(group.stream().map(StoreProductDiscountRule::getUpdatedTime).filter(Objects::nonNull).max(Date::compareTo).orElse(head.getUpdatedTime()));
+			records.add(vo);
+		}
 
-		Page<StoreProductDiscountRuleListVo> result = new Page<>(pn, ps, rulePage.getTotal());
+		Page<StoreProductDiscountRuleListVo> result = new Page<>(pn, ps, grouped.size());
 		result.setRecords(records);
 		return R.data(result);
 	}
@@ -103,7 +112,9 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		vo.setId(rule.getId());
 		vo.setStoreId(rule.getStoreId());
 		vo.setProductId(rule.getProductId());
-		vo.setProductName(buildProductNameMap(rule.getStoreId()).get(rule.getProductId()));
+		ProductInfo info = getProductInfo(rule.getStoreId(), rule.getProductId());
+		vo.setProductName(info == null ? null : info.name);
+		vo.setOriginalPrice(info == null ? null : info.price);
 		vo.setRuleName(rule.getRuleName());
 		vo.setDiscountType(rule.getDiscountType());
 		vo.setDiscountRate(rule.getDiscountRate());
@@ -111,8 +122,24 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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()));
+		// 同组(商品+规则头)聚合同桌台下的时段(来自主表行)
+		LambdaQueryWrapper<StoreProductDiscountRule> groupQw = new LambdaQueryWrapper<StoreProductDiscountRule>()
+				.eq(StoreProductDiscountRule::getStoreId, rule.getStoreId())
+				.eq(StoreProductDiscountRule::getProductId, rule.getProductId())
+				.eq(StoreProductDiscountRule::getRuleName, rule.getRuleName())
+				.eq(StoreProductDiscountRule::getDiscountType, rule.getDiscountType())
+				.eq(StoreProductDiscountRule::getDiscountRate, rule.getDiscountRate())
+				.eq(StoreProductDiscountRule::getEffectiveMode, rule.getEffectiveMode());
+		if (rule.getStartDate() == null) groupQw.isNull(StoreProductDiscountRule::getStartDate); else groupQw.eq(StoreProductDiscountRule::getStartDate, rule.getStartDate());
+		if (rule.getEndDate() == null) groupQw.isNull(StoreProductDiscountRule::getEndDate); else groupQw.eq(StoreProductDiscountRule::getEndDate, rule.getEndDate());
+		List<StoreProductDiscountRule> sameGroup = ruleMapper.selectList(groupQw);
+		vo.setSlots(sameGroup.stream().map(r -> {
+			StoreProductDiscountRuleSlotDto s = new StoreProductDiscountRuleSlotDto();
+			s.setWeekdayMask(r.getWeekdayMask());
+			s.setStartTime(r.getStartTime() == null ? null : r.getStartTime().format(TIME_FORMATTER));
+			s.setEndTime(r.getEndTime() == null ? null : r.getEndTime().format(TIME_FORMATTER));
+			return s;
+		}).collect(Collectors.toList()));
 		return R.data(vo);
 	}
 
@@ -120,10 +147,20 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 	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());
+		// 单表:按时段展开写入多行(无时段则按“全星期/全天”写入一行)
+		Integer firstId = null;
+		List<StoreProductDiscountRuleSlotDto> slots = (dto.getSlots()==null || dto.getSlots().isEmpty())
+				? Collections.singletonList(allWeekAllDaySlotDto())
+				: dto.getSlots();
+		for (StoreProductDiscountRuleSlotDto s : slots) {
+			StoreProductDiscountRule e = toEntity(dto, false);
+			e.setWeekdayMask(s.getWeekdayMask());
+			if (StringUtils.hasText(s.getStartTime())) e.setStartTime(LocalTime.parse(s.getStartTime(), TIME_FORMATTER));
+			if (StringUtils.hasText(s.getEndTime())) e.setEndTime(LocalTime.parse(s.getEndTime(), TIME_FORMATTER));
+			ruleMapper.insert(e);
+			if (firstId == null) firstId = e.getId();
+		}
+		return R.data(firstId);
 	}
 
 	@Override
@@ -132,20 +169,48 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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());
+		// 找到旧记录的“规则头”,删除该组所有行,再按新入参重建
+		StoreProductDiscountRule existing = ruleMapper.selectById(dto.getId());
+		LambdaQueryWrapper<StoreProductDiscountRule> delQw = new LambdaQueryWrapper<StoreProductDiscountRule>()
+				.eq(StoreProductDiscountRule::getStoreId, existing.getStoreId())
+				.eq(StoreProductDiscountRule::getProductId, existing.getProductId())
+				.eq(StoreProductDiscountRule::getRuleName, existing.getRuleName())
+				.eq(StoreProductDiscountRule::getDiscountType, existing.getDiscountType())
+				.eq(StoreProductDiscountRule::getDiscountRate, existing.getDiscountRate())
+				.eq(StoreProductDiscountRule::getEffectiveMode, existing.getEffectiveMode());
+		if (existing.getStartDate() == null) delQw.isNull(StoreProductDiscountRule::getStartDate); else delQw.eq(StoreProductDiscountRule::getStartDate, existing.getStartDate());
+		if (existing.getEndDate() == null) delQw.isNull(StoreProductDiscountRule::getEndDate); else delQw.eq(StoreProductDiscountRule::getEndDate, existing.getEndDate());
+		ruleMapper.delete(delQw);
+		// 无时段则按“全星期/全天”写入一行
+		List<StoreProductDiscountRuleSlotDto> slots = (dto.getSlots()==null || dto.getSlots().isEmpty())
+				? Collections.singletonList(allWeekAllDaySlotDto())
+				: dto.getSlots();
+		for (StoreProductDiscountRuleSlotDto s : slots) {
+			StoreProductDiscountRule e = toEntity(dto, true);
+			e.setWeekdayMask(s.getWeekdayMask());
+			if (StringUtils.hasText(s.getStartTime())) e.setStartTime(LocalTime.parse(s.getStartTime(), TIME_FORMATTER));
+			if (StringUtils.hasText(s.getEndTime())) e.setEndTime(LocalTime.parse(s.getEndTime(), TIME_FORMATTER));
+			ruleMapper.insert(e);
+		}
 		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));
+		StoreProductDiscountRule existing = ruleMapper.selectById(id);
+		if (existing == null) return R.fail("规则不存在");
+		// 删除同组所有行
+		LambdaQueryWrapper<StoreProductDiscountRule> delQw = new LambdaQueryWrapper<StoreProductDiscountRule>()
+				.eq(StoreProductDiscountRule::getStoreId, existing.getStoreId())
+				.eq(StoreProductDiscountRule::getProductId, existing.getProductId())
+				.eq(StoreProductDiscountRule::getRuleName, existing.getRuleName())
+				.eq(StoreProductDiscountRule::getDiscountType, existing.getDiscountType())
+				.eq(StoreProductDiscountRule::getDiscountRate, existing.getDiscountRate())
+				.eq(StoreProductDiscountRule::getEffectiveMode, existing.getEffectiveMode());
+		if (existing.getStartDate() == null) delQw.isNull(StoreProductDiscountRule::getStartDate); else delQw.eq(StoreProductDiscountRule::getStartDate, existing.getStartDate());
+		if (existing.getEndDate() == null) delQw.isNull(StoreProductDiscountRule::getEndDate); else delQw.eq(StoreProductDiscountRule::getEndDate, existing.getEndDate());
+		ruleMapper.delete(delQw);
 		return R.data(true);
 	}
 
@@ -172,20 +237,13 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		if (candidates == null || candidates.isEmpty()) {
 			return 0;
 		}
-		List<Integer> ruleIds = candidates.stream().map(StoreProductDiscountRule::getId).collect(Collectors.toList());
-		Map<Integer, List<StoreProductDiscountRuleSlot>> slotMap = ruleSlotMapper.selectList(
-				new LambdaQueryWrapper<StoreProductDiscountRuleSlot>().in(StoreProductDiscountRuleSlot::getRuleId, ruleIds))
-				.stream().collect(Collectors.groupingBy(StoreProductDiscountRuleSlot::getRuleId));
 		int closed = 0;
 		for (StoreProductDiscountRule rule : candidates) {
 			LocalDate endDate = new java.sql.Date(rule.getEndDate().getTime()).toLocalDate();
-			LocalTime maxEnd = slotMap.getOrDefault(rule.getId(), Collections.emptyList()).stream()
-					.map(StoreProductDiscountRuleSlot::getEndTime)
-					.filter(Objects::nonNull)
-					.max(LocalTime::compareTo)
-					.orElse(LocalTime.of(23, 59, 59));
+			LocalTime maxEnd = rule.getEndTime() != null ? rule.getEndTime() : LocalTime.of(23, 59, 59);
 			LocalDateTime expireAt = LocalDateTime.of(endDate, maxEnd);
 			if (!now.isBefore(expireAt)) {
+				// 过期则直接关闭该行
 				StoreProductDiscountRule update = new StoreProductDiscountRule();
 				update.setId(rule.getId());
 				update.setStatus(0);
@@ -202,30 +260,52 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 
 	/* ---------------- private helpers ---------------- */
 
-	private Map<Integer, String> buildProductNameMap(Integer storeId) {
-		// 简单从 store_cuisine 读取菜名(同门店)
-		// 如需跨多产品表,可在此聚合
-		// 这里只取未删除记录
-		// 为了避免全表扫描,真实环境建议按 productId 列表批量查询
-		return new HashMap<>(); // 可按需要补充查询
+	private static class ProductInfo {
+		String name;
+		String imageUrl;
+		java.math.BigDecimal price;
 	}
 
-	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);
+	private Map<Integer, ProductInfo> buildProductInfoMap(Integer storeId, List<Integer> productIds) {
+		if (productIds == null || productIds.isEmpty()) return Collections.emptyMap();
+		Map<Integer, ProductInfo> result = new HashMap<>();
+		// 优先从美食表取
+		List<StoreCuisine> cuisines = storeCuisineService.list(new LambdaQueryWrapper<StoreCuisine>()
+				.eq(StoreCuisine::getStoreId, storeId)
+				.in(StoreCuisine::getId, productIds));
+		for (StoreCuisine c : cuisines) {
+			ProductInfo pi = new ProductInfo();
+			pi.name = c.getName();
+			pi.imageUrl = c.getImages();
+			pi.price = c.getTotalPrice();
+			result.put(c.getId(), pi);
+		}
+		// 其余从价格表取
+		List<Integer> remain = productIds.stream().filter(id -> !result.containsKey(id)).collect(Collectors.toList());
+		if (!remain.isEmpty()) {
+			List<StorePrice> prices = storePriceService.list(new LambdaQueryWrapper<StorePrice>()
+					.eq(StorePrice::getStoreId, storeId)
+					.in(StorePrice::getId, remain));
+			for (StorePrice p : prices) {
+				ProductInfo pi = new ProductInfo();
+				pi.name = p.getName();
+				pi.imageUrl = p.getImages();
+				pi.price = p.getTotalPrice();
+				result.put(p.getId(), pi);
+			}
 		}
-		return map;
+		return result;
+	}
+
+	private ProductInfo getProductInfo(Integer storeId, Integer productId) {
+		ProductInfo cached = buildProductInfoMap(storeId, java.util.Arrays.asList(productId)).get(productId);
+		return cached;
 	}
 
-	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 String groupKey(StoreProductDiscountRule r) {
+		return r.getStoreId() + "|" + r.getProductId() + "|" + r.getRuleName() + "|" + r.getDiscountType()
+				+ "|" + (r.getDiscountRate()==null?"":r.getDiscountRate().toPlainString())
+				+ "|" + r.getEffectiveMode() + "|" + formatDate(r.getStartDate()) + "|" + formatDate(r.getEndDate());
 	}
 
 	private StoreProductDiscountRule toEntity(StoreProductDiscountRuleSaveDto dto, boolean isUpdate) {
@@ -244,18 +324,6 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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 "参数不能为空";
@@ -301,7 +369,7 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		// 开启状态才参与冲突校验
 		if (dto.getStatus() != null && dto.getStatus() == 0) return null;
 
-		// 时间冲突校验(同店同菜品且status=1)
+		// 时间冲突校验(同店同菜品且status=1),单表直接基于已有行判断
 		LambdaQueryWrapper<StoreProductDiscountRule> qw = new LambdaQueryWrapper<>();
 		qw.eq(StoreProductDiscountRule::getStoreId, dto.getStoreId())
 				.eq(StoreProductDiscountRule::getProductId, dto.getProductId())
@@ -309,14 +377,9 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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();
@@ -324,23 +387,19 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		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;
+			if (!(!newStart.after(oldEnd) && !oldStart.after(newEnd))) continue;
+			int oMask = old.getWeekdayMask() == null ? 127 : old.getWeekdayMask();
+			LocalTime oSt = old.getStartTime() == null ? LocalTime.MIN : old.getStartTime();
+			LocalTime oEt = old.getEndTime() == null ? LocalTime.MAX : old.getEndTime();
 			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 "该菜品优惠时间重叠,请调整生效日期/星期/时间";
+				if ( (nMask & oMask) > 0 && nSt.isBefore(oEt) && oSt.isBefore(nEt) ) {
+					if (!dto.getDiscountType().equals(old.getDiscountType())) {
+						return "该菜品在该时间段已配置其他类型优惠";
 					}
+					return "该菜品优惠时间重叠,请调整生效日期/星期/时间";
 				}
 			}
 		}

+ 133 - 214
alien-store/src/main/java/shop/alien/store/service/impl/StoreServiceFeeRuleServiceImpl.java

@@ -2,7 +2,6 @@ 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;
@@ -12,16 +11,12 @@ 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;
@@ -55,8 +50,6 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
     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
@@ -78,26 +71,38 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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());
+        Map<String, List<StoreServiceFeeRule>> grouped = rulePage.getRecords().stream()
+                .collect(Collectors.groupingBy(this::groupKey));
+        Map<Integer, String> tableIdNameMap = storeBookingTableService.getTableListWithCategoryName(storeId, null).stream()
+                .collect(Collectors.toMap(StoreBookingTableVo::getId, StoreBookingTableVo::getTableNumber, (a, b) -> a));
+
+        List<StoreServiceFeeRuleListVo> records = new ArrayList<>();
+        for (List<StoreServiceFeeRule> group : grouped.values()) {
+            StoreServiceFeeRule head = group.get(0);
+            // 按桌台展开:每个桌台一条记录
+            Map<Integer, List<StoreServiceFeeRule>> byTable = group.stream()
+                    .filter(r -> r.getTableId() != null)
+                    .collect(Collectors.groupingBy(StoreServiceFeeRule::getTableId));
+            for (Map.Entry<Integer, List<StoreServiceFeeRule>> entry : byTable.entrySet()) {
+                Integer tableId = entry.getKey();
+                List<StoreServiceFeeRule> tableRows = entry.getValue();
+                StoreServiceFeeRuleListVo vo = new StoreServiceFeeRuleListVo();
+                vo.setId(tableRows.stream().map(StoreServiceFeeRule::getId).min(Integer::compareTo).orElse(head.getId()));
+                vo.setStoreId(head.getStoreId());
+                vo.setFeeName(head.getFeeName());
+                vo.setFeeType(head.getFeeType());
+                vo.setFeeValue(head.getFeeValue());
+                vo.setStatus(head.getStatus());
+                vo.setEffectiveMode(head.getEffectiveMode());
+                vo.setUpdatedTime(tableRows.stream().map(StoreServiceFeeRule::getUpdatedTime).filter(d -> d != null).max(Date::compareTo).orElse(head.getUpdatedTime()));
+                String tableName = tableIdNameMap.getOrDefault(tableId, "");
+                vo.setTableCount(1);
+                vo.setTableNameList(StringUtils.hasText(tableName) ? Collections.singletonList(tableName) : Collections.emptyList());
+                records.add(vo);
+            }
+        }
+
+        Page<StoreServiceFeeRuleListVo> result = new Page<>(pn, ps, records.size());
         result.setRecords(records);
         return R.data(result);
     }
@@ -112,6 +117,27 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
             return R.fail("服务费规则不存在");
         }
 
+        // 同组且同桌台的规则(用于编辑当前桌台下的时段)
+        LambdaQueryWrapper<StoreServiceFeeRule> sameGroupWrapper = new LambdaQueryWrapper<StoreServiceFeeRule>()
+                .eq(StoreServiceFeeRule::getStoreId, rule.getStoreId())
+                .eq(StoreServiceFeeRule::getFeeName, rule.getFeeName())
+                .eq(StoreServiceFeeRule::getFeeType, rule.getFeeType())
+                .eq(StoreServiceFeeRule::getFeeValue, rule.getFeeValue())
+                .eq(StoreServiceFeeRule::getStatus, rule.getStatus())
+                .eq(StoreServiceFeeRule::getEffectiveMode, rule.getEffectiveMode())
+                .eq(StoreServiceFeeRule::getTableId, rule.getTableId());
+        if (rule.getStartDate() == null) {
+            sameGroupWrapper.isNull(StoreServiceFeeRule::getStartDate);
+        } else {
+            sameGroupWrapper.eq(StoreServiceFeeRule::getStartDate, rule.getStartDate());
+        }
+        if (rule.getEndDate() == null) {
+            sameGroupWrapper.isNull(StoreServiceFeeRule::getEndDate);
+        } else {
+            sameGroupWrapper.eq(StoreServiceFeeRule::getEndDate, rule.getEndDate());
+        }
+        List<StoreServiceFeeRule> sameGroupSameTable = ruleMapper.selectList(sameGroupWrapper);
+
         StoreServiceFeeRuleDetailVo vo = new StoreServiceFeeRuleDetailVo();
         vo.setId(rule.getId());
         vo.setStoreId(rule.getStoreId());
@@ -122,14 +148,16 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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()));
+        // 仅返回当前卡片对应的桌台
+        vo.setTableIds(rule.getTableId() == null ? Collections.emptyList() : Collections.singletonList(rule.getTableId()));
+        // 仅返回当前桌台下该服务费名称的时段
+        vo.setSlots(sameGroupSameTable.stream().map(r -> {
+            StoreServiceFeeRuleSlotDto s = new StoreServiceFeeRuleSlotDto();
+            s.setWeekdayMask(r.getWeekdayMask());
+            s.setStartTime(r.getStartTime() != null ? r.getStartTime().format(TIME_FORMATTER) : null);
+            s.setEndTime(r.getEndTime() != null ? r.getEndTime().format(TIME_FORMATTER) : null);
+            return s;
+        }).collect(Collectors.toList()));
         log.info("StoreServiceFeeRuleServiceImpl.getRuleDetail?接口出参vo={}", vo);
         return R.data(vo);
     }
@@ -142,15 +170,19 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
             return R.fail(checkMsg);
         }
         Integer userId = getCurrentUserId();
-        // 以桌为主体,选择了几个桌就生成几条主表记录
+        // 以桌台 × 时段为粒度写入主表多行
         List<Integer> createdIds = new ArrayList<>();
         for (Integer tableId : dto.getTableIds()) {
-            StoreServiceFeeRule rule = new StoreServiceFeeRule();
-            fillRuleFromDto(rule, dto, userId, false);
-            ruleMapper.insert(rule);
-            // 为该条主表记录仅绑定当前桌台,并写入时段
-            saveChildrenForSingleTable(rule.getId(), dto, tableId, userId);
-            createdIds.add(rule.getId());
+            for (StoreServiceFeeRuleSlotDto slot : dto.getSlots()) {
+                StoreServiceFeeRule rule = new StoreServiceFeeRule();
+                fillRuleFromDto(rule, dto, userId, false);
+                rule.setTableId(tableId);
+                rule.setWeekdayMask(slot.getWeekdayMask());
+                rule.setStartTime(LocalTime.parse(slot.getStartTime(), TIME_FORMATTER));
+                rule.setEndTime(LocalTime.parse(slot.getEndTime(), TIME_FORMATTER));
+                ruleMapper.insert(rule);
+                createdIds.add(rule.getId());
+            }
         }
         return R.data(createdIds);
     }
@@ -169,15 +201,31 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
             return R.fail(checkMsg);
         }
 
+        // 删除同组旧记录并重建
+        List<StoreServiceFeeRule> sameGroup = ruleMapper.selectList(new LambdaQueryWrapper<StoreServiceFeeRule>()
+                .eq(StoreServiceFeeRule::getStoreId, existing.getStoreId())
+                .eq(StoreServiceFeeRule::getFeeName, existing.getFeeName())
+                .eq(StoreServiceFeeRule::getFeeType, existing.getFeeType())
+                .eq(StoreServiceFeeRule::getFeeValue, existing.getFeeValue())
+                .eq(StoreServiceFeeRule::getStatus, existing.getStatus())
+                .eq(StoreServiceFeeRule::getEffectiveMode, existing.getEffectiveMode())
+                .eq(StoreServiceFeeRule::getStartDate, existing.getStartDate())
+                .eq(StoreServiceFeeRule::getEndDate, existing.getEndDate()));
+        for (StoreServiceFeeRule r : sameGroup) {
+            ruleMapper.deleteById(r.getId());
+        }
         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);
+        for (Integer tableId : dto.getTableIds()) {
+            for (StoreServiceFeeRuleSlotDto slotDto : dto.getSlots()) {
+                StoreServiceFeeRule rule = new StoreServiceFeeRule();
+                fillRuleFromDto(rule, dto, userId, true);
+                rule.setTableId(tableId);
+                rule.setWeekdayMask(slotDto.getWeekdayMask());
+                rule.setStartTime(LocalTime.parse(slotDto.getStartTime(), TIME_FORMATTER));
+                rule.setEndTime(LocalTime.parse(slotDto.getEndTime(), TIME_FORMATTER));
+                ruleMapper.insert(rule);
+            }
+        }
         return R.data(true);
     }
 
@@ -190,9 +238,15 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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));
+        ruleMapper.delete(new LambdaQueryWrapper<StoreServiceFeeRule>()
+                .eq(StoreServiceFeeRule::getStoreId, existing.getStoreId())
+                .eq(StoreServiceFeeRule::getFeeName, existing.getFeeName())
+                .eq(StoreServiceFeeRule::getFeeType, existing.getFeeType())
+                .eq(StoreServiceFeeRule::getFeeValue, existing.getFeeValue())
+                .eq(StoreServiceFeeRule::getStatus, existing.getStatus())
+                .eq(StoreServiceFeeRule::getEffectiveMode, existing.getEffectiveMode())
+                .eq(StoreServiceFeeRule::getStartDate, existing.getStartDate())
+                .eq(StoreServiceFeeRule::getEndDate, existing.getEndDate()));
         return R.data(true);
     }
 
@@ -224,21 +278,10 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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));
+            LocalTime maxEndTime = rule.getEndTime() != null ? rule.getEndTime() : LocalTime.of(23, 59, 59);
             LocalDateTime expireAt = LocalDateTime.of(endDate, maxEndTime);
             if (!now.isBefore(expireAt)) {
                 StoreServiceFeeRule update = new StoreServiceFeeRule();
@@ -256,44 +299,9 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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 String groupKey(StoreServiceFeeRule r) {
+        return r.getStoreId() + "|" + r.getFeeName() + "|" + r.getFeeType() + "|" + r.getFeeValue() + "|" + r.getStatus()
+                + "|" + r.getEffectiveMode() + "|" + formatDate(r.getStartDate()) + "|" + formatDate(r.getEndDate());
     }
 
     private void fillRuleFromDto(StoreServiceFeeRule rule, StoreServiceFeeRuleSaveDto dto, Integer userId, boolean isUpdate) {
@@ -317,49 +325,7 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         }
     }
 
-    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 void saveChildrenForSingleTable(Integer ruleId, StoreServiceFeeRuleSaveDto dto, Integer tableId, Integer userId) {
-        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);
@@ -371,28 +337,15 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         List<Integer> tableIdsForNameCheck = dto.getTableIds() == null ? Collections.emptyList()
                 : dto.getTableIds().stream().distinct().collect(Collectors.toList());
         if (!tableIdsForNameCheck.isEmpty()) {
-            List<StoreServiceFeeRuleTable> sameTableRelations = ruleTableMapper.selectList(
-                    new LambdaQueryWrapper<StoreServiceFeeRuleTable>()
-                            .eq(StoreServiceFeeRuleTable::getStoreId, dto.getStoreId())
-                            .in(StoreServiceFeeRuleTable::getTableId, tableIdsForNameCheck));
-            if (!sameTableRelations.isEmpty()) {
-                List<Integer> sameTableRuleIds = sameTableRelations.stream()
-                        .map(StoreServiceFeeRuleTable::getRuleId)
-                        .distinct()
-                        .collect(Collectors.toList());
-                if (excludeRuleId != null) {
-                    sameTableRuleIds = sameTableRuleIds.stream()
-                            .filter(id -> !id.equals(excludeRuleId))
-                            .collect(Collectors.toList());
-                }
-                if (!sameTableRuleIds.isEmpty()) {
-                    LambdaQueryWrapper<StoreServiceFeeRule> sameTableNameWrapper = new LambdaQueryWrapper<>();
-                    sameTableNameWrapper.in(StoreServiceFeeRule::getId, sameTableRuleIds)
-                            .eq(StoreServiceFeeRule::getFeeName, dto.getFeeName().trim());
-                    if (ruleMapper.selectCount(sameTableNameWrapper) > 0) {
-                        return "同桌台下服务费名称已存在";
-                    }
-                }
+            LambdaQueryWrapper<StoreServiceFeeRule> sameName = new LambdaQueryWrapper<>();
+            sameName.eq(StoreServiceFeeRule::getStoreId, dto.getStoreId())
+                    .in(StoreServiceFeeRule::getTableId, tableIdsForNameCheck)
+                    .eq(StoreServiceFeeRule::getFeeName, dto.getFeeName().trim());
+            if (excludeRuleId != null) {
+                sameName.ne(StoreServiceFeeRule::getId, excludeRuleId);
+            }
+            if (ruleMapper.selectCount(sameName) > 0) {
+                return "同桌台下服务费名称已存在";
             }
         }
 
@@ -463,24 +416,9 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
 
     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())
+        ruleWrapper.eq(StoreServiceFeeRule::getStoreId, dto.getStoreId())
+                .in(StoreServiceFeeRule::getTableId, tableIds)
                 .eq(StoreServiceFeeRule::getStatus, 1);
         if (excludeRuleId != null) {
             ruleWrapper.ne(StoreServiceFeeRule::getId, excludeRuleId);
@@ -489,11 +427,6 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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));
@@ -505,27 +438,19 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
             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 + "]服务费时间重叠,请调整生效日期/星期/时间";
+                if ((ns.getWeekdayMask() & (existRule.getWeekdayMask() == null ? 0 : existRule.getWeekdayMask())) <= 0) {
+                    continue;
+                }
+                if (isTimeOverlap(nsStart, nsEnd, existRule.getStartTime(), existRule.getEndTime())) {
+                    Integer tableId = existRule.getTableId();
+                    if (tableId != null && tableIds.contains(tableId)) {
+                        if (!dto.getFeeType().equals(existRule.getFeeType())) {
+                            return "桌台[" + tableId + "]在该时间段已配置其他类型服务费";
                         }
+                        return "桌台[" + tableId + "]服务费时间重叠,请调整生效日期/星期/时间";
                     }
                 }
             }
@@ -557,13 +482,7 @@ public class StoreServiceFeeRuleServiceImpl implements StoreServiceFeeRuleServic
         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 {