12
0

2 Ревизии 5a1ad288c5 ... ac56c7570c

Автор SHA1 Съобщение Дата
  刘云鑫 ac56c7570c Merge remote-tracking branch 'origin/sit-new-checkstand' into sit-new-checkstand преди 2 седмици
  刘云鑫 b9018a22b3 feat:查询菜品->查询菜品优惠替换菜品价格 преди 2 седмици

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

@@ -112,4 +112,20 @@ public class PriceListVo {
     @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date updatedTime;
+
+    /** 以下字段由价目分页等接口按 {@code store_product_discount_rule} 与当前时间计算回填,非库表列 */
+    @ApiModelProperty(value = "当前时刻是否存在生效中的菜品优惠(store_product_discount_rule)")
+    private Boolean hasActiveDiscount;
+
+    @ApiModelProperty(value = "优惠类型: FREE / DISCOUNT")
+    private String discountType;
+
+    @ApiModelProperty(value = "折扣比例(0-100),DISCOUNT 时有值")
+    private BigDecimal discountRate;
+
+    @ApiModelProperty(value = "当前生效规则名称(最优价对应规则)")
+    private String discountRuleName;
+
+    @ApiModelProperty(value = "按当前时间命中规则估算的优惠后价格")
+    private BigDecimal discountedPrice;
 }

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

@@ -26,6 +26,7 @@ import shop.alien.store.service.StoreCuisineComboService;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StoreInfoService;
 import shop.alien.store.service.StorePriceService;
+import shop.alien.store.service.StoreProductDiscountService;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.store.util.ai.AiGetPriceUtil;
 
@@ -63,6 +64,8 @@ public class StoreCuisineController {
 
     private final StoreCuisineComboService storeCuisineComboService;
 
+    private final StoreProductDiscountService storeProductDiscountService;
+
     @ApiOperation("新增美食套餐或单品")
     @ApiOperationSupport(order = 1)
     @PostMapping("/addCuisineCombo")
@@ -531,6 +534,8 @@ public class StoreCuisineController {
                     priceListVo.add(vo);
                 }
             }
+            // 回填 store_product_discount_rule:当前生效优惠与估算折后价(美食价目 productId=菜品 id)
+            storeProductDiscountService.fillActiveDiscountForPriceList(priceListVo);
             return R.data(priceListVo);
         }else{
             Page<StorePrice> page = new Page<>(pageNum, pageSize);
@@ -573,6 +578,8 @@ public class StoreCuisineController {
                     priceListVo.add(vo);
                 }
             }
+            // 同上,通用价目 productId=store_price.id
+            storeProductDiscountService.fillActiveDiscountForPriceList(priceListVo);
             return R.data(priceListVo);
         }
     }

+ 1 - 2
alien-store/src/main/java/shop/alien/store/controller/UserReservationController.java

@@ -9,11 +9,10 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.dto.UserReservationDTO;
 import shop.alien.entity.store.vo.UserReservationVo;
-import shop.alien.store.service.UserReservationService;
 import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
+import shop.alien.store.service.UserReservationService;
 import shop.alien.store.vo.ReservationOrderDetailVo;
 
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;

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

@@ -5,6 +5,9 @@ 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;
+import shop.alien.entity.store.vo.PriceListVo;
+
+import java.util.List;
 
 public interface StoreProductDiscountService {
 
@@ -26,5 +29,13 @@ public interface StoreProductDiscountService {
      * @return 本次关闭数量
      */
     int autoCloseExpiredCustomRules();
+
+    /**
+     * 按门店批量查询 {@code store_product_discount_rule}(status=1),结合日期、星期、时段判断当前是否命中;
+     * 同一菜品多规则时取优惠后价最低的一条,写入 {@link PriceListVo} 的 hasActiveDiscount / discount* / discountedPrice。
+     *
+     * @param items 已含 storeId、id(商品主键)、totalPrice 的价目行
+     */
+    void fillActiveDiscountForPriceList(List<PriceListVo> items);
 }
 

+ 166 - 1
alien-store/src/main/java/shop/alien/store/service/impl/StoreProductDiscountServiceImpl.java

@@ -12,6 +12,7 @@ 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.PriceListVo;
 import shop.alien.entity.store.vo.StoreProductDiscountRuleDetailVo;
 import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
 import shop.alien.mapper.StoreProductDiscountRuleMapper;
@@ -20,6 +21,7 @@ import shop.alien.store.service.StoreProductDiscountService;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StorePriceService;
 
+import java.math.RoundingMode;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
@@ -258,7 +260,170 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
 		return closed;
 	}
 
-	/* ---------------- private helpers ---------------- */
+	/**
+	 * 按门店分组批量查规则,避免分页内逐条查库;先清空各行的优惠字段再回填命中项。
+	 */
+	@Override
+	public void fillActiveDiscountForPriceList(List<PriceListVo> items) {
+		if (items == null || items.isEmpty()) {
+			return;
+		}
+		for (PriceListVo vo : items) {
+			vo.setHasActiveDiscount(Boolean.FALSE);
+			vo.setDiscountType(null);
+			vo.setDiscountRate(null);
+			vo.setDiscountRuleName(null);
+			vo.setDiscountedPrice(null);
+		}
+		LocalDateTime now = LocalDateTime.now();
+		Map<Integer, List<PriceListVo>> byStore = items.stream()
+				.filter(vo -> vo.getStoreId() != null && vo.getId() != null)
+				.collect(Collectors.groupingBy(PriceListVo::getStoreId));
+		for (Map.Entry<Integer, List<PriceListVo>> e : byStore.entrySet()) {
+			Integer storeId = e.getKey();
+			List<PriceListVo> row = e.getValue();
+			List<Integer> productIds = row.stream().map(PriceListVo::getId).distinct().collect(Collectors.toList());
+			Map<Integer, java.math.BigDecimal> basePriceByProduct = row.stream()
+					.collect(Collectors.toMap(PriceListVo::getId, PriceListVo::getTotalPrice, (a, b) -> a));
+			Map<Integer, RulePick> picks = resolveBestActiveRules(storeId, productIds, basePriceByProduct, now);
+			for (PriceListVo vo : row) {
+				RulePick pick = picks.get(vo.getId());
+				if (pick == null || pick.head == null) {
+					continue;
+				}
+				vo.setHasActiveDiscount(Boolean.TRUE);
+				vo.setDiscountType(pick.head.getDiscountType());
+				vo.setDiscountRate(pick.head.getDiscountRate());
+				vo.setDiscountRuleName(pick.head.getRuleName());
+				vo.setDiscountedPrice(pick.discountedPrice);
+			}
+		}
+	}
+
+	/* ---------------- 价目列表优惠回填(与 fillActiveDiscountForPriceList 配套) ---------------- */
+
+	/** 命中规则的代表行(同组多行仅时段不同,取首行即可读类型/名称/折扣率) */
+	private static final class RulePick {
+		final StoreProductDiscountRule head;
+		final java.math.BigDecimal discountedPrice;
+
+		RulePick(StoreProductDiscountRule head, java.math.BigDecimal discountedPrice) {
+			this.head = head;
+			this.discountedPrice = discountedPrice;
+		}
+	}
+
+	/**
+	 * 对每个商品:按 groupKey 聚合同一规则的多时段行,任一时段命中即该规则可选;再与同类规则比优惠后价取最低。
+	 */
+	private Map<Integer, RulePick> resolveBestActiveRules(Integer storeId, List<Integer> productIds,
+			Map<Integer, java.math.BigDecimal> basePriceByProduct, LocalDateTime now) {
+		if (productIds == null || productIds.isEmpty()) {
+			return Collections.emptyMap();
+		}
+		List<StoreProductDiscountRule> all = ruleMapper.selectList(
+				new LambdaQueryWrapper<StoreProductDiscountRule>()
+						.eq(StoreProductDiscountRule::getStoreId, storeId)
+						.eq(StoreProductDiscountRule::getStatus, 1)
+						.in(StoreProductDiscountRule::getProductId, productIds));
+		if (all == null || all.isEmpty()) {
+			return Collections.emptyMap();
+		}
+		Map<Integer, List<StoreProductDiscountRule>> byProduct = all.stream()
+				.collect(Collectors.groupingBy(StoreProductDiscountRule::getProductId));
+		Map<Integer, RulePick> result = new HashMap<>();
+		for (Integer pid : productIds) {
+			List<StoreProductDiscountRule> plist = byProduct.getOrDefault(pid, Collections.emptyList());
+			Map<String, List<StoreProductDiscountRule>> groups = plist.stream().collect(Collectors.groupingBy(this::groupKey));
+			java.math.BigDecimal base = basePriceByProduct.get(pid);
+			StoreProductDiscountRule bestHead = null;
+			java.math.BigDecimal bestPrice = null;
+			for (List<StoreProductDiscountRule> group : groups.values()) {
+				if (!groupMatchesNow(group, now)) {
+					continue;
+				}
+				StoreProductDiscountRule head = group.get(0);
+				java.math.BigDecimal dp = computeDiscountedPrice(base, head);
+				if (dp == null && !TYPE_FREE.equals(head.getDiscountType())) {
+					continue;
+				}
+				if (bestPrice == null || (dp != null && dp.compareTo(bestPrice) < 0)) {
+					bestPrice = dp;
+					bestHead = head;
+				}
+			}
+			if (bestHead != null) {
+				result.put(pid, new RulePick(bestHead, bestPrice));
+			}
+		}
+		return result;
+	}
+
+	/** 同一规则拆成多行时段时,只要有一行满足当前时刻即视为该规则生效 */
+	private boolean groupMatchesNow(List<StoreProductDiscountRule> group, LocalDateTime now) {
+		for (StoreProductDiscountRule r : group) {
+			if (rowMatchesNow(r, now)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 单行命中条件:日期在规则范围内、星期掩码含当天、时刻落在 [startTime, endTime](二者皆空视为全天)。
+	 * weekdayMask:位 0~6 对应周一至周日,与 {@link StoreProductDiscountRule#getWeekdayMask()} 约定一致。
+	 */
+	private boolean rowMatchesNow(StoreProductDiscountRule r, LocalDateTime now) {
+		LocalDate day = now.toLocalDate();
+		if (!isDateInRuleRange(day, r)) {
+			return false;
+		}
+		int dow = now.getDayOfWeek().getValue();
+		int mask = r.getWeekdayMask() == null ? 127 : r.getWeekdayMask();
+		if ((mask & (1 << (dow - 1))) == 0) {
+			return false;
+		}
+		LocalTime t = now.toLocalTime();
+		LocalTime st = r.getStartTime();
+		LocalTime et = r.getEndTime();
+		if (st == null && et == null) {
+			return true;
+		}
+		if (st == null || et == null) {
+			return true;
+		}
+		return !t.isBefore(st) && !t.isAfter(et);
+	}
+
+	/** PERMANENT 不校验日历;CUSTOM 要求当天在 [startDate, endDate] 闭区间内 */
+	private boolean isDateInRuleRange(LocalDate day, StoreProductDiscountRule r) {
+		if (MODE_PERMANENT.equals(r.getEffectiveMode())) {
+			return true;
+		}
+		if (!MODE_CUSTOM.equals(r.getEffectiveMode())) {
+			return false;
+		}
+		if (r.getStartDate() == null || r.getEndDate() == null) {
+			return false;
+		}
+		LocalDate ls = new java.sql.Date(r.getStartDate().getTime()).toLocalDate();
+		LocalDate le = new java.sql.Date(r.getEndDate().getTime()).toLocalDate();
+		return !day.isBefore(ls) && !day.isAfter(le);
+	}
+
+	/** FREE 视为 0 元;DISCOUNT 需原价才能算出折后价,原价缺失则该规则不参与比价 */
+	private java.math.BigDecimal computeDiscountedPrice(java.math.BigDecimal total, StoreProductDiscountRule head) {
+		if (TYPE_FREE.equals(head.getDiscountType())) {
+			return java.math.BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+		}
+		if (TYPE_DISCOUNT.equals(head.getDiscountType()) && head.getDiscountRate() != null) {
+			if (total == null) {
+				return null;
+			}
+			return total.multiply(head.getDiscountRate()).divide(java.math.BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+		}
+		return null;
+	}
 
 	private static class ProductInfo {
 		String name;