|
@@ -0,0 +1,1025 @@
|
|
|
|
|
+package shop.alien.dining.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+import shop.alien.dining.config.BaseRedisService;
|
|
|
|
|
+import shop.alien.dining.constants.OrderMenuConstants;
|
|
|
|
|
+import shop.alien.dining.service.GenericCartService;
|
|
|
|
|
+import shop.alien.entity.store.StoreCart;
|
|
|
|
|
+import shop.alien.entity.store.StoreCouponUsage;
|
|
|
|
|
+import shop.alien.entity.store.StoreInfo;
|
|
|
|
|
+import shop.alien.entity.store.StorePrice;
|
|
|
|
|
+import shop.alien.entity.store.StoreTable;
|
|
|
|
|
+import shop.alien.entity.store.dto.AddCartItemDTO;
|
|
|
|
|
+import shop.alien.entity.store.dto.CartDTO;
|
|
|
|
|
+import shop.alien.entity.store.dto.CartItemDTO;
|
|
|
|
|
+import shop.alien.mapper.StoreCartMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreCouponUsageMapper;
|
|
|
|
|
+import shop.alien.mapper.StorePriceMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreInfoMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreProductDiscountRuleMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreTableMapper;
|
|
|
|
|
+import shop.alien.dining.support.DiningMenuPricing;
|
|
|
|
|
+import shop.alien.dining.support.StoreCartMenuFilters;
|
|
|
|
|
+import shop.alien.dining.util.TokenUtil;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
|
|
+import java.util.concurrent.Executors;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 通用价目表(store_price)购物车:Redis/DB 与美食购物车隔离(menu_type=2)。
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class GenericCartServiceImpl implements GenericCartService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String CART_KEY_PREFIX = OrderMenuConstants.GENERIC_CART_REDIS_PREFIX;
|
|
|
|
|
+ private static final String COUPON_USED_KEY_PREFIX = OrderMenuConstants.GENERIC_COUPON_REDIS_PREFIX;
|
|
|
|
|
+ private static final int CART_EXPIRE_SECONDS = 24 * 60 * 60; // 24小时过期
|
|
|
|
|
+
|
|
|
|
|
+ // 异步写入数据库的线程池(专门用于购物车数据库写入)
|
|
|
|
|
+ private static final ExecutorService CART_DB_WRITE_EXECUTOR = Executors.newFixedThreadPool(5, r -> {
|
|
|
|
|
+ Thread t = new Thread(r, "generic-cart-db-write-" + System.currentTimeMillis());
|
|
|
|
|
+ t.setDaemon(true);
|
|
|
|
|
+ return t;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
|
|
+ private final StoreTableMapper storeTableMapper;
|
|
|
|
|
+ private final StorePriceMapper storePriceMapper;
|
|
|
|
|
+ private final StoreCartMapper storeCartMapper;
|
|
|
|
|
+ private final StoreCouponUsageMapper storeCouponUsageMapper;
|
|
|
|
|
+ private final StoreInfoMapper storeInfoMapper;
|
|
|
|
|
+ private final StoreProductDiscountRuleMapper storeProductDiscountRuleMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO getCart(Integer tableId) {
|
|
|
|
|
+ log.info("获取通用价目购物车, tableId={}", tableId);
|
|
|
|
|
+ String cartKey = CART_KEY_PREFIX + tableId;
|
|
|
|
|
+ String cartJson = baseRedisService.getString(cartKey);
|
|
|
|
|
+
|
|
|
|
|
+ CartDTO cart = new CartDTO();
|
|
|
|
|
+ cart.setTableId(tableId);
|
|
|
|
|
+
|
|
|
|
|
+ // 查询桌号信息
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ cart.setTableNumber(table.getTableNumber());
|
|
|
|
|
+ cart.setStoreId(table.getStoreId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.hasText(cartJson)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject cartObj = JSON.parseObject(cartJson);
|
|
|
|
|
+ List<CartItemDTO> items = cartObj.getList("items", CartItemDTO.class);
|
|
|
|
|
+ if (items != null) {
|
|
|
|
|
+ cart.setItems(items);
|
|
|
|
|
+ // 计算总金额和总数量
|
|
|
|
|
+ BigDecimal totalAmount = items.stream()
|
|
|
|
|
+ .map(CartItemDTO::getSubtotalAmount)
|
|
|
|
|
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ Integer totalQuantity = items.stream()
|
|
|
|
|
+ .mapToInt(CartItemDTO::getQuantity)
|
|
|
|
|
+ .sum();
|
|
|
|
|
+ cart.setTotalAmount(totalAmount);
|
|
|
|
|
+ cart.setTotalQuantity(totalQuantity);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cart.setItems(new ArrayList<>());
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析购物车数据失败: {}", e.getMessage(), e);
|
|
|
|
|
+ cart.setItems(new ArrayList<>());
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Redis中没有,尝试从数据库加载
|
|
|
|
|
+ cart = loadCartFromDatabase(tableId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按门店标价 + {@code store_product_discount_rule} 刷新行单价/小计/总价,并写回 Redis+DB(与下单明细口径一致)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void applyRealtimeMenuPricing(CartDTO cart) {
|
|
|
|
|
+ if (cart == null || cart.getStoreId() == null || cart.getItems() == null || cart.getItems().isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+ Set<Integer> cuisineIds = items.stream()
|
|
|
|
|
+ .map(CartItemDTO::getCuisineId)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .filter(id -> id > 0)
|
|
|
|
|
+ .collect(Collectors.toSet());
|
|
|
|
|
+ Map<Integer, BigDecimal> listByIdReal = cuisineIds.isEmpty()
|
|
|
|
|
+ ? java.util.Collections.emptyMap()
|
|
|
|
|
+ : DiningMenuPricing.resolveListUnitPriceByGenericPriceId(cuisineIds, storePriceMapper);
|
|
|
|
|
+ Map<Integer, BigDecimal> saleById = cuisineIds.isEmpty()
|
|
|
|
|
+ ? java.util.Collections.emptyMap()
|
|
|
|
|
+ : DiningMenuPricing.resolveSaleUnitPriceForGenericPrice(cart.getStoreId(), listByIdReal, storeProductDiscountRuleMapper);
|
|
|
|
|
+
|
|
|
|
|
+ boolean changed = false;
|
|
|
|
|
+ for (CartItemDTO it : items) {
|
|
|
|
|
+ Integer cid = it.getCuisineId();
|
|
|
|
|
+ int qty = it.getQuantity() != null ? it.getQuantity() : 0;
|
|
|
|
|
+ if (cid == null || cid <= 0) {
|
|
|
|
|
+ BigDecimal p = it.getUnitPrice() != null ? it.getUnitPrice() : BigDecimal.ZERO;
|
|
|
|
|
+ it.setOriginalUnitPrice(p);
|
|
|
|
|
+ it.setCurrentUnitPrice(p);
|
|
|
|
|
+ it.setHasActiveDiscount(Boolean.FALSE);
|
|
|
|
|
+ BigDecimal origSub = p.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal newSub = origSub;
|
|
|
|
|
+ it.setOriginalSubtotalAmount(origSub);
|
|
|
|
|
+ if (!Objects.equals(it.getUnitPrice(), p) || !Objects.equals(it.getSubtotalAmount(), newSub)) {
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ it.setUnitPrice(p);
|
|
|
|
|
+ it.setSubtotalAmount(newSub);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal list = listByIdReal.getOrDefault(cid, BigDecimal.ZERO);
|
|
|
|
|
+ BigDecimal sale = saleById.getOrDefault(cid, list);
|
|
|
|
|
+ it.setOriginalUnitPrice(list);
|
|
|
|
|
+ it.setCurrentUnitPrice(sale);
|
|
|
|
|
+ it.setHasActiveDiscount(list.compareTo(sale) != 0);
|
|
|
|
|
+ BigDecimal origSub = list.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal newSub = sale.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ it.setOriginalSubtotalAmount(origSub);
|
|
|
|
|
+ if (!Objects.equals(it.getUnitPrice(), sale) || !Objects.equals(it.getSubtotalAmount(), newSub)
|
|
|
|
|
+ || !Objects.equals(it.getOriginalSubtotalAmount(), origSub)) {
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ it.setUnitPrice(sale);
|
|
|
|
|
+ it.setSubtotalAmount(newSub);
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal total = items.stream()
|
|
|
|
|
+ .map(CartItemDTO::getSubtotalAmount)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ cart.setTotalAmount(total);
|
|
|
|
|
+ cart.setTotalQuantity(items.stream().mapToInt(i -> i.getQuantity() != null ? i.getQuantity() : 0).sum());
|
|
|
|
|
+ if (changed) {
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从数据库加载购物车
|
|
|
|
|
+ */
|
|
|
|
|
+ private CartDTO loadCartFromDatabase(Integer tableId) {
|
|
|
|
|
+ log.info("从数据库加载购物车, tableId={}", tableId);
|
|
|
|
|
+ CartDTO cart = new CartDTO();
|
|
|
|
|
+ cart.setTableId(tableId);
|
|
|
|
|
+ cart.setItems(new ArrayList<>());
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+
|
|
|
|
|
+ // 查询桌号信息
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ cart.setTableNumber(table.getTableNumber());
|
|
|
|
|
+ cart.setStoreId(table.getStoreId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从数据库查询购物车数据
|
|
|
|
|
+ LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(StoreCart::getTableId, tableId);
|
|
|
|
|
+ wrapper.eq(StoreCart::getDeleteFlag, 0);
|
|
|
|
|
+ StoreCartMenuFilters.applyGenericCart(wrapper);
|
|
|
|
|
+ List<StoreCart> cartList = storeCartMapper.selectList(wrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (cartList != null && !cartList.isEmpty()) {
|
|
|
|
|
+ List<CartItemDTO> items = cartList.stream().map(cartItem -> {
|
|
|
|
|
+ CartItemDTO item = new CartItemDTO();
|
|
|
|
|
+ item.setCuisineId(cartItem.getCuisineId());
|
|
|
|
|
+ item.setCuisineName(cartItem.getCuisineName());
|
|
|
|
|
+ item.setCuisineImage(cartItem.getCuisineImage());
|
|
|
|
|
+ item.setUnitPrice(cartItem.getUnitPrice());
|
|
|
|
|
+ item.setQuantity(cartItem.getQuantity());
|
|
|
|
|
+ item.setLockedQuantity(cartItem.getLockedQuantity());
|
|
|
|
|
+ item.setSubtotalAmount(cartItem.getSubtotalAmount());
|
|
|
|
|
+ item.setAddUserId(cartItem.getAddUserId());
|
|
|
|
|
+ item.setAddUserPhone(cartItem.getAddUserPhone());
|
|
|
|
|
+ item.setRemark(cartItem.getRemark());
|
|
|
|
|
+ return item;
|
|
|
|
|
+ }).collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ cart.setItems(items);
|
|
|
|
|
+ BigDecimal totalAmount = items.stream()
|
|
|
|
|
+ .map(CartItemDTO::getSubtotalAmount)
|
|
|
|
|
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ Integer totalQuantity = items.stream()
|
|
|
|
|
+ .mapToInt(CartItemDTO::getQuantity)
|
|
|
|
|
+ .sum();
|
|
|
|
|
+ cart.setTotalAmount(totalAmount);
|
|
|
|
|
+ cart.setTotalQuantity(totalQuantity);
|
|
|
|
|
+
|
|
|
|
|
+ // 同步到Redis
|
|
|
|
|
+ saveCartToRedis(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO addItem(AddCartItemDTO dto) {
|
|
|
|
|
+ log.info("添加商品到购物车, dto={}", dto);
|
|
|
|
|
+ // 验证桌号(须为通用价目桌 type=2)
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(dto.getTableId());
|
|
|
|
|
+ if (table == null) {
|
|
|
|
|
+ throw new RuntimeException("桌号不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (table.getType() == null || !Integer.valueOf(2).equals(table.getType())) {
|
|
|
|
|
+ throw new RuntimeException("当前桌台不是通用价目桌,无法使用本购物车");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // dto.cuisineId 此处表示 store_price.id
|
|
|
|
|
+ StorePrice price = storePriceMapper.selectById(dto.getCuisineId());
|
|
|
|
|
+ if (price == null) {
|
|
|
|
|
+ throw new RuntimeException("价目项不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (price.getShelfStatus() == null || price.getShelfStatus() != 1) {
|
|
|
|
|
+ throw new RuntimeException("价目项已下架");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (price.getStatus() == null || price.getStatus() != 1) {
|
|
|
|
|
+ throw new RuntimeException("价目项未审核通过");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前用户信息
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+ String userPhone = TokenUtil.getCurrentUserPhone();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(dto.getTableId());
|
|
|
|
|
+
|
|
|
|
|
+ // 查找是否已存在该商品
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+ CartItemDTO existingItem = items.stream()
|
|
|
|
|
+ .filter(item -> item.getCuisineId().equals(dto.getCuisineId()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (existingItem != null) {
|
|
|
|
|
+ // 商品已存在
|
|
|
|
|
+ Integer lockedQuantity = existingItem.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0) {
|
|
|
|
|
+ // 如果商品有已下单数量,将新数量叠加到当前数量和已下单数量上
|
|
|
|
|
+ Integer newQuantity = existingItem.getQuantity() + dto.getQuantity();
|
|
|
|
|
+ Integer newLockedQuantity = lockedQuantity + dto.getQuantity();
|
|
|
|
|
+ existingItem.setQuantity(newQuantity);
|
|
|
|
|
+ existingItem.setLockedQuantity(newLockedQuantity);
|
|
|
|
|
+ existingItem.setSubtotalAmount(existingItem.getUnitPrice()
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(newQuantity)));
|
|
|
|
|
+ log.info("商品已存在且有已下单数量,叠加数量, priceId={}, oldQuantity={}, newQuantity={}, oldOrderedQuantity={}, newOrderedQuantity={}",
|
|
|
|
|
+ dto.getCuisineId(), existingItem.getQuantity() - dto.getQuantity(), newQuantity, lockedQuantity, newLockedQuantity);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 商品已存在但没有已下单数量,不允许重复添加
|
|
|
|
|
+ throw new RuntimeException("已添加过本商品");
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 添加新商品
|
|
|
|
|
+ CartItemDTO newItem = new CartItemDTO();
|
|
|
|
|
+ newItem.setCuisineId(price.getId());
|
|
|
|
|
+ newItem.setCuisineName(price.getName());
|
|
|
|
|
+ newItem.setCuisineType(1);
|
|
|
|
|
+ newItem.setCuisineImage(price.getImages());
|
|
|
|
|
+ newItem.setUnitPrice(price.getTotalPrice());
|
|
|
|
|
+ newItem.setQuantity(dto.getQuantity());
|
|
|
|
|
+ newItem.setSubtotalAmount(price.getTotalPrice()
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(dto.getQuantity())));
|
|
|
|
|
+ newItem.setAddUserId(userId);
|
|
|
|
|
+ newItem.setAddUserPhone(userPhone);
|
|
|
|
|
+ newItem.setRemark(dto.getRemark());
|
|
|
|
|
+ items.add(newItem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity) {
|
|
|
|
|
+ log.info("更新购物车商品数量, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果数量为0或小于0,删除该商品
|
|
|
|
|
+ if (quantity == null || quantity <= 0) {
|
|
|
|
|
+ log.info("商品数量为0或小于0,删除商品, tableId={}, cuisineId={}", tableId, cuisineId);
|
|
|
|
|
+ return removeItem(tableId, cuisineId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+ CartItemDTO item = items.stream()
|
|
|
|
|
+ .filter(i -> i.getCuisineId().equals(cuisineId))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (item != null) {
|
|
|
|
|
+ // 商品已存在,更新数量
|
|
|
|
|
+ // 检查已下单数量:不允许将数量减少到小于已下单数量
|
|
|
|
|
+ Integer lockedQuantity = item.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0) {
|
|
|
|
|
+ if (quantity < lockedQuantity) {
|
|
|
|
|
+ throw new RuntimeException("商品数量不能少于已下单数量(" + lockedQuantity + "),该数量已下单");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ item.setQuantity(quantity);
|
|
|
|
|
+ item.setSubtotalAmount(item.getUnitPrice()
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(quantity)));
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("商品不在购物车中,自动添加, tableId={}, priceId={}, quantity={}", tableId, cuisineId, quantity);
|
|
|
|
|
+ StoreTable t = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (t == null) {
|
|
|
|
|
+ throw new RuntimeException("桌号不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (t.getType() == null || !Integer.valueOf(2).equals(t.getType())) {
|
|
|
|
|
+ throw new RuntimeException("当前桌台不是通用价目桌");
|
|
|
|
|
+ }
|
|
|
|
|
+ StorePrice price = storePriceMapper.selectById(cuisineId);
|
|
|
|
|
+ if (price == null) {
|
|
|
|
|
+ throw new RuntimeException("价目项不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (price.getShelfStatus() == null || price.getShelfStatus() != 1) {
|
|
|
|
|
+ throw new RuntimeException("价目项已下架");
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+ String userPhone = TokenUtil.getCurrentUserPhone();
|
|
|
|
|
+ CartItemDTO newItem = new CartItemDTO();
|
|
|
|
|
+ newItem.setCuisineId(price.getId());
|
|
|
|
|
+ newItem.setCuisineName(price.getName());
|
|
|
|
|
+ newItem.setCuisineType(1);
|
|
|
|
|
+ newItem.setCuisineImage(price.getImages());
|
|
|
|
|
+ newItem.setUnitPrice(price.getTotalPrice());
|
|
|
|
|
+ newItem.setQuantity(quantity);
|
|
|
|
|
+ newItem.setSubtotalAmount(price.getTotalPrice()
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(quantity)));
|
|
|
|
|
+ newItem.setAddUserId(userId);
|
|
|
|
|
+ newItem.setAddUserPhone(userPhone);
|
|
|
|
|
+ items.add(newItem);
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ log.info("商品已自动添加到购物车, tableId={}, priceId={}, quantity={}", tableId, cuisineId, quantity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO removeItem(Integer tableId, Integer cuisineId) {
|
|
|
|
|
+ log.info("删除购物车商品, tableId={}, cuisineId={}", tableId, cuisineId);
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有已下单数量,如果有则不允许删除
|
|
|
|
|
+ CartItemDTO item = items.stream()
|
|
|
|
|
+ .filter(i -> i.getCuisineId().equals(cuisineId))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (item != null) {
|
|
|
|
|
+ Integer lockedQuantity = item.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0) {
|
|
|
|
|
+ throw new RuntimeException("商品已下单,已下单数量为 " + lockedQuantity + ",不允许删除");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ items.removeIf(i -> i.getCuisineId().equals(cuisineId));
|
|
|
|
|
+
|
|
|
|
|
+ if (items.isEmpty()) {
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void clearCart(Integer tableId) {
|
|
|
|
|
+ log.info("清空购物车(保留已下单商品), tableId={}", tableId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
|
|
+ log.info("购物车为空,无需清空, tableId={}", tableId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 分离已下单的商品、未下单的商品和餐具
|
|
|
|
|
+ List<CartItemDTO> orderedItems = new ArrayList<>(); // 已下单的商品(保留,数量恢复为已下单数量)
|
|
|
|
|
+ List<Integer> orderedCuisineIds = new ArrayList<>(); // 已下单的商品ID列表
|
|
|
|
|
+ List<CartItemDTO> unorderedItems = new ArrayList<>(); // 未下单的商品(删除)
|
|
|
|
|
+ CartItemDTO tablewareItem = null; // 餐具项(始终保留)
|
|
|
|
|
+ boolean hasChanges = false; // 是否有变化(需要更新)
|
|
|
|
|
+
|
|
|
|
|
+ for (CartItemDTO item : items) {
|
|
|
|
|
+ // 餐具始终保留,不清空
|
|
|
|
|
+ if (TABLEWARE_CUISINE_ID.equals(item.getCuisineId())) {
|
|
|
|
|
+ tablewareItem = item;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Integer lockedQuantity = item.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0) {
|
|
|
|
|
+ // 有已下单数量,保留该商品,但将当前数量恢复为已下单数量
|
|
|
|
|
+ Integer currentQuantity = item.getQuantity();
|
|
|
|
|
+ if (currentQuantity != null && !currentQuantity.equals(lockedQuantity)) {
|
|
|
|
|
+ // 当前数量不等于已下单数量,需要恢复
|
|
|
|
|
+ item.setQuantity(lockedQuantity);
|
|
|
|
|
+ item.setSubtotalAmount(item.getUnitPrice().multiply(BigDecimal.valueOf(lockedQuantity)));
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ log.info("恢复已下单商品数量, cuisineId={}, oldQuantity={}, orderedQuantity={}",
|
|
|
|
|
+ item.getCuisineId(), currentQuantity, lockedQuantity);
|
|
|
|
|
+ }
|
|
|
|
|
+ orderedItems.add(item);
|
|
|
|
|
+ orderedCuisineIds.add(item.getCuisineId());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有已下单数量,标记为删除
|
|
|
|
|
+ unorderedItems.add(item);
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将餐具项添加到保留列表中,若有已下单数量则恢复为已下单数量(与菜品逻辑一致)
|
|
|
|
|
+ if (tablewareItem != null) {
|
|
|
|
|
+ Integer tablewareLocked = tablewareItem.getLockedQuantity();
|
|
|
|
|
+ if (tablewareLocked != null && tablewareLocked > 0) {
|
|
|
|
|
+ Integer currentQty = tablewareItem.getQuantity();
|
|
|
|
|
+ if (currentQty == null || !currentQty.equals(tablewareLocked)) {
|
|
|
|
|
+ tablewareItem.setQuantity(tablewareLocked);
|
|
|
|
|
+ tablewareItem.setSubtotalAmount(tablewareItem.getUnitPrice().multiply(BigDecimal.valueOf(tablewareLocked)));
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ log.info("恢复餐具数量为已下单数量, cuisineId={}, oldQuantity={}, orderedQuantity={}",
|
|
|
|
|
+ tablewareItem.getCuisineId(), currentQty, tablewareLocked);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ orderedItems.add(tablewareItem);
|
|
|
|
|
+ orderedCuisineIds.add(tablewareItem.getCuisineId());
|
|
|
|
|
+ log.info("保留餐具项, cuisineId={}, quantity={}", tablewareItem.getCuisineId(), tablewareItem.getQuantity());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有变化(有未下单的商品需要删除,或者已下单商品数量需要恢复),进行更新
|
|
|
|
|
+ if (hasChanges) {
|
|
|
|
|
+ // 1. 更新购物车(删除未下单商品,已下单商品数量已恢复)
|
|
|
|
|
+ cart.setItems(orderedItems);
|
|
|
|
|
+ if (!orderedItems.isEmpty()) {
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新Redis(保留已下单的商品,数量已恢复)
|
|
|
|
|
+ if (orderedItems.isEmpty()) {
|
|
|
|
|
+ // 如果所有商品都未下单,清空Redis
|
|
|
|
|
+ String cartKey = CART_KEY_PREFIX + tableId;
|
|
|
|
|
+ baseRedisService.delete(cartKey);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 保存更新后的购物车到Redis(已下单商品数量已恢复为已下单数量)
|
|
|
|
|
+ saveCartToRedis(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 从数据库中逻辑删除未下单的商品(排除餐具)
|
|
|
|
|
+ if (!unorderedItems.isEmpty()) {
|
|
|
|
|
+ LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(StoreCart::getTableId, tableId);
|
|
|
|
|
+ wrapper.eq(StoreCart::getDeleteFlag, 0);
|
|
|
|
|
+ StoreCartMenuFilters.applyGenericCart(wrapper);
|
|
|
|
|
+ // 排除餐具(cuisineId = -1)
|
|
|
|
|
+ wrapper.ne(StoreCart::getCuisineId, TABLEWARE_CUISINE_ID);
|
|
|
|
|
+ if (!orderedCuisineIds.isEmpty()) {
|
|
|
|
|
+ // 排除已下单的商品ID(包括餐具)
|
|
|
|
|
+ wrapper.notIn(StoreCart::getCuisineId, orderedCuisineIds);
|
|
|
|
|
+ }
|
|
|
|
|
+ List<StoreCart> cartListToDelete = storeCartMapper.selectList(wrapper);
|
|
|
|
|
+ if (cartListToDelete != null && !cartListToDelete.isEmpty()) {
|
|
|
|
|
+ List<Integer> cartIds = cartListToDelete.stream()
|
|
|
|
|
+ .map(StoreCart::getId)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
|
|
|
|
|
+ storeCartMapper.deleteBatchIds(cartIds);
|
|
|
|
|
+ log.info("删除未下单商品(已排除餐具), tableId={}, count={}", tableId, cartIds.size());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 更新数据库中已下单商品的数量(恢复为已下单数量)
|
|
|
|
|
+ if (!orderedItems.isEmpty()) {
|
|
|
|
|
+ // 保存更新后的购物车到数据库(会更新已下单商品的数量)
|
|
|
|
|
+ saveCartToDatabase(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 更新桌号表的购物车统计
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ table.setCartItemCount(cart.getTotalQuantity());
|
|
|
|
|
+ table.setCartTotalAmount(cart.getTotalAmount());
|
|
|
|
|
+ storeTableMapper.updateById(table);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("清空购物车完成(保留已下单商品和餐具,数量恢复为已下单数量), tableId={}, 删除商品数={}, 保留商品数={}",
|
|
|
|
|
+ tableId, unorderedItems.size(), orderedItems.size());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("购物车无需更新, tableId={}", tableId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean hasUsedCoupon(Integer tableId) {
|
|
|
|
|
+ // 先查Redis
|
|
|
|
|
+ String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
|
|
|
|
|
+ String couponId = baseRedisService.getString(couponUsedKey);
|
|
|
|
|
+ if (StringUtils.hasText(couponId)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Redis中没有,查数据库
|
|
|
|
|
+ LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getTableId, tableId);
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
|
|
|
|
|
+ wrapper.in(StoreCouponUsage::getUsageStatus, 0, 1, 2); // 已标记使用、已下单、已支付
|
|
|
|
|
+ wrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
|
|
|
|
|
+ wrapper.last("LIMIT 1");
|
|
|
|
|
+ StoreCouponUsage usage = storeCouponUsageMapper.selectOne(wrapper);
|
|
|
|
|
+ return usage != null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void markCouponUsed(Integer tableId, Integer couponId) {
|
|
|
|
|
+ // 保存到Redis
|
|
|
|
|
+ String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
|
|
|
|
|
+ baseRedisService.setString(couponUsedKey, String.valueOf(couponId), (long) CART_EXPIRE_SECONDS);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到数据库
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table == null) {
|
|
|
|
|
+ log.warn("桌号不存在, tableId={}", tableId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否已存在
|
|
|
|
|
+ LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getTableId, tableId);
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getCouponId, couponId);
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
|
|
|
|
|
+ StoreCouponUsage existing = storeCouponUsageMapper.selectOne(wrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ StoreCouponUsage usage = new StoreCouponUsage();
|
|
|
|
|
+ usage.setTableId(tableId);
|
|
|
|
|
+ usage.setStoreId(table.getStoreId());
|
|
|
|
|
+ usage.setCouponId(couponId);
|
|
|
|
|
+ usage.setUsageStatus(0); // 已标记使用
|
|
|
|
|
+ usage.setCreatedTime(now);
|
|
|
|
|
+ usage.setUpdatedTime(now); // 设置更新时间,避免数据库约束错误
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+ if (userId != null) {
|
|
|
|
|
+ usage.setCreatedUserId(userId);
|
|
|
|
|
+ }
|
|
|
|
|
+ storeCouponUsageMapper.insert(usage);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新桌号表的优惠券ID
|
|
|
|
|
+ table.setCurrentCouponId(couponId);
|
|
|
|
|
+ storeTableMapper.updateById(table);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void clearCouponUsed(Integer tableId) {
|
|
|
|
|
+ // 清空Redis
|
|
|
|
|
+ String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
|
|
|
|
|
+ baseRedisService.delete(couponUsedKey);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新数据库(逻辑删除未下单的记录,使用 MyBatis-Plus 的 deleteBatchIds)
|
|
|
|
|
+ LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getTableId, tableId);
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
|
|
|
|
|
+ wrapper.eq(StoreCouponUsage::getUsageStatus, 0); // 只删除已标记使用但未下单的
|
|
|
|
|
+ List<StoreCouponUsage> usageList = storeCouponUsageMapper.selectList(wrapper);
|
|
|
|
|
+ if (usageList != null && !usageList.isEmpty()) {
|
|
|
|
|
+ List<Integer> usageIds = usageList.stream()
|
|
|
|
|
+ .map(StoreCouponUsage::getId)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
|
|
|
|
|
+ storeCouponUsageMapper.deleteBatchIds(usageIds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新桌号表的优惠券ID
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ table.setCurrentCouponId(null);
|
|
|
|
|
+ storeTableMapper.updateById(table);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 餐具的特殊ID(用于标识餐具项)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final Integer TABLEWARE_CUISINE_ID = -1;
|
|
|
|
|
+ private static final String TABLEWARE_NAME = "餐具费";
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取餐具单价(从 store_info 表获取)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param storeId 门店ID
|
|
|
|
|
+ * @return 餐具单价(BigDecimal),如果门店不存在或未设置餐具费,返回 0.00
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal getTablewareUnitPrice(Integer storeId) {
|
|
|
|
|
+ if (storeId == null) {
|
|
|
|
|
+ log.warn("门店ID为空,返回默认餐具单价 0.00");
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
|
|
|
|
|
+ if (storeInfo == null) {
|
|
|
|
|
+ log.warn("门店不存在, storeId={},返回默认餐具单价 0.00", storeId);
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer tablewareFee = storeInfo.getTablewareFee();
|
|
|
|
|
+ if (tablewareFee == null || tablewareFee < 0) {
|
|
|
|
|
+ log.warn("门店餐具费未设置或无效, storeId={}, tablewareFee={},返回默认餐具单价 0.00", storeId, tablewareFee);
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ return BigDecimal.valueOf(tablewareFee);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO setDinerCount(Integer tableId, Integer dinerCount) {
|
|
|
|
|
+ log.info("设置用餐人数, tableId={}, dinerCount={}", tableId, dinerCount);
|
|
|
|
|
+
|
|
|
|
|
+ if (dinerCount == null || dinerCount <= 0) {
|
|
|
|
|
+ throw new RuntimeException("用餐人数必须大于0");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取门店ID和餐具单价
|
|
|
|
|
+ Integer storeId = cart.getStoreId();
|
|
|
|
|
+ if (storeId == null) {
|
|
|
|
|
+ // 如果购物车中没有门店ID,从桌号获取
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ storeId = table.getStoreId();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal tablewareUnitPrice = getTablewareUnitPrice(storeId);
|
|
|
|
|
+
|
|
|
|
|
+ // 商铺未设置餐具费时,不往购物车加餐具;若已有餐具项则移除
|
|
|
|
|
+ if (tablewareUnitPrice == null || tablewareUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
|
|
+ log.info("门店未设置餐具费, storeId={},设置就餐人数时不添加餐具", storeId);
|
|
|
|
|
+ CartItemDTO existing = items.stream()
|
|
|
|
|
+ .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+ if (existing != null) {
|
|
|
|
|
+ items.remove(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 查找是否已存在餐具项
|
|
|
|
|
+ CartItemDTO tablewareItem = items.stream()
|
|
|
|
|
+ .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+ String userPhone = TokenUtil.getCurrentUserPhone();
|
|
|
|
|
+
|
|
|
|
|
+ if (tablewareItem != null) {
|
|
|
|
|
+ Integer lockedQuantity = tablewareItem.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0 && dinerCount < lockedQuantity) {
|
|
|
|
|
+ throw new RuntimeException("餐具数量不能少于已下单数量(" + lockedQuantity + ")");
|
|
|
|
|
+ }
|
|
|
|
|
+ tablewareItem.setQuantity(dinerCount);
|
|
|
|
|
+ tablewareItem.setUnitPrice(tablewareUnitPrice);
|
|
|
|
|
+ tablewareItem.setSubtotalAmount(tablewareUnitPrice.multiply(BigDecimal.valueOf(dinerCount)));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ CartItemDTO newTablewareItem = new CartItemDTO();
|
|
|
|
|
+ newTablewareItem.setCuisineId(TABLEWARE_CUISINE_ID);
|
|
|
|
|
+ newTablewareItem.setCuisineName(TABLEWARE_NAME);
|
|
|
|
|
+ newTablewareItem.setCuisineType(0);
|
|
|
|
|
+ newTablewareItem.setCuisineImage("");
|
|
|
|
|
+ newTablewareItem.setUnitPrice(tablewareUnitPrice);
|
|
|
|
|
+ newTablewareItem.setQuantity(dinerCount);
|
|
|
|
|
+ newTablewareItem.setSubtotalAmount(tablewareUnitPrice.multiply(BigDecimal.valueOf(dinerCount)));
|
|
|
|
|
+ newTablewareItem.setAddUserId(userId);
|
|
|
|
|
+ newTablewareItem.setAddUserPhone(userPhone);
|
|
|
|
|
+ items.add(newTablewareItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO updateTablewareQuantity(Integer tableId, Integer quantity) {
|
|
|
|
|
+ log.info("更新餐具数量, tableId={}, quantity={}", tableId, quantity);
|
|
|
|
|
+
|
|
|
|
|
+ if (quantity == null || quantity < 0) {
|
|
|
|
|
+ throw new RuntimeException("餐具数量不能小于0");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (quantity == 0) {
|
|
|
|
|
+ // 数量为0时,删除餐具项
|
|
|
|
|
+ return removeItem(tableId, TABLEWARE_CUISINE_ID);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取门店ID和餐具单价
|
|
|
|
|
+ Integer storeId = cart.getStoreId();
|
|
|
|
|
+ if (storeId == null) {
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(tableId);
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ storeId = table.getStoreId();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal tablewareUnitPrice = getTablewareUnitPrice(storeId);
|
|
|
|
|
+
|
|
|
|
|
+ // 商铺未设置餐具费(单价为0或未配置)时,不往购物车加餐具;若已有餐具项则移除
|
|
|
|
|
+ if (tablewareUnitPrice == null || tablewareUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
|
|
+ log.info("门店未设置餐具费, storeId={},不添加餐具到购物车", storeId);
|
|
|
|
|
+ CartItemDTO existing = items.stream()
|
|
|
|
|
+ .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+ if (existing != null) {
|
|
|
|
|
+ items.remove(existing);
|
|
|
|
|
+ if (items.isEmpty()) {
|
|
|
|
|
+ cart.setTotalAmount(BigDecimal.ZERO);
|
|
|
|
|
+ cart.setTotalQuantity(0);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找餐具项
|
|
|
|
|
+ CartItemDTO tablewareItem = items.stream()
|
|
|
|
|
+ .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (tablewareItem == null) {
|
|
|
|
|
+ // 如果不存在餐具项,创建一个
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+ String userPhone = TokenUtil.getCurrentUserPhone();
|
|
|
|
|
+
|
|
|
|
|
+ tablewareItem = new CartItemDTO();
|
|
|
|
|
+ tablewareItem.setCuisineId(TABLEWARE_CUISINE_ID);
|
|
|
|
|
+ tablewareItem.setCuisineName(TABLEWARE_NAME);
|
|
|
|
|
+ tablewareItem.setCuisineType(0); // 0表示餐具
|
|
|
|
|
+ tablewareItem.setCuisineImage("");
|
|
|
|
|
+ tablewareItem.setUnitPrice(tablewareUnitPrice);
|
|
|
|
|
+ tablewareItem.setAddUserId(userId);
|
|
|
|
|
+ tablewareItem.setAddUserPhone(userPhone);
|
|
|
|
|
+ items.add(tablewareItem);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 下单后只能增不能减:已有已下单数量时,数量不能少于已下单数量
|
|
|
|
|
+ Integer lockedQuantity = tablewareItem.getLockedQuantity();
|
|
|
|
|
+ if (lockedQuantity != null && lockedQuantity > 0 && quantity < lockedQuantity) {
|
|
|
|
|
+ throw new RuntimeException("餐具数量不能少于已下单数量(" + lockedQuantity + ")");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果已存在,更新单价(可能门店修改了餐具费)
|
|
|
|
|
+ tablewareItem.setUnitPrice(tablewareUnitPrice);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新数量
|
|
|
|
|
+ tablewareItem.setQuantity(quantity);
|
|
|
|
|
+ tablewareItem.setSubtotalAmount(tablewareUnitPrice.multiply(BigDecimal.valueOf(quantity)));
|
|
|
|
|
+
|
|
|
|
|
+ applyRealtimeMenuPricing(cart);
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO lockCartItems(Integer tableId) {
|
|
|
|
|
+ log.info("锁定购物车商品数量(设置已下单数量), tableId={}", tableId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
|
|
+ log.warn("购物车为空,无需锁定, tableId={}", tableId);
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历所有商品,将当前数量设置为已下单数量
|
|
|
|
|
+ boolean hasChanges = false;
|
|
|
|
|
+ for (CartItemDTO item : items) {
|
|
|
|
|
+ Integer currentQuantity = item.getQuantity();
|
|
|
|
|
+ Integer lockedQuantity = item.getLockedQuantity();
|
|
|
|
|
+
|
|
|
|
|
+ if (currentQuantity != null && currentQuantity > 0) {
|
|
|
|
|
+ if (lockedQuantity == null || lockedQuantity == 0) {
|
|
|
|
|
+ // 如果还没有已下单数量,将当前数量设置为已下单数量
|
|
|
|
|
+ item.setLockedQuantity(currentQuantity);
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ log.info("设置商品已下单数量, cuisineId={}, orderedQuantity={}", item.getCuisineId(), currentQuantity);
|
|
|
|
|
+ } else if (currentQuantity > lockedQuantity) {
|
|
|
|
|
+ // 如果已有已下单数量,且当前数量大于已下单数量(再次下单的情况),将新增数量累加到已下单数量
|
|
|
|
|
+ Integer newLockedQuantity = lockedQuantity + (currentQuantity - lockedQuantity);
|
|
|
|
|
+ item.setLockedQuantity(newLockedQuantity);
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ log.info("更新商品已下单数量, cuisineId={}, oldOrderedQuantity={}, newOrderedQuantity={}",
|
|
|
|
|
+ item.getCuisineId(), lockedQuantity, newLockedQuantity);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有变化,保存购物车
|
|
|
|
|
+ if (hasChanges) {
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public CartDTO unlockCartItems(Integer tableId) {
|
|
|
|
|
+ log.info("解锁购物车商品数量(清除已下单数量), tableId={}", tableId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取购物车
|
|
|
|
|
+ CartDTO cart = getCart(tableId);
|
|
|
|
|
+ List<CartItemDTO> items = cart.getItems();
|
|
|
|
|
+
|
|
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
|
|
+ log.info("购物车为空,无需解锁, tableId={}", tableId);
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历所有商品,清除已下单数量(lockedQuantity)
|
|
|
|
|
+ boolean hasChanges = false;
|
|
|
|
|
+ for (CartItemDTO item : items) {
|
|
|
|
|
+ if (item.getLockedQuantity() != null && item.getLockedQuantity() > 0) {
|
|
|
|
|
+ // 清除已下单数量,允许重新下单
|
|
|
|
|
+ item.setLockedQuantity(null);
|
|
|
|
|
+ hasChanges = true;
|
|
|
|
|
+ log.info("清除商品已下单数量, cuisineId={}", item.getCuisineId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有变化,保存购物车
|
|
|
|
|
+ if (hasChanges) {
|
|
|
|
|
+ saveCart(cart);
|
|
|
|
|
+ log.info("解锁购物车商品数量完成, tableId={}", tableId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("购物车无需解锁, tableId={}", tableId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cart;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存购物车到Redis和数据库(优化后的双写策略)
|
|
|
|
|
+ * Redis同步写入(保证实时性),数据库异步批量写入(提高性能)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveCart(CartDTO cart) {
|
|
|
|
|
+ // 1. 同步保存到Redis(保证实时性)
|
|
|
|
|
+ saveCartToRedis(cart);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 异步保存到数据库(不阻塞主流程,提高性能)
|
|
|
|
|
+ CompletableFuture.runAsync(() -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ saveCartToDatabase(cart);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("异步保存购物车到数据库失败, tableId={}, error={}", cart.getTableId(), e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, CART_DB_WRITE_EXECUTOR);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存购物车到Redis
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveCartToRedis(CartDTO cart) {
|
|
|
|
|
+ String cartKey = CART_KEY_PREFIX + cart.getTableId();
|
|
|
|
|
+ String cartJson = JSON.toJSONString(cart);
|
|
|
|
|
+ baseRedisService.setString(cartKey, cartJson, (long) CART_EXPIRE_SECONDS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存购物车到数据库(优化后的批量操作版本)
|
|
|
|
|
+ * 使用批量逻辑删除和批量插入,提高性能
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveCartToDatabase(CartDTO cart) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ Integer userId = TokenUtil.getCurrentUserId();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 批量逻辑删除该桌号的所有购物车记录(使用 MyBatis-Plus 的 deleteBatchIds)
|
|
|
|
|
+ LambdaQueryWrapper<StoreCart> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ queryWrapper.eq(StoreCart::getTableId, cart.getTableId())
|
|
|
|
|
+ .eq(StoreCart::getDeleteFlag, 0);
|
|
|
|
|
+ StoreCartMenuFilters.applyGenericCart(queryWrapper);
|
|
|
|
|
+ List<StoreCart> existingCartList = storeCartMapper.selectList(queryWrapper);
|
|
|
|
|
+ if (existingCartList != null && !existingCartList.isEmpty()) {
|
|
|
|
|
+ List<Integer> cartIds = existingCartList.stream()
|
|
|
|
|
+ .map(StoreCart::getId)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
|
|
|
|
|
+ storeCartMapper.deleteBatchIds(cartIds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 批量插入新的购物车记录
|
|
|
|
|
+ if (cart.getItems() != null && !cart.getItems().isEmpty()) {
|
|
|
|
|
+ List<StoreCart> cartList = new ArrayList<>(cart.getItems().size());
|
|
|
|
|
+ for (CartItemDTO item : cart.getItems()) {
|
|
|
|
|
+ StoreCart storeCart = new StoreCart();
|
|
|
|
|
+ storeCart.setTableId(cart.getTableId());
|
|
|
|
|
+ storeCart.setStoreId(cart.getStoreId());
|
|
|
|
|
+ storeCart.setCuisineId(item.getCuisineId());
|
|
|
|
|
+ storeCart.setCuisineName(item.getCuisineName());
|
|
|
|
|
+ storeCart.setCuisineImage(item.getCuisineImage());
|
|
|
|
|
+ storeCart.setUnitPrice(item.getUnitPrice());
|
|
|
|
|
+ storeCart.setQuantity(item.getQuantity());
|
|
|
|
|
+ storeCart.setLockedQuantity(item.getLockedQuantity());
|
|
|
|
|
+ storeCart.setSubtotalAmount(item.getSubtotalAmount());
|
|
|
|
|
+ storeCart.setAddUserId(item.getAddUserId());
|
|
|
|
|
+ storeCart.setAddUserPhone(item.getAddUserPhone());
|
|
|
|
|
+ storeCart.setRemark(item.getRemark());
|
|
|
|
|
+ storeCart.setDeleteFlag(0);
|
|
|
|
|
+ storeCart.setMenuType(OrderMenuConstants.MENU_TYPE_GENERIC_PRICE);
|
|
|
|
|
+ storeCart.setCreatedTime(now);
|
|
|
|
|
+ storeCart.setCreatedUserId(userId);
|
|
|
|
|
+ storeCart.setUpdatedTime(now);
|
|
|
|
|
+ cartList.add(storeCart);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 批量插入(如果数量较少,直接循环插入;如果数量较多,可以考虑分批插入)
|
|
|
|
|
+ if (cartList.size() <= 50) {
|
|
|
|
|
+ // 小批量直接插入
|
|
|
|
|
+ for (StoreCart storeCart : cartList) {
|
|
|
|
|
+ storeCartMapper.insert(storeCart);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 大批量分批插入(每批50条)
|
|
|
|
|
+ int batchSize = 50;
|
|
|
|
|
+ for (int i = 0; i < cartList.size(); i += batchSize) {
|
|
|
|
|
+ int end = Math.min(i + batchSize, cartList.size());
|
|
|
|
|
+ List<StoreCart> batch = cartList.subList(i, end);
|
|
|
|
|
+ for (StoreCart storeCart : batch) {
|
|
|
|
|
+ storeCartMapper.insert(storeCart);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 更新桌号表的购物车统计
|
|
|
|
|
+ StoreTable table = storeTableMapper.selectById(cart.getTableId());
|
|
|
|
|
+ if (table != null) {
|
|
|
|
|
+ table.setCartItemCount(cart.getTotalQuantity());
|
|
|
|
|
+ table.setCartTotalAmount(cart.getTotalAmount());
|
|
|
|
|
+ storeTableMapper.updateById(table);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.debug("购物车数据已异步保存到数据库, tableId={}, itemCount={}",
|
|
|
|
|
+ cart.getTableId(), cart.getItems() != null ? cart.getItems().size() : 0);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("保存购物车到数据库失败, tableId={}, error={}", cart.getTableId(), e.getMessage(), e);
|
|
|
|
|
+ // 数据库保存失败不影响Redis,继续执行
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|