|
@@ -12,6 +12,7 @@ import shop.alien.entity.result.R;
|
|
|
import shop.alien.entity.store.*;
|
|
import shop.alien.entity.store.*;
|
|
|
import shop.alien.entity.store.dto.StoreProductDiscountRuleSaveDto;
|
|
import shop.alien.entity.store.dto.StoreProductDiscountRuleSaveDto;
|
|
|
import shop.alien.entity.store.dto.StoreProductDiscountRuleSlotDto;
|
|
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.StoreProductDiscountRuleDetailVo;
|
|
|
import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
|
|
import shop.alien.entity.store.vo.StoreProductDiscountRuleListVo;
|
|
|
import shop.alien.mapper.StoreProductDiscountRuleMapper;
|
|
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.StoreCuisineService;
|
|
|
import shop.alien.store.service.StorePriceService;
|
|
import shop.alien.store.service.StorePriceService;
|
|
|
|
|
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.text.ParseException;
|
|
import java.text.ParseException;
|
|
|
import java.text.SimpleDateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDate;
|
|
@@ -258,7 +260,170 @@ public class StoreProductDiscountServiceImpl implements StoreProductDiscountServ
|
|
|
return closed;
|
|
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 {
|
|
private static class ProductInfo {
|
|
|
String name;
|
|
String name;
|