Просмотр исходного кода

Merge remote-tracking branch 'origin/sit-OrderFood' into sit-OrderFood

刘云鑫 1 месяц назад
Родитель
Сommit
62f43c32bc

+ 25 - 19
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -133,7 +133,7 @@ public class StoreOrderController {
         }
     }
 
-    @ApiOperation(value = "清空购物车", notes = "清空购物车中所有商品,并推送SSE和WebSocket消息")
+    @ApiOperation(value = "清空购物车", notes = "清空购物车中所有商品(保留餐具和已下单商品),并推送SSE和WebSocket消息")
     @DeleteMapping("/cart/clear")
     public R<CartDTO> clearCart(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
@@ -143,29 +143,18 @@ public class StoreOrderController {
                 return R.fail("用户未登录");
             }
             
-            // 清空购物车(会自动保留已下单的商品)
+            // 清空购物车(会自动保留已下单的商品和餐具
             cartService.clearCart(tableId);
             
-            // 创建空的购物车对象用于返回和推送
-            CartDTO emptyCart = new CartDTO();
-            emptyCart.setTableId(tableId);
-            emptyCart.setItems(java.util.Collections.emptyList());
-            emptyCart.setTotalAmount(java.math.BigDecimal.ZERO);
-            emptyCart.setTotalQuantity(0);
-            
-            // 查询桌号信息
-            StoreTable table = storeTableMapper.selectById(tableId);
-            if (table != null) {
-                emptyCart.setTableNumber(table.getTableNumber());
-                emptyCart.setStoreId(table.getStoreId());
-            }
+            // 获取清空后的购物车(包含保留的餐具和已下单商品)
+            CartDTO cart = cartService.getCart(tableId);
             
             // 推送购物车更新消息(SSE)
-            sseService.pushCartUpdate(tableId, emptyCart);
+            sseService.pushCartUpdate(tableId, cart);
             // 推送购物车更新消息(WebSocket)
-//            CartWebSocketProcess.pushCartUpdate(tableId, emptyCart);
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
             
-            return R.data(emptyCart);
+            return R.data(cart);
         } catch (Exception e) {
             log.error("清空购物车失败: {}", e.getMessage(), e);
             return R.fail("清空购物车失败: " + e.getMessage());
@@ -501,6 +490,23 @@ public class StoreOrderController {
         }
     }
 
+    @ApiOperation(value = "商家手动完成订单", notes = "供商家使用,手动点击完成订单。不校验订单是否处于已支付状态,直接将订单状态改为已完成。订单状态:0-待支付,1-已支付,2-已取消,3-已完成")
+    @PostMapping("/complete-by-merchant/{orderId}")
+    public R<Boolean> completeOrderByMerchant(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = orderService.completeOrderByMerchant(orderId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("商家手动完成订单失败: {}", e.getMessage(), e);
+            return R.fail("商家手动完成订单失败: " + e.getMessage());
+        }
+    }
+
     @ApiOperation(value = "更新订单优惠券", notes = "重新选择优惠券")
     @PostMapping("/update-coupon")
     public R<StoreOrder> updateOrderCoupon(@Valid @RequestBody shop.alien.entity.store.dto.UpdateOrderCouponDTO dto) {
@@ -518,7 +524,7 @@ public class StoreOrderController {
         }
     }
 
-    @ApiOperation(value = "管理员重置餐桌", notes = "管理员重置餐桌:删除购物车数据、订单数据,并重置餐桌表初始化")
+    @ApiOperation(value = "管理员重置餐桌", notes = "管理员重置餐桌:删除购物车数据、未支付/已取消的订单数据,并重置餐桌表初始化。已支付/已完成的订单会被保留,避免数据丢失")
     @PostMapping("/admin/reset-table/{tableId}")
     public R<Boolean> resetTable(@ApiParam(value = "餐桌ID", required = true) @PathVariable Integer tableId) {
         try {

+ 8 - 0
alien-dining/src/main/java/shop/alien/dining/service/CartService.java

@@ -110,4 +110,12 @@ public interface CartService {
      * @return 购物车信息
      */
     CartDTO lockCartItems(Integer tableId);
+
+    /**
+     * 解锁购物车商品数量(取消订单时调用,清除已下单数量,允许重新下单)
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO unlockCartItems(Integer tableId);
 }

+ 10 - 1
alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java

@@ -104,6 +104,14 @@ public interface StoreOrderService extends IService<StoreOrder> {
     boolean completeOrder(Integer orderId);
 
     /**
+     * 商家手动完成订单(不校验支付状态)
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean completeOrderByMerchant(Integer orderId);
+
+    /**
      * 更新订单优惠券
      *
      * @param orderId 订单ID
@@ -113,7 +121,8 @@ public interface StoreOrderService extends IService<StoreOrder> {
     StoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
 
     /**
-     * 管理员重置餐桌(删除购物车数据、订单数据,并重置餐桌表)
+     * 管理员重置餐桌(删除购物车数据、未支付/已取消的订单数据,并重置餐桌表)
+     * 注意:已支付/已完成的订单会被保留,避免数据丢失
      *
      * @param tableId 餐桌ID
      * @return 是否成功

+ 59 - 6
alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java

@@ -390,13 +390,20 @@ public class CartServiceImpl implements CartService {
             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) {
                 // 有已下单数量,保留该商品,但将当前数量恢复为已下单数量
@@ -418,6 +425,13 @@ public class CartServiceImpl implements CartService {
             }
         }
         
+        // 将餐具项添加到保留列表中
+        if (tablewareItem != null) {
+            orderedItems.add(tablewareItem);
+            orderedCuisineIds.add(tablewareItem.getCuisineId());
+            log.info("保留餐具项, cuisineId={}, quantity={}", tablewareItem.getCuisineId(), tablewareItem.getQuantity());
+        }
+        
         // 如果有变化(有未下单的商品需要删除,或者已下单商品数量需要恢复),进行更新
         if (hasChanges) {
             // 1. 更新购物车(删除未下单商品,已下单商品数量已恢复)
@@ -442,13 +456,15 @@ public class CartServiceImpl implements CartService {
                 saveCartToRedis(cart);
             }
             
-            // 2. 从数据库中逻辑删除未下单的商品
+            // 2. 从数据库中逻辑删除未下单的商品(排除餐具)
             if (!unorderedItems.isEmpty()) {
                 LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
                 wrapper.eq(StoreCart::getTableId, tableId);
                 wrapper.eq(StoreCart::getDeleteFlag, 0);
+                // 排除餐具(cuisineId = -1)
+                wrapper.ne(StoreCart::getCuisineId, TABLEWARE_CUISINE_ID);
                 if (!orderedCuisineIds.isEmpty()) {
-                    // 排除已下单的商品ID
+                    // 排除已下单的商品ID(包括餐具)
                     wrapper.notIn(StoreCart::getCuisineId, orderedCuisineIds);
                 }
                 List<StoreCart> cartListToDelete = storeCartMapper.selectList(wrapper);
@@ -458,7 +474,7 @@ public class CartServiceImpl implements CartService {
                             .collect(Collectors.toList());
                     // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
                     storeCartMapper.deleteBatchIds(cartIds);
-                    log.info("删除未下单商品, tableId={}, count={}", tableId, cartIds.size());
+                    log.info("删除未下单商品(已排除餐具), tableId={}, count={}", tableId, cartIds.size());
                 }
             }
             
@@ -476,7 +492,7 @@ public class CartServiceImpl implements CartService {
                 storeTableMapper.updateById(table);
             }
             
-            log.info("清空购物车完成(保留已下单商品,数量恢复为已下单数量), tableId={}, 删除商品数={}, 保留商品数={}", 
+            log.info("清空购物车完成(保留已下单商品和餐具,数量恢复为已下单数量), tableId={}, 删除商品数={}, 保留商品数={}", 
                     tableId, unorderedItems.size(), orderedItems.size());
         } else {
             log.info("购物车无需更新, tableId={}", tableId);
@@ -591,12 +607,14 @@ public class CartServiceImpl implements CartService {
         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(new Date());
+            usage.setCreatedTime(now);
+            usage.setUpdatedTime(now); // 设置更新时间,避免数据库约束错误
             Integer userId = TokenUtil.getCurrentUserId();
             if (userId != null) {
                 usage.setCreatedUserId(userId);
@@ -853,6 +871,41 @@ public class CartServiceImpl implements CartService {
         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同步写入(保证实时性),数据库异步批量写入(提高性能)

+ 32 - 17
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java

@@ -123,40 +123,55 @@ public class DiningUserServiceImpl implements DiningUserService {
 
     /**
      * 查找或创建用户
+     * 规则:只要 user_phone 存在,就代表存在账号,不创建新账号
      * 
      * @param openid 微信OpenID
      * @param phone 解析后的手机号(如果为null,说明没有提供phoneCode或解析失败)
      */
     private LifeUser findOrCreateUser(String openid, String phone) {
-        // 1. 通过 openid 查找用户(从 Redis 映射)
-        LifeUser user = findUserByOpenid(openid);
+        LifeUser user = null;
         
-        // 2. 如果找不到用户,尝试通过手机号查找
-        if (user == null && StringUtils.isNotBlank(phone)) {
+        // 1. 如果提供了手机号,优先通过手机号查找(只要手机号存在就代表账号存在)
+        if (StringUtils.isNotBlank(phone)) {
             user = findUserByPhone(phone);
             if (user != null) {
-                // 建立 openid 和 userId 的映射关系
+                // 账号已存在,建立 openid 和 userId 的映射关系
                 saveOpenidMapping(openid, user.getId());
+                log.info("通过手机号找到已存在账号: phone={}, userId={}, openid={}", 
+                        maskString(phone, 7), user.getId(), maskString(openid, 8));
+                // 如果用户已有手机号,但新解析的手机号不同,更新手机号
+                if (StringUtils.isNotBlank(user.getUserPhone()) && !user.getUserPhone().equals(phone)) {
+                    log.info("检测到手机号变更,更新手机号: userId={}, oldPhone={}, newPhone={}", 
+                            user.getId(), maskString(user.getUserPhone(), 7), maskString(phone, 7));
+                    updateUserPhone(user, phone);
+                }
+                return user;
             }
         }
-
-        // 3. 用户不存在则创建
-        if (user == null) {
-            user = createNewUser(openid, phone);
-            if (user != null) {
-                saveOpenidMapping(openid, user.getId());
-            }
-        } else {
-            // 4. 如果用户存在但没有手机号,且提供了解析后的手机号,则更新手机号
-            if (StringUtils.isBlank(user.getUserPhone()) && StringUtils.isNotBlank(phone)) {
+        
+        // 2. 如果通过手机号找不到(或未提供手机号),尝试通过 openid 查找
+        user = findUserByOpenid(openid);
+        
+        // 3. 如果通过 openid 找到了用户,且提供了手机号,更新手机号
+        if (user != null) {
+            if (StringUtils.isNotBlank(phone) && StringUtils.isBlank(user.getUserPhone())) {
+                // 用户存在但没有手机号,更新手机号
                 updateUserPhone(user, phone);
-            } else if (StringUtils.isNotBlank(user.getUserPhone()) && StringUtils.isNotBlank(phone) 
+            } else if (StringUtils.isNotBlank(phone) && StringUtils.isNotBlank(user.getUserPhone()) 
                     && !user.getUserPhone().equals(phone)) {
-                // 5. 如果用户已有手机号,但新解析的手机号不同,也更新(可选,根据业务需求决定)
+                // 用户已有手机号,但新解析的手机号不同,也更新
                 log.info("检测到手机号变更,更新手机号: userId={}, oldPhone={}, newPhone={}", 
                         user.getId(), maskString(user.getUserPhone(), 7), maskString(phone, 7));
                 updateUserPhone(user, phone);
             }
+            return user;
+        }
+
+        // 4. 如果都找不到,且未提供手机号(或手机号不存在),才创建新账号
+        // 注意:如果提供了手机号但找不到,说明这个手机号没有注册过,可以创建新账号
+        user = createNewUser(openid, phone);
+        if (user != null) {
+            saveOpenidMapping(openid, user.getId());
         }
 
         return user;

+ 514 - 113
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -43,6 +43,45 @@ import java.util.stream.Collectors;
 @Transactional(rollbackFor = Exception.class)
 public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOrder> implements StoreOrderService {
 
+    /**
+     * 金额验证误差阈值(0.01元)
+     */
+    private static final BigDecimal AMOUNT_TOLERANCE = new BigDecimal("0.01");
+
+    /**
+     * 订单金额信息封装类
+     */
+    private static class OrderAmountInfo {
+        private final BigDecimal totalAmount;      // 订单总金额(菜品总价)
+        private final BigDecimal tablewareFee;     // 餐具费
+        private final BigDecimal discountAmount;   // 优惠金额
+        private final BigDecimal payAmount;        // 实付金额
+
+        public OrderAmountInfo(BigDecimal totalAmount, BigDecimal tablewareFee, 
+                              BigDecimal discountAmount, BigDecimal payAmount) {
+            this.totalAmount = totalAmount;
+            this.tablewareFee = tablewareFee;
+            this.discountAmount = discountAmount;
+            this.payAmount = payAmount;
+        }
+
+        public BigDecimal getTotalAmount() {
+            return totalAmount;
+        }
+
+        public BigDecimal getTablewareFee() {
+            return tablewareFee;
+        }
+
+        public BigDecimal getDiscountAmount() {
+            return discountAmount;
+        }
+
+        public BigDecimal getPayAmount() {
+            return payAmount;
+        }
+    }
+
     private final StoreOrderDetailMapper orderDetailMapper;
     private final StoreTableMapper storeTableMapper;
     private final StoreTableLogMapper storeTableLogMapper;
@@ -63,11 +102,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         log.info("创建订单, dto={}", dto);
 
         // 获取当前用户信息
-        Integer userId = TokenUtil.getCurrentUserId();
-        if (userId == null) {
-            throw new RuntimeException("用户未登录");
-        }
-        String userPhone = TokenUtil.getCurrentUserPhone();
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
+        String userPhone = (String) userInfo[1];
 
         // 检查订单锁定状态
         Integer lockUserId = orderLockService.checkOrderLock(dto.getTableId());
@@ -118,16 +155,59 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             throw new RuntimeException("购物车中没有新增商品或商品数量未增加,无法创建订单");
         }
 
-        // 验证优惠券(可选,couponId 可以为 null,不选择优惠券时 discountAmount 为 0)
-        BigDecimal discountAmount = BigDecimal.ZERO;
+        // 验证并纠正前端传入的金额(订单总金额、餐具费、优惠金额、实付金额)
+        OrderAmountInfo amountInfo = validateAndCorrectOrderAmounts(dto, cart, storeInfo, table);
+        
+        // 使用验证后的金额
+        BigDecimal totalAmount = amountInfo.getTotalAmount();
+        BigDecimal tablewareFee = amountInfo.getTablewareFee();
+        BigDecimal discountAmount = amountInfo.getDiscountAmount();
+        BigDecimal payAmount = amountInfo.getPayAmount();
+        
+        // 验证优惠券(可选,couponId 可以为 null)
+        // 注意:订单总金额、优惠金额、实付金额都由前端计算并传入,后端验证其正确性
+        LifeDiscountCoupon coupon = null; // 缓存优惠券对象,避免重复查询
+        
         if (dto.getCouponId() != null) {
-            // 检查桌号是否已使用优惠券(考虑换桌情况)
+            // 如果使用了优惠券,必须传入优惠金额
+            if (discountAmount == null || discountAmount.compareTo(BigDecimal.ZERO) < 0) {
+                throw new RuntimeException("使用优惠券时必须传入优惠金额");
+            }
+            
+            // 检查桌号是否已使用优惠券,如果已使用则替换为新优惠券
             if (cartService.hasUsedCoupon(dto.getTableId())) {
-                throw new RuntimeException("该桌已使用优惠券,不能重复使用");
+                // 获取旧的优惠券使用记录
+                LambdaQueryWrapper<StoreCouponUsage> oldUsageWrapper = new LambdaQueryWrapper<>();
+                oldUsageWrapper.eq(StoreCouponUsage::getTableId, dto.getTableId());
+                oldUsageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+                oldUsageWrapper.in(StoreCouponUsage::getUsageStatus, 0, 1); // 已标记使用、已下单
+                oldUsageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+                oldUsageWrapper.last("LIMIT 1");
+                StoreCouponUsage oldUsage = storeCouponUsageMapper.selectOne(oldUsageWrapper);
+                
+                if (oldUsage != null) {
+                    // 如果旧优惠券已经关联到订单(已下单状态),需要将旧优惠券使用记录状态改为"已取消"
+                    if (oldUsage.getUsageStatus() == 1 && oldUsage.getOrderId() != null) {
+                        oldUsage.setUsageStatus(3); // 已取消
+                        oldUsage.setUpdatedTime(new Date());
+                        storeCouponUsageMapper.updateById(oldUsage);
+                        log.info("替换优惠券:取消旧优惠券使用记录, tableId={}, oldCouponId={}, newCouponId={}", 
+                                dto.getTableId(), oldUsage.getCouponId(), dto.getCouponId());
+                    } else {
+                        // 如果只是已标记使用但未下单,直接逻辑删除
+                        storeCouponUsageMapper.deleteById(oldUsage.getId());
+                        log.info("替换优惠券:删除未下单的旧优惠券使用记录, tableId={}, oldCouponId={}, newCouponId={}", 
+                                dto.getTableId(), oldUsage.getCouponId(), dto.getCouponId());
+                    }
+                }
+                
+                // 清除旧的优惠券使用标记
+                cartService.clearCouponUsed(dto.getTableId());
+                log.info("替换优惠券:清除旧优惠券标记, tableId={}, newCouponId={}", dto.getTableId(), dto.getCouponId());
             }
 
-            // 验证优惠券
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(dto.getCouponId());
+            // 验证优惠券(只查询一次)
+            coupon = lifeDiscountCouponMapper.selectById(dto.getCouponId());
             if (coupon == null) {
                 throw new RuntimeException("优惠券不存在");
             }
@@ -137,29 +217,47 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 throw new RuntimeException("优惠券不属于该门店");
             }
 
-            // 计算餐具费(从门店信息获取)
-            BigDecimal tablewareFee = calculateTablewareFee(storeInfo, dto.getDinerCount());
-
-            // 验证最低消费(菜品总价 + 餐具费)
-            BigDecimal totalWithTableware = cart.getTotalAmount().add(tablewareFee);
-            if (coupon.getMinimumSpendingAmount() != null
-                    && totalWithTableware.compareTo(coupon.getMinimumSpendingAmount()) < 0) {
-                throw new RuntimeException("订单金额未达到优惠券最低消费要求");
+            // 验证前端计算的优惠金额是否合理(防止前端计算错误)
+            BigDecimal totalWithTableware = totalAmount.add(tablewareFee);
+            BigDecimal expectedDiscountAmount = calculateDiscountAmount(coupon, totalWithTableware);
+            
+            // 保存前端传入的实付金额(优先使用前端传入的值)
+            BigDecimal frontendPayAmount = payAmount;
+            
+            // 验证并纠正优惠金额
+            discountAmount = validateAndCorrectAmount(
+                    discountAmount, 
+                    expectedDiscountAmount, 
+                    "优惠金额"
+            );
+            
+            // 如果优惠金额被纠正了,重新计算实付金额用于验证
+            // 但始终优先使用前端传入的实付金额
+            BigDecimal recalculatedPayAmount = totalAmount.add(tablewareFee).subtract(discountAmount);
+            if (frontendPayAmount != null) {
+                // 如果前端传入了实付金额,验证其是否正确(仅用于记录日志)
+                BigDecimal payAmountDifference = frontendPayAmount.subtract(recalculatedPayAmount).abs();
+                if (payAmountDifference.compareTo(AMOUNT_TOLERANCE) > 0) {
+                    log.warn("优惠金额纠正后,前端传入的实付金额与重新计算的不一致, frontend={}, recalculated={}, difference={},将使用前端传入的值", 
+                            frontendPayAmount, recalculatedPayAmount, payAmountDifference);
+                }
+                // 始终使用前端传入的值
+                payAmount = frontendPayAmount;
+            } else {
+                // 如果前端没有传入实付金额,使用重新计算的值
+                payAmount = recalculatedPayAmount;
             }
 
-            // 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
-            discountAmount = calculateDiscountAmount(coupon, totalWithTableware);
-
-            // 标记桌号已使用优惠券
+            // 标记桌号已使用新优惠券
             cartService.markCouponUsed(dto.getTableId(), dto.getCouponId());
+        } else {
+            // 如果没有使用优惠券,优惠金额应该为0
+            if (discountAmount != null && discountAmount.compareTo(BigDecimal.ZERO) != 0) {
+                throw new RuntimeException("未使用优惠券时,优惠金额必须为0");
+            }
+            discountAmount = BigDecimal.ZERO;
         }
 
-        // 计算餐具费(从门店信息获取)
-        BigDecimal tablewareFee = calculateTablewareFee(storeInfo, dto.getDinerCount());
-
-        // 计算实付金额(菜品总价 + 餐具费 - 优惠金额)
-        BigDecimal payAmount = cart.getTotalAmount().add(tablewareFee).subtract(discountAmount);
-
         Date now = new Date();
         StoreOrder order = null;
         String orderNo = null;
@@ -186,15 +284,15 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 Integer addDishCountBefore = orderDetailMapper.selectCount(checkBeforeAddDishWrapper);
                 isFirstAddDish = (addDishCountBefore == null || addDishCountBefore == 0);
                 
-                // 更新订单信息
+                // 更新订单信息(使用前端计算的金额,但经过后端验证)
                 order.setDinerCount(dto.getDinerCount());
                 order.setContactPhone(dto.getContactPhone());
                 order.setTablewareFee(tablewareFee);
-                order.setTotalAmount(cart.getTotalAmount());
+                order.setTotalAmount(totalAmount); // 使用验证后的订单总金额
                 order.setCouponId(dto.getCouponId());
                 order.setCurrentCouponId(dto.getCouponId());
-                order.setDiscountAmount(discountAmount);
-                order.setPayAmount(payAmount);
+                order.setDiscountAmount(discountAmount); // 使用验证后的优惠金额
+                order.setPayAmount(payAmount); // 使用验证后的实付金额
                 order.setRemark(dto.getRemark());
                 order.setUpdatedUserId(userId);
                 order.setUpdatedTime(now);
@@ -234,11 +332,11 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             order.setPayUserId(userId);
             order.setPayUserPhone(userPhone);
             order.setOrderStatus(0); // 待支付
-            order.setTotalAmount(cart.getTotalAmount());
+            order.setTotalAmount(totalAmount); // 使用验证后的订单总金额
             order.setCouponId(dto.getCouponId());
             order.setCurrentCouponId(dto.getCouponId()); // 记录当前使用的优惠券
-            order.setDiscountAmount(discountAmount);
-            order.setPayAmount(payAmount);
+            order.setDiscountAmount(discountAmount); // 使用验证后的优惠金额
+            order.setPayAmount(payAmount); // 使用验证后的实付金额
             
             // 如果immediatePay为0,只创建订单不支付;为1则创建订单并支付
             // 暂时不实现立即支付,由前端调用支付接口
@@ -278,13 +376,33 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 usage.setUpdatedTime(new Date());
                 storeCouponUsageMapper.updateById(usage);
             }
-        } else if (isUpdate) {
-            // 如果是更新订单且没有使用优惠券,需要清除之前的优惠券使用记录
-            // 这里暂时不处理,因为可能用户只是想更新订单信息,不想改变优惠券
+        }
+        
+        // 如果是更新订单,且优惠券发生变化,需要处理旧优惠券的使用记录
+        if (isUpdate && finalOrder.getCouponId() != null && 
+            (dto.getCouponId() == null || !finalOrder.getCouponId().equals(dto.getCouponId()))) {
+            // 订单的优惠券被替换或取消,需要将旧优惠券使用记录状态改为"已取消"
+            LambdaQueryWrapper<StoreCouponUsage> oldUsageWrapper = new LambdaQueryWrapper<>();
+            oldUsageWrapper.eq(StoreCouponUsage::getOrderId, finalOrder.getId());
+            oldUsageWrapper.eq(StoreCouponUsage::getCouponId, finalOrder.getCouponId());
+            oldUsageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            oldUsageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            oldUsageWrapper.last("LIMIT 1");
+            StoreCouponUsage oldUsage = storeCouponUsageMapper.selectOne(oldUsageWrapper);
+            if (oldUsage != null) {
+                oldUsage.setUsageStatus(3); // 已取消
+                oldUsage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(oldUsage);
+                log.info("更新订单时替换优惠券:取消旧优惠券使用记录, orderId={}, oldCouponId={}, newCouponId={}", 
+                        finalOrder.getId(), finalOrder.getCouponId(), dto.getCouponId());
+            }
         }
 
         // 创建订单明细
         // 如果是更新订单,需要判断哪些商品是新增的或数量增加的,标记为加餐
+        // 餐具的特殊ID(用于标识餐具项)
+        final Integer TABLEWARE_CUISINE_ID = -1;
+        
         List<StoreOrderDetail> orderDetails = cart.getItems().stream()
                 .map(item -> {
                     StoreOrderDetail detail = new StoreOrderDetail();
@@ -292,7 +410,30 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                     detail.setOrderNo(finalOrderNo);
                     detail.setCuisineId(item.getCuisineId());
                     detail.setCuisineName(item.getCuisineName());
-                    detail.setCuisineType(item.getCuisineType());
+                    
+                    // 设置菜品类型:如果为null,根据是否为餐具设置默认值
+                    Integer cuisineType = item.getCuisineType();
+                    if (cuisineType == null) {
+                        // 如果是餐具,设置为0;否则设置为1(默认单品)
+                        if (TABLEWARE_CUISINE_ID.equals(item.getCuisineId())) {
+                            cuisineType = 0; // 0表示餐具
+                        } else {
+                            // 尝试从菜品信息中获取,如果获取不到则默认为1(单品)
+                            try {
+                                StoreCuisine cuisine = storeCuisineMapper.selectById(item.getCuisineId());
+                                if (cuisine != null && cuisine.getCuisineType() != null) {
+                                    cuisineType = cuisine.getCuisineType();
+                                } else {
+                                    cuisineType = 1; // 默认为单品
+                                }
+                            } catch (Exception e) {
+                                log.warn("获取菜品类型失败, cuisineId={}, 使用默认值1", item.getCuisineId(), e);
+                                cuisineType = 1; // 默认为单品
+                            }
+                        }
+                    }
+                    detail.setCuisineType(cuisineType);
+                    
                     detail.setCuisineImage(item.getCuisineImage());
                     detail.setUnitPrice(item.getUnitPrice());
                     detail.setQuantity(item.getQuantity());
@@ -371,10 +512,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         log.info("支付订单, orderId={}, payType={}", orderId, payType);
 
         // 获取当前用户信息
-        Integer userId = TokenUtil.getCurrentUserId();
-        if (userId == null) {
-            throw new RuntimeException("用户未登录");
-        }
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
 
         // 检查结算锁定状态
         Integer lockUserId = orderLockService.checkSettlementLock(orderId);
@@ -382,16 +521,38 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             throw new RuntimeException("订单结算已被其他用户锁定,无法支付");
         }
 
-        StoreOrder order = this.getById(orderId);
-        if (order == null) {
-            throw new RuntimeException("订单不存在");
-        }
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "支付");
 
-        if (order.getOrderStatus() != 0) {
-            throw new RuntimeException("订单状态不正确,无法支付");
+        // 支付时重新计算优惠券金额和实付金额(此时订单金额已确定,不会再变化)
+        BigDecimal discountAmount = BigDecimal.ZERO;
+        BigDecimal tablewareFee = order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO;
+        BigDecimal totalAmount = order.getTotalAmount() != null ? order.getTotalAmount() : BigDecimal.ZERO;
+        BigDecimal totalWithTableware = totalAmount.add(tablewareFee);
+        
+        if (order.getCouponId() != null) {
+            // 查询优惠券信息
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            if (coupon == null) {
+                throw new RuntimeException("优惠券不存在");
+            }
+            
+            // 验证最低消费(菜品总价 + 餐具费)
+            if (coupon.getMinimumSpendingAmount() != null
+                    && totalWithTableware.compareTo(coupon.getMinimumSpendingAmount()) < 0) {
+                throw new RuntimeException("订单金额未达到优惠券最低消费要求(" + coupon.getMinimumSpendingAmount() + "元)");
+            }
+            
+            // 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
+            discountAmount = calculateDiscountAmount(coupon, totalWithTableware);
         }
-
-        // 这里可以调用支付接口,暂时直接更新为已支付
+        
+        // 计算实付金额(菜品总价 + 餐具费 - 优惠金额)
+        BigDecimal payAmount = totalWithTableware.subtract(discountAmount);
+        
+        // 更新订单信息(包括优惠金额和实付金额)
+        order.setDiscountAmount(discountAmount);
+        order.setPayAmount(payAmount);
         order.setOrderStatus(1); // 已支付
         order.setPayStatus(1); // 已支付
         order.setPayType(payType);
@@ -418,8 +579,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             }
         }
 
-        // 支付完成后,自动重置餐桌(保留订单数据,只重置餐桌绑定关系和购物车
-        // resetTableAfterPayment 方法会完全清空购物车,所以不需要单独调用 clearCart
+        // 支付完成后,清空购物车和重置餐桌状态(保留订单数据,不删除订单
+        // resetTableAfterPayment 方法会清空购物车和重置餐桌状态,但不会删除订单数据
         resetTableAfterPayment(order.getTableId());
 
         // 支付订单成功后,自动解锁结算锁定
@@ -439,14 +600,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     public boolean cancelOrder(Integer orderId) {
         log.info("取消订单, orderId={}", orderId);
 
-        StoreOrder order = this.getById(orderId);
-        if (order == null) {
-            throw new RuntimeException("订单不存在");
-        }
-
-        if (order.getOrderStatus() != 0) {
-            throw new RuntimeException("订单状态不正确,无法取消");
-        }
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "取消");
 
         order.setOrderStatus(2); // 已取消
         order.setUpdatedTime(new Date());
@@ -459,17 +614,27 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
 
         this.updateById(order);
 
-        // 更新桌号状态
+        // 恢复购物车的已下单数量(允许重新下单)
+        cartService.unlockCartItems(order.getTableId());
+
+        // 清除优惠券使用标记
+        cartService.clearCouponUsed(order.getTableId());
+
+        // 更新桌号状态(检查购物车是否为空,如果为空则设为空闲)
         StoreTable table = storeTableMapper.selectById(order.getTableId());
         if (table != null) {
             table.setCurrentOrderId(null);
-            table.setStatus(0); // 空闲
+            // 检查购物车是否为空,如果为空则设为空闲
+            CartDTO cart = cartService.getCart(order.getTableId());
+            if (cart.getItems() == null || cart.getItems().isEmpty()) {
+                table.setStatus(0); // 空闲
+            } else {
+                // 购物车还有商品,保持当前状态或设为就餐中
+                table.setStatus(1); // 就餐中
+            }
             storeTableMapper.updateById(table);
         }
 
-        // 清除优惠券使用标记
-        cartService.clearCouponUsed(order.getTableId());
-
         log.info("订单取消成功, orderId={}", orderId);
         return true;
     }
@@ -676,15 +841,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     public StoreOrder addDishToOrder(Integer orderId, Integer cuisineId, Integer quantity, String remark) {
         log.info("加餐, orderId={}, cuisineId={}, quantity={}", orderId, cuisineId, quantity);
 
-        // 验证订单
-        StoreOrder order = this.getById(orderId);
-        if (order == null) {
-            throw new RuntimeException("订单不存在");
-        }
-
-        if (order.getOrderStatus() != 0) {
-            throw new RuntimeException("订单状态不正确,无法加餐");
-        }
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "加餐");
 
         // 验证菜品
         StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
@@ -696,8 +854,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         }
 
         // 获取当前用户信息
-        Integer userId = TokenUtil.getCurrentUserId();
-        String userPhone = TokenUtil.getCurrentUserPhone();
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
+        String userPhone = (String) userInfo[1];
 
         // 在加餐之前,检查订单明细中是否已经有 is_add_dish=1 的记录
         // 如果没有,说明这是首次加餐(首次订单发生变化)
@@ -717,10 +876,13 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
 
         Date now = new Date();
         Integer quantityBefore = 0; // 加餐前的数量(用于记录变更日志)
+        BigDecimal addAmount; // 新增的金额
         
         if (existingDetail != null) {
             // 记录加餐前的数量
             quantityBefore = existingDetail.getQuantity();
+            // 计算新增金额(单价 * 新增数量)
+            addAmount = existingDetail.getUnitPrice().multiply(BigDecimal.valueOf(quantity));
             // 更新数量
             existingDetail.setQuantity(existingDetail.getQuantity() + quantity);
             existingDetail.setSubtotalAmount(existingDetail.getUnitPrice()
@@ -739,40 +901,44 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         } else {
             // 添加新菜品(加餐)
             quantityBefore = 0; // 新菜品,加餐前数量为0
+            // 计算新增金额(单价 * 数量)
+            addAmount = cuisine.getTotalPrice().multiply(BigDecimal.valueOf(quantity));
             StoreOrderDetail detail = new StoreOrderDetail();
             detail.setOrderId(orderId);
             detail.setOrderNo(order.getOrderNo());
             detail.setCuisineId(cuisine.getId());
             detail.setCuisineName(cuisine.getName());
-            detail.setCuisineType(cuisine.getCuisineType());
+            
+            // 设置菜品类型:如果为null,默认为1(单品)
+            Integer cuisineType = cuisine.getCuisineType();
+            if (cuisineType == null) {
+                cuisineType = 1; // 默认为单品
+                log.warn("菜品类型为null,使用默认值1, cuisineId={}", cuisine.getId());
+            }
+            detail.setCuisineType(cuisineType);
+            
             detail.setCuisineImage(cuisine.getImages());
             detail.setUnitPrice(cuisine.getTotalPrice());
             detail.setQuantity(quantity);
-            detail.setSubtotalAmount(cuisine.getTotalPrice().multiply(BigDecimal.valueOf(quantity)));
+            detail.setSubtotalAmount(addAmount);
             detail.setAddUserId(userId);
             detail.setAddUserPhone(userPhone);
             detail.setIsAddDish(1); // 标记为加餐
             detail.setAddDishTime(now);
             detail.setRemark(remark);
             detail.setCreatedUserId(userId);
+            detail.setCreatedTime(now);
+            detail.setUpdatedTime(now);
             orderDetailMapper.insert(detail);
         }
 
-        // 重新计算订单总金额
-        LambdaQueryWrapper<StoreOrderDetail> allDetailWrapper = new LambdaQueryWrapper<>();
-        allDetailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
-        allDetailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
-        List<StoreOrderDetail> allDetails = orderDetailMapper.selectList(allDetailWrapper);
-        BigDecimal newTotalAmount = allDetails.stream()
-                .map(StoreOrderDetail::getSubtotalAmount)
-                .reduce(BigDecimal.ZERO, BigDecimal::add);
-
-        // 重新计算实付金额(菜品总价 + 餐具费 - 优惠金额)
-        BigDecimal newPayAmount = newTotalAmount.add(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO)
-                .subtract(order.getDiscountAmount() != null ? order.getDiscountAmount() : BigDecimal.ZERO);
+        // 增量计算订单总金额(避免重新查询所有订单明细)
+        BigDecimal newTotalAmount = order.getTotalAmount().add(addAmount);
 
+        // 加餐时只更新订单总金额,不计算优惠金额和实付金额(因为可能还会继续加菜,支付时再统一计算)
+        // 如果订单使用了优惠券,不验证最低消费(因为可能还会继续加菜,支付时再验证)
         order.setTotalAmount(newTotalAmount);
-        order.setPayAmount(newPayAmount);
+        // order.setDiscountAmount 和 order.setPayAmount 保持不变,支付时再计算
         order.setUpdatedTime(new Date());
         order.setUpdatedUserId(userId);
         this.updateById(order);
@@ -856,18 +1022,45 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     }
 
     @Override
-    public StoreOrder updateOrderCoupon(Integer orderId, Integer couponId) {
-        log.info("更新订单优惠券, orderId={}, couponId={}", orderId, couponId);
+    public boolean completeOrderByMerchant(Integer orderId) {
+        log.info("商家手动完成订单, orderId={}", orderId);
 
         StoreOrder order = this.getById(orderId);
         if (order == null) {
             throw new RuntimeException("订单不存在");
         }
 
-        if (order.getOrderStatus() != 0) {
-            throw new RuntimeException("订单状态不正确,无法修改优惠券");
+        // 商家手动完成订单,不校验支付状态,直接设置为已完成
+        order.setOrderStatus(3); // 已完成
+        order.setUpdatedTime(new Date());
+
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
+        }
+
+        this.updateById(order);
+
+        // 更新桌号状态
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            table.setStatus(0); // 空闲
+            storeTableMapper.updateById(table);
         }
 
+        log.info("商家手动完成订单成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public StoreOrder updateOrderCoupon(Integer orderId, Integer couponId) {
+        log.info("更新订单优惠券, orderId={}, couponId={}", orderId, couponId);
+
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "修改优惠券");
+
         BigDecimal discountAmount = BigDecimal.ZERO;
 
         if (couponId != null) {
@@ -889,16 +1082,15 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 throw new RuntimeException("订单金额未达到优惠券最低消费要求");
             }
 
-            // 计算优惠金额
-            discountAmount = coupon.getNominalValue();
-            BigDecimal totalAmount = totalWithTableware;
-            if (discountAmount.compareTo(totalAmount) > 0) {
-                discountAmount = totalAmount;
-            }
+            // 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
+            discountAmount = calculateDiscountAmount(coupon, totalWithTableware);
         }
 
-        // 如果之前有优惠券,更新使用记录状态为已取消
+        // 如果之前有优惠券,且优惠券发生变化(包括取消优惠券),需要处理旧优惠券使用记录
         if (order.getCouponId() != null && (couponId == null || !order.getCouponId().equals(couponId))) {
+            // 清除旧优惠券的使用标记(如果取消优惠券或更换优惠券)
+            cartService.clearCouponUsed(order.getTableId());
+            
             LambdaQueryWrapper<StoreCouponUsage> oldUsageWrapper = new LambdaQueryWrapper<>();
             oldUsageWrapper.eq(StoreCouponUsage::getOrderId, orderId);
             oldUsageWrapper.eq(StoreCouponUsage::getCouponId, order.getCouponId());
@@ -913,8 +1105,12 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             }
         }
 
-        // 如果新设置了优惠券,更新或创建使用记录
+        // 如果新设置了优惠券,需要标记桌号已使用优惠券,并更新或创建使用记录
         if (couponId != null) {
+            // 检查桌号是否已使用优惠券(如果更换优惠券,之前的标记已清除)
+            if (!cartService.hasUsedCoupon(order.getTableId())) {
+                cartService.markCouponUsed(order.getTableId(), couponId);
+            }
             LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
             usageWrapper.eq(StoreCouponUsage::getTableId, order.getTableId());
             usageWrapper.eq(StoreCouponUsage::getCouponId, couponId);
@@ -932,6 +1128,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                 // 创建新的使用记录
                 StoreTable table = storeTableMapper.selectById(order.getTableId());
                 if (table != null) {
+                    Date now = new Date();
                     StoreCouponUsage newUsage = new StoreCouponUsage();
                     newUsage.setTableId(order.getTableId());
                     newUsage.setStoreId(order.getStoreId());
@@ -939,7 +1136,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                     newUsage.setCouponId(couponId);
                     newUsage.setDiscountAmount(discountAmount);
                     newUsage.setUsageStatus(1); // 已下单
-                    newUsage.setCreatedTime(new Date());
+                    newUsage.setCreatedTime(now);
+                    newUsage.setUpdatedTime(now); // 设置更新时间,避免数据库约束错误
                     Integer userId = TokenUtil.getCurrentUserId();
                     if (userId != null) {
                         newUsage.setCreatedUserId(userId);
@@ -995,13 +1193,17 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             log.info("删除购物车数据, tableId={}, count={}", tableId, cartList.size());
         }
         
-        // 2. 删除订单数据(逻辑删除,包括订单明细)
+        // 2. 只删除未支付或已取消的订单(保留已支付/已完成的订单,避免数据丢失)
+        // 订单状态:0-待支付,1-已支付,2-已取消,3-已完成
         LambdaQueryWrapper<StoreOrder> orderWrapper = new LambdaQueryWrapper<>();
         orderWrapper.eq(StoreOrder::getTableId, tableId);
         orderWrapper.eq(StoreOrder::getDeleteFlag, 0);
+        // 只查询未支付(0)或已取消(2)的订单
+        orderWrapper.in(StoreOrder::getOrderStatus, 0, 2);
         List<StoreOrder> orderList = this.list(orderWrapper);
+        List<Integer> orderIds = new ArrayList<>();
         if (orderList != null && !orderList.isEmpty()) {
-            List<Integer> orderIds = orderList.stream()
+            orderIds = orderList.stream()
                     .map(StoreOrder::getId)
                     .collect(Collectors.toList());
             
@@ -1040,19 +1242,80 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             // 删除订单(逻辑删除,使用 MyBatis-Plus 的 removeByIds)
             // 使用 removeByIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
             this.removeByIds(orderIds);
-            log.info("删除订单数据, tableId={}, count={}", tableId, orderList.size());
+            log.info("删除未支付/已取消订单数据, tableId={}, count={}", tableId, orderList.size());
         }
         
-        // 3. 清空Redis中的购物车缓存
+        // 查询该桌号的所有订单(包括已支付/已完成的),用于后续处理锁定记录
+        LambdaQueryWrapper<StoreOrder> allOrderWrapper = new LambdaQueryWrapper<>();
+        allOrderWrapper.eq(StoreOrder::getTableId, tableId);
+        allOrderWrapper.eq(StoreOrder::getDeleteFlag, 0);
+        List<StoreOrder> allOrderList = this.list(allOrderWrapper);
+        List<Integer> allOrderIds = new ArrayList<>();
+        if (allOrderList != null && !allOrderList.isEmpty()) {
+            allOrderIds = allOrderList.stream()
+                    .map(StoreOrder::getId)
+                    .collect(Collectors.toList());
+        }
+        
+        // 3. 只删除未下单的优惠券使用记录(保留已下单/已支付的记录,避免数据丢失)
+        // usageStatus: 0-已标记使用, 1-已下单, 2-已支付, 3-已取消
+        LambdaQueryWrapper<StoreCouponUsage> couponUsageWrapper = new LambdaQueryWrapper<>();
+        couponUsageWrapper.eq(StoreCouponUsage::getTableId, tableId);
+        couponUsageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+        // 只删除已标记使用但未下单的记录(usageStatus=0)
+        couponUsageWrapper.eq(StoreCouponUsage::getUsageStatus, 0);
+        List<StoreCouponUsage> couponUsageList = storeCouponUsageMapper.selectList(couponUsageWrapper);
+        if (couponUsageList != null && !couponUsageList.isEmpty()) {
+            List<Integer> couponUsageIds = couponUsageList.stream()
+                    .map(StoreCouponUsage::getId)
+                    .collect(Collectors.toList());
+            // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            storeCouponUsageMapper.deleteBatchIds(couponUsageIds);
+            log.info("删除未下单的优惠券使用记录, tableId={}, count={}", tableId, couponUsageList.size());
+        }
+        
+        // 4. 删除该桌号的所有订单锁定记录(逻辑删除)
+        // 包括下单锁定(lock_type=1,通过 tableId 查找)和结算锁定(lock_type=2,通过订单ID关联)
+        
+        // 删除下单锁定(通过 tableId 查找)
+        LambdaQueryWrapper<StoreOrderLock> tableLockWrapper = new LambdaQueryWrapper<>();
+        tableLockWrapper.eq(StoreOrderLock::getTableId, tableId);
+        tableLockWrapper.eq(StoreOrderLock::getDeleteFlag, 0);
+        List<StoreOrderLock> tableLockList = storeOrderLockMapper.selectList(tableLockWrapper);
+        if (tableLockList != null && !tableLockList.isEmpty()) {
+            List<Integer> tableLockIds = tableLockList.stream()
+                    .map(StoreOrderLock::getId)
+                    .collect(Collectors.toList());
+            storeOrderLockMapper.deleteBatchIds(tableLockIds);
+            log.info("删除下单锁定记录, tableId={}, count={}", tableId, tableLockList.size());
+        }
+        
+        // 删除结算锁定(通过 orderId 查找,如果有订单的话)
+        // 注意:这里使用 allOrderIds,包括所有订单(已删除和未删除的),因为锁定记录可能关联已支付的订单
+        if (!allOrderIds.isEmpty()) {
+            LambdaQueryWrapper<StoreOrderLock> orderLockWrapper = new LambdaQueryWrapper<>();
+            orderLockWrapper.in(StoreOrderLock::getOrderId, allOrderIds);
+            orderLockWrapper.eq(StoreOrderLock::getDeleteFlag, 0);
+            List<StoreOrderLock> orderLockList = storeOrderLockMapper.selectList(orderLockWrapper);
+            if (orderLockList != null && !orderLockList.isEmpty()) {
+                List<Integer> orderLockIds = orderLockList.stream()
+                        .map(StoreOrderLock::getId)
+                        .collect(Collectors.toList());
+                storeOrderLockMapper.deleteBatchIds(orderLockIds);
+                log.info("删除结算锁定记录, orderIds={}, count={}", allOrderIds, orderLockList.size());
+            }
+        }
+        
+        // 5. 清空Redis中的购物车缓存
         String cartKey = "cart:table:" + tableId;
         baseRedisService.delete(cartKey);
         log.info("清空Redis购物车缓存, tableId={}", tableId);
         
-        // 4. 清除优惠券使用标记
+        // 6. 清除优惠券使用标记(Redis中的标记)
         cartService.clearCouponUsed(tableId);
         log.info("清除优惠券使用标记, tableId={}", tableId);
         
-        // 5. 重置餐桌表(使用 LambdaUpdateWrapper 来显式设置 null 值)
+        // 7. 重置餐桌表(使用 LambdaUpdateWrapper 来显式设置 null 值)
         LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.eq(StoreTable::getId, tableId)
                 .set(StoreTable::getCurrentOrderId, null)  // 显式设置 null
@@ -1278,10 +1541,18 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
 
         // 4. 查询优惠券信息(如果有)
         String couponName = null;
+        Integer couponType = null;
+        BigDecimal discountRate = null;
+        BigDecimal nominalValue = null;
+        BigDecimal minimumSpendingAmount = null;
         if (order.getCouponId() != null) {
             LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
             if (coupon != null) {
                 couponName = coupon.getName();
+                couponType = coupon.getCouponType();
+                discountRate = coupon.getDiscountRate();
+                nominalValue = coupon.getNominalValue();
+                minimumSpendingAmount = coupon.getMinimumSpendingAmount();
             }
         }
 
@@ -1305,6 +1576,10 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         vo.setTablewareFee(order.getTablewareFee());
         vo.setCouponId(order.getCouponId());
         vo.setCouponName(couponName);
+        vo.setCouponType(couponType);
+        vo.setDiscountRate(discountRate);
+        vo.setNominalValue(nominalValue);
+        vo.setMinimumSpendingAmount(minimumSpendingAmount);
         vo.setDiscountAmount(order.getDiscountAmount());
         vo.setPayAmount(order.getPayAmount());
         vo.setOrderStatus(order.getOrderStatus());
@@ -1561,8 +1836,10 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             changeLogs.add(changeLog);
         }
 
-        // 批量插入变更记录
+        // 批量插入变更记录(使用 MyBatis-Plus 的批量插入)
         if (!changeLogs.isEmpty()) {
+            // 使用循环插入(MyBatis-Plus 的 BaseMapper 没有批量插入方法,需要手动实现或使用 ServiceImpl 的 saveBatch)
+            // 注意:如果变更记录数量很大,可以考虑使用 MyBatis 的批量插入
             for (StoreOrderChangeLog log : changeLogs) {
                 orderChangeLogMapper.insert(log);
             }
@@ -1762,6 +2039,39 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     }
 
     /**
+     * 验证订单是否存在且状态正确(用于支付、取消、加餐、更新优惠券等操作)
+     *
+     * @param orderId      订单ID
+     * @param expectedStatus 期望的订单状态(null表示不校验状态)
+     * @param operation    操作名称(用于错误提示)
+     * @return 订单对象
+     */
+    private StoreOrder validateOrderForOperation(Integer orderId, Integer expectedStatus, String operation) {
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+        if (expectedStatus != null && order.getOrderStatus() != expectedStatus) {
+            throw new RuntimeException("订单状态不正确,无法" + operation);
+        }
+        return order;
+    }
+
+    /**
+     * 获取当前登录用户信息
+     *
+     * @return 用户ID和手机号的数组 [userId, userPhone]
+     */
+    private Object[] getCurrentUserInfo() {
+        Integer userId = TokenUtil.getCurrentUserId();
+        String userPhone = TokenUtil.getCurrentUserPhone();
+        if (userId == null) {
+            throw new RuntimeException("用户未登录");
+        }
+        return new Object[]{userId, userPhone};
+    }
+
+    /**
      * 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
      *
      * @param coupon           优惠券对象
@@ -1800,4 +2110,95 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
 
         return discountAmount;
     }
+
+    /**
+     * 验证并纠正前端传入的订单金额
+     * 包括:订单总金额、餐具费、优惠金额、实付金额
+     *
+     * @param dto       创建订单DTO
+     * @param cart      购物车信息
+     * @param storeInfo 门店信息
+     * @param table     餐桌信息
+     * @return 验证并纠正后的订单金额信息
+     */
+    private OrderAmountInfo validateAndCorrectOrderAmounts(
+            CreateOrderDTO dto, 
+            CartDTO cart, 
+            StoreInfo storeInfo, 
+            StoreTable table) {
+        
+        // 1. 获取前端传入的金额(如果为null则使用默认值)
+        BigDecimal frontendTotalAmount = dto.getTotalAmount() != null 
+                ? dto.getTotalAmount() 
+                : cart.getTotalAmount();
+        BigDecimal frontendTablewareFee = dto.getTablewareFee() != null 
+                ? dto.getTablewareFee() 
+                : BigDecimal.ZERO;
+        BigDecimal frontendDiscountAmount = dto.getDiscountAmount() != null 
+                ? dto.getDiscountAmount() 
+                : BigDecimal.ZERO;
+        BigDecimal frontendPayAmount = dto.getPayAmount();
+
+        // 2. 计算后端验证值
+        BigDecimal backendTotalAmount = cart.getTotalAmount();
+        BigDecimal backendTablewareFee = calculateTablewareFee(storeInfo, dto.getDinerCount());
+
+        // 3. 验证并纠正订单总金额
+        BigDecimal totalAmount = validateAndCorrectAmount(
+                frontendTotalAmount,
+                backendTotalAmount,
+                "订单总金额"
+        );
+
+        // 4. 验证并纠正餐具费
+        BigDecimal tablewareFee = validateAndCorrectAmount(
+                frontendTablewareFee,
+                backendTablewareFee,
+                "餐具费"
+        );
+
+        // 5. 计算期望的实付金额(订单总金额 + 餐具费 - 优惠金额)
+        BigDecimal expectedPayAmount = totalAmount.add(tablewareFee).subtract(frontendDiscountAmount);
+
+        // 6. 验证并纠正实付金额
+        BigDecimal payAmount = frontendPayAmount != null
+                ? validateAndCorrectAmount(frontendPayAmount, expectedPayAmount, "实付金额")
+                : expectedPayAmount;
+
+        // 7. 返回验证后的金额信息
+        return new OrderAmountInfo(totalAmount, tablewareFee, frontendDiscountAmount, payAmount);
+    }
+
+    /**
+     * 验证并纠正金额
+     * 如果前端金额与后端计算金额的误差超过阈值,则使用后端计算的金额
+     *
+     * @param frontendAmount 前端传入的金额
+     * @param backendAmount  后端计算的金额
+     * @param amountName     金额名称(用于日志)
+     * @return 验证并纠正后的金额
+     */
+    private BigDecimal validateAndCorrectAmount(
+            BigDecimal frontendAmount, 
+            BigDecimal backendAmount, 
+            String amountName) {
+        
+        if (frontendAmount == null || backendAmount == null) {
+            log.warn("{}验证失败:前端金额或后端金额为null, frontend={}, backend={}", 
+                    amountName, frontendAmount, backendAmount);
+            return backendAmount != null ? backendAmount : BigDecimal.ZERO;
+        }
+
+        BigDecimal difference = frontendAmount.subtract(backendAmount).abs();
+        
+        if (difference.compareTo(AMOUNT_TOLERANCE) > 0) {
+            log.warn("前端计算的{}与后端计算不一致, frontend={}, backend={}, difference={}", 
+                    amountName, frontendAmount, backendAmount, difference);
+            // 使用后端计算的金额,确保数据准确性
+            return backendAmount;
+        }
+
+        // 误差在允许范围内,使用前端金额
+        return frontendAmount;
+    }
 }

+ 1 - 0
alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java

@@ -297,6 +297,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
                     storeOrder.setOrderStatus(1);
                     if (storeOrderService.updateById(storeOrder)) {
                         log.info("小程序更新订单成功,订单号outTradeNo:{}", outTradeNo);
+                        // 支付完成后,清空购物车和重置餐桌状态(保留订单数据,不删除订单)
                         try {
                             storeOrderService.resetTableAfterPayment(storeOrder.getTableId());
                             log.info("支付完成后重置餐桌成功, tableId={}", storeOrder.getTableId());

+ 3 - 3
alien-entity/src/main/java/shop/alien/entity/store/LifeDiscountCoupon.java

@@ -42,7 +42,7 @@ public class LifeDiscountCoupon extends Model<LifeDiscountCoupon> {
     private String name;
 
     @ApiModelProperty(value = "面值")
-    @TableField(value = "nominal_value", fill = FieldFill.UPDATE)
+    @TableField(value = "nominal_value", insertStrategy = FieldStrategy.IGNORED, updateStrategy = FieldStrategy.IGNORED)
     private BigDecimal nominalValue;
 
     @ApiModelProperty(value = "有效期(天)")
@@ -58,7 +58,7 @@ public class LifeDiscountCoupon extends Model<LifeDiscountCoupon> {
     private LocalDate endDate;
 
     @ApiModelProperty(value = "库存(优惠券数量)")
-    @TableField(value = "single_qty", fill = FieldFill.UPDATE)
+    @TableField(value = "single_qty")
     private Integer singleQty;
 
     @ApiModelProperty(value = "补充说明")
@@ -74,7 +74,7 @@ public class LifeDiscountCoupon extends Model<LifeDiscountCoupon> {
     private Integer restrictedQuantity;
 
     @ApiModelProperty(value = "最低消费")
-    @TableField(value = "minimum_spending_amount", fill = FieldFill.UPDATE)
+    @TableField(value = "minimum_spending_amount", insertStrategy = FieldStrategy.IGNORED, updateStrategy = FieldStrategy.IGNORED)
     private BigDecimal minimumSpendingAmount;
 
     @ApiModelProperty(value = "类型   1-优惠券  2-红包 3-平台优惠券 4代金券")

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CreateOrderDTO.java

@@ -26,6 +26,18 @@ public class CreateOrderDTO {
     @ApiModelProperty(value = "优惠券ID(可选,不选择优惠券时传 null 或不传此字段)")
     private Integer couponId;
 
+    @ApiModelProperty(value = "订单总金额(由前端计算,菜品总价,不含餐具费和优惠金额)")
+    private java.math.BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费(由前端计算,基于门店餐具费单价 × 就餐人数)")
+    private java.math.BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠金额(由前端计算,如果使用优惠券则必传,不使用优惠券时传 0 或不传)")
+    private java.math.BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "实付金额(由前端计算,订单总金额 + 餐具费 - 优惠金额)")
+    private java.math.BigDecimal payAmount;
+
     @ApiModelProperty(value = "联系电话")
     private String contactPhone;
 

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

@@ -73,6 +73,18 @@ public class OrderInfoVO {
     @ApiModelProperty(value = "优惠券名称")
     private String couponName;
 
+    @ApiModelProperty(value = "优惠券类型:1-满减券,2-折扣券")
+    private Integer couponType;
+
+    @ApiModelProperty(value = "折扣率(0-100,用于折扣券,例如80表示8折)")
+    private BigDecimal discountRate;
+
+    @ApiModelProperty(value = "面值(用于满减券)")
+    private BigDecimal nominalValue;
+
+    @ApiModelProperty(value = "最低消费")
+    private BigDecimal minimumSpendingAmount;
+
     @ApiModelProperty(value = "优惠金额")
     private BigDecimal discountAmount;
 

+ 16 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java

@@ -230,6 +230,22 @@ public class DiningServiceController {
         }
     }
 
+    @ApiOperation(value = "商家手动完成订单", notes = "供商家使用,手动点击完成订单。不校验订单是否处于已支付状态,直接将订单状态改为已完成。订单状态:0-待支付,1-已支付,2-已取消,3-已完成")
+    @ApiOperationSupport(order = 10)
+    @PostMapping("/order/complete-by-merchant/{orderId}")
+    public R<Boolean> completeOrderByMerchant(
+            HttpServletRequest request,
+            @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            String authorization = getAuthorization(request);
+            log.info("商家手动完成订单: orderId={}", orderId);
+            return diningServiceFeign.completeOrderByMerchant(authorization, orderId);
+        } catch (Exception e) {
+            log.error("商家手动完成订单失败: {}", e.getMessage(), e);
+            return R.fail("商家手动完成订单失败: " + e.getMessage());
+        }
+    }
+
     // ==================== 用户相关接口 ====================
 
     @ApiOperation(value = "获取用户信息", notes = "获取当前登录用户的详细信息(从token中获取用户ID)")

+ 12 - 0
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -179,6 +179,18 @@ public interface DiningServiceFeign {
             @RequestHeader(value = "Authorization", required = false) String authorization,
             @PathVariable("orderId") Integer orderId);
 
+    /**
+     * 商家手动完成订单(不校验支付状态)
+     *
+     * @param authorization 请求头 Authorization
+     * @param orderId       订单ID
+     * @return R.data 为 Boolean(是否成功)
+     */
+    @PostMapping("/store/order/complete-by-merchant/{orderId}")
+    R<Boolean> completeOrderByMerchant(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @PathVariable("orderId") Integer orderId);
+
     // ==================== 用户相关接口 ====================
 
     /**

+ 37 - 43
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java

@@ -129,13 +129,22 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
             //发布优惠券表信息
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
+            
+            // 显式设置面值(nominalValue),确保能正确保存
+            BigDecimal nominalValue = lifeDiscountCouponDto.getNominalValue();
+            if (nominalValue != null) {
+                lifeDiscountCoupon.setNominalValue(nominalValue);
+            }
+            
             // 显式设置最低消费金额(门槛费),确保能正确保存
-            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
-            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
-                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            // 如果 DTO 中有值(包括0),使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            BigDecimal minimumSpendingAmount = lifeDiscountCouponDto.getMinimumSpendingAmount();
+            if (minimumSpendingAmount != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(minimumSpendingAmount);
             } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
+            
             // 根据开始领取时间判断可领取状态
             // 判断是否在领取时间内
 
@@ -227,13 +236,22 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
             LifeDiscountCoupon lifeDiscountCoupon = new LifeDiscountCoupon();
             lifeDiscountCoupon.setId(Integer.parseInt(lifeDiscountCouponDto.getCouponId()));
             BeanUtils.copyProperties(lifeDiscountCouponDto, lifeDiscountCoupon);
+            
+            // 显式设置面值(nominalValue),确保能正确保存
+            BigDecimal nominalValue = lifeDiscountCouponDto.getNominalValue();
+            if (nominalValue != null) {
+                lifeDiscountCoupon.setNominalValue(nominalValue);
+            }
+            
             // 显式设置最低消费金额(门槛费),确保能正确保存
-            // 如果 DTO 中有值,使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
-            if (lifeDiscountCouponDto.getMinimumSpendingAmount() != null) {
-                lifeDiscountCoupon.setMinimumSpendingAmount(lifeDiscountCouponDto.getMinimumSpendingAmount());
+            // 如果 DTO 中有值(包括0),使用 DTO 的值;如果为 null,设置为 0(表示无门槛)
+            BigDecimal minimumSpendingAmount = lifeDiscountCouponDto.getMinimumSpendingAmount();
+            if (minimumSpendingAmount != null) {
+                lifeDiscountCoupon.setMinimumSpendingAmount(minimumSpendingAmount);
             } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
+            
 
             // 根据开始领取时间判断可领取状态
             // 判断是否在领取时间内
@@ -1858,7 +1876,7 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
     }
 
     /**
-     * 收藏店铺时自动发放优惠券(每种类型一张
+     * 收藏店铺时自动发放优惠券(每个优惠券都要发放,已领取的跳过
      * 
      * @param userId   用户ID
      * @param storeId  店铺ID
@@ -1903,24 +1921,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                 return 0;
             }
 
-            // 按 couponType 分组,每种类型只取第一个(最新的)
-            Map<Integer, LifeDiscountCoupon> couponByType = new HashMap<>();
-            for (LifeDiscountCoupon coupon : allCoupons) {
-                Integer couponType = coupon.getCouponType();
-                if (couponType != null && !couponByType.containsKey(couponType)) {
-                    couponByType.put(couponType, coupon);
-                }
-            }
-
-            if (couponByType.isEmpty()) {
-                return 0;
-            }
-
             // 查询用户已通过收藏店铺领取的优惠券(只检查 issueSource=2,不检查其他来源)
             // 业务规则:好评赠券和收藏领券不冲突,可以分别领取
             // 因此只检查是否通过收藏领取过,不检查好评等其他方式
             // 获取所有可发放的优惠券ID列表
-            List<Integer> availableCouponIds = couponByType.values().stream()
+            List<Integer> availableCouponIds = allCoupons.stream()
                     .map(LifeDiscountCoupon::getId)
                     .collect(Collectors.toList());
             
@@ -1935,31 +1940,20 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                     .filter(Objects::nonNull)
                     .collect(Collectors.toSet());
             
-            // 通过已领取的优惠券ID查询对应的优惠券类型,建立类型到已领取优惠券的映射
-            final Set<Integer> existingCouponTypes;
-            if (!existingCouponIds.isEmpty()) {
-                LambdaQueryWrapper<LifeDiscountCoupon> existingCouponQuery = new LambdaQueryWrapper<>();
-                existingCouponQuery.in(LifeDiscountCoupon::getId, existingCouponIds)
-                        .isNotNull(LifeDiscountCoupon::getCouponType);
-                List<LifeDiscountCoupon> existingCouponList = lifeDiscountCouponMapper.selectList(existingCouponQuery);
-                existingCouponTypes = existingCouponList.stream()
-                        .map(LifeDiscountCoupon::getCouponType)
-                        .filter(Objects::nonNull)
-                        .collect(Collectors.toSet());
-            } else {
-                existingCouponTypes = new HashSet<>();
+            // 过滤掉用户已通过收藏领取过的优惠券(每个优惠券都要检查,不是按类型)
+            // 业务规则:每个优惠券都要检查是否已领取,已领取的跳过
+            final Set<Integer> finalExistingCouponIds = existingCouponIds;
+            List<LifeDiscountCoupon> couponsToIssue = allCoupons.stream()
+                    .filter(coupon -> !finalExistingCouponIds.contains(coupon.getId()))
+                    .collect(Collectors.toList());
+
+            if (couponsToIssue.isEmpty()) {
+                return 0;
             }
-            
-            // 过滤掉用户已通过收藏领取过的优惠券类型
-            // 业务规则:收藏时每种类型只发放一张,但好评等其他方式可以另外领取
-            final Set<Integer> finalExistingCouponTypes = existingCouponTypes;
-            couponByType.entrySet().removeIf(entry -> finalExistingCouponTypes.contains(entry.getKey()));
 
-            // 发放每种类型的优惠券(每种一张)
-            // 注意:此时 couponByType 已经过滤掉了用户已领取过的类型,所以直接发放即可
+            // 发放每个优惠券(每个优惠券都要发放,不是按类型)
             int issuedCount = 0;
-            for (Map.Entry<Integer, LifeDiscountCoupon> entry : couponByType.entrySet()) {
-                LifeDiscountCoupon coupon = entry.getValue();
+            for (LifeDiscountCoupon coupon : couponsToIssue) {
 
                 // 再次检查库存(防止并发问题)
                 if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {