lutong vor 2 Wochen
Ursprung
Commit
bbb910fcbd
98 geänderte Dateien mit 11822 neuen und 1015 gelöschten Zeilen
  1. 17 0
      alien-dining/doc/wechat-store-tables-ddl.sql
  2. 10 10
      alien-dining/src/main/java/shop/alien/dining/config/CartWebSocketProcess.java
  3. 6 4
      alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java
  4. 1 1
      alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java
  5. 15 15
      alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java
  6. 58 56
      alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java
  7. 11 11
      alien-dining/src/main/java/shop/alien/dining/service/CartService.java
  8. 2 2
      alien-dining/src/main/java/shop/alien/dining/service/DiningService.java
  9. 10 10
      alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java
  10. 21 19
      alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java
  11. 147 147
      alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java
  12. 71 67
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java
  13. 2 2
      alien-dining/src/main/java/shop/alien/dining/service/impl/OrderLockServiceImpl.java
  14. 59 59
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java
  15. 256 254
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  16. 1 1
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java
  17. 27 27
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java
  18. 32 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatAddCartItemDTO.java
  19. 34 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCartDTO.java
  20. 51 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCartItemDTO.java
  21. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatChangeTableDTO.java
  22. 47 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCreateOrderDTO.java
  23. 23 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatStoreInfoWithHomepageCuisinesDTO.java
  24. 22 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatUpdateOrderCouponDTO.java
  25. 30 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatCategoryWithCuisinesVO.java
  26. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderChangeLogBatchVO.java
  27. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderChangeLogItemVO.java
  28. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderConfirmVO.java
  29. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderCuisineItemVO.java
  30. 74 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderDetailWithChangeLogVO.java
  31. 105 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderInfoVO.java
  32. 65 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderSettlementVO.java
  33. 31 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderSuccessVO.java
  34. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatStoreOrderPageVO.java
  35. 99 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCart.java
  36. 88 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCouponUsage.java
  37. 143 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisine.java
  38. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisineCategory.java
  39. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisineCombo.java
  40. 141 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrder.java
  41. 120 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderChangeLog.java
  42. 108 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderDetail.java
  43. 76 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderLock.java
  44. 177 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStorePaymentConfig.java
  45. 98 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreTable.java
  46. 78 0
      alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreTableLog.java
  47. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCartMapper.java
  48. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCouponUsageMapper.java
  49. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineCategoryMapper.java
  50. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineComboMapper.java
  51. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineMapper.java
  52. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderChangeLogMapper.java
  53. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderDetailMapper.java
  54. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderLockMapper.java
  55. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderMapper.java
  56. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStorePaymentConfigMapper.java
  57. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreTableLogMapper.java
  58. 10 0
      alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreTableMapper.java
  59. 26 0
      alien-store/src/main/java/shop/alien/store/annotation/OperationLog.java
  60. 22 0
      alien-store/src/main/java/shop/alien/store/aspect/DiningOperationLogAspect.java
  61. 48 0
      alien-store/src/main/java/shop/alien/store/controller/DiningCollectController.java
  62. 307 0
      alien-store/src/main/java/shop/alien/store/controller/DiningController.java
  63. 188 0
      alien-store/src/main/java/shop/alien/store/controller/DiningCouponController.java
  64. 60 0
      alien-store/src/main/java/shop/alien/store/controller/DiningFileUploadController.java
  65. 59 57
      alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java
  66. 188 0
      alien-store/src/main/java/shop/alien/store/controller/DiningUserController.java
  67. 108 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  68. 578 0
      alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java
  69. 34 0
      alien-store/src/main/java/shop/alien/store/dto/ChangePhoneDto.java
  70. 56 0
      alien-store/src/main/java/shop/alien/store/dto/UserProfileUpdateDto.java
  71. 20 0
      alien-store/src/main/java/shop/alien/store/dto/VerifyTokenDto.java
  72. 29 0
      alien-store/src/main/java/shop/alien/store/dto/WeChatLoginDto.java
  73. 48 0
      alien-store/src/main/java/shop/alien/store/enums/OrderStatus.java
  74. 80 0
      alien-store/src/main/java/shop/alien/store/feign/AlienStoreFeign.java
  75. 0 273
      alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java
  76. 121 0
      alien-store/src/main/java/shop/alien/store/service/CartService.java
  77. 22 0
      alien-store/src/main/java/shop/alien/store/service/DiningCollectService.java
  78. 101 0
      alien-store/src/main/java/shop/alien/store/service/DiningCouponService.java
  79. 150 0
      alien-store/src/main/java/shop/alien/store/service/DiningService.java
  80. 58 0
      alien-store/src/main/java/shop/alien/store/service/DiningUserService.java
  81. 61 0
      alien-store/src/main/java/shop/alien/store/service/OrderLockService.java
  82. 67 0
      alien-store/src/main/java/shop/alien/store/service/ScanOrderStoreInfoService.java
  83. 35 0
      alien-store/src/main/java/shop/alien/store/service/SseService.java
  84. 189 0
      alien-store/src/main/java/shop/alien/store/service/StoreOrderService.java
  85. 1072 0
      alien-store/src/main/java/shop/alien/store/service/impl/CartServiceImpl.java
  86. 39 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningCollectServiceImpl.java
  87. 471 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningCouponServiceImpl.java
  88. 690 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningServiceImpl.java
  89. 697 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningUserServiceImpl.java
  90. 118 0
      alien-store/src/main/java/shop/alien/store/service/impl/OrderLockServiceImpl.java
  91. 239 0
      alien-store/src/main/java/shop/alien/store/service/impl/ScanOrderStoreInfoServiceImpl.java
  92. 180 0
      alien-store/src/main/java/shop/alien/store/service/impl/SseServiceImpl.java
  93. 2019 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOrderServiceImpl.java
  94. 227 0
      alien-store/src/main/java/shop/alien/store/util/TokenUtil.java
  95. 375 0
      alien-store/src/main/java/shop/alien/store/util/WeChatMiniProgramUtil.java
  96. 65 0
      alien-store/src/main/java/shop/alien/store/util/WeChatPayUtil.java
  97. 76 0
      alien-store/src/main/java/shop/alien/store/vo/DiningUserVo.java
  98. 40 0
      alien-store/src/main/java/shop/alien/store/vo/TokenVerifyVo.java

+ 17 - 0
alien-dining/doc/wechat-store-tables-ddl.sql

@@ -0,0 +1,17 @@
+-- 小程序点餐专用表:与 APP 共库不同表,除用户/门店主数据(store_info)/优惠券定义外独立维护。
+-- MySQL 5.7+:CREATE TABLE ... LIKE 复制列定义与索引;请在低峰执行并做好备份。
+
+CREATE TABLE IF NOT EXISTS wechat_store_table LIKE store_table;
+CREATE TABLE IF NOT EXISTS wechat_store_cuisine LIKE store_cuisine;
+CREATE TABLE IF NOT EXISTS wechat_store_cuisine_category LIKE store_cuisine_category;
+CREATE TABLE IF NOT EXISTS wechat_store_cuisine_combo LIKE store_cuisine_combo;
+CREATE TABLE IF NOT EXISTS wechat_store_cart LIKE store_cart;
+CREATE TABLE IF NOT EXISTS wechat_store_order LIKE store_order;
+CREATE TABLE IF NOT EXISTS wechat_store_order_detail LIKE store_order_detail;
+CREATE TABLE IF NOT EXISTS wechat_store_order_lock LIKE store_order_lock;
+CREATE TABLE IF NOT EXISTS wechat_store_table_log LIKE store_table_log;
+CREATE TABLE IF NOT EXISTS wechat_store_order_change_log LIKE store_order_change_log;
+CREATE TABLE IF NOT EXISTS wechat_store_coupon_usage LIKE store_coupon_usage;
+CREATE TABLE IF NOT EXISTS wechat_store_payment_config LIKE store_payment_config;
+
+-- 上线后需将小程序侧菜单/桌台等基础数据同步或录入到 wechat_* 表(与 APP 侧无数据互通)。

+ 10 - 10
alien-dining/src/main/java/shop/alien/dining/config/CartWebSocketProcess.java

@@ -7,8 +7,8 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.stereotype.Component;
 import shop.alien.dining.service.CartService;
-import shop.alien.entity.store.dto.AddCartItemDTO;
-import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.wechat.WechatAddCartItemDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
 
 import javax.websocket.*;
 import javax.websocket.server.PathParam;
@@ -79,7 +79,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
 
             // 发送当前购物车数据
             try {
-                CartDTO cart = cartService.getCart(tableId);
+                WechatCartDTO cart = cartService.getCart(tableId);
                 sendMessage(createMessage("cart_update", "购物车数据", cart));
             } catch (Exception e) {
                 log.error("获取购物车数据失败, tableId={}", tableId, e);
@@ -178,14 +178,14 @@ public class CartWebSocketProcess implements ApplicationContextAware {
                 return;
             }
 
-            AddCartItemDTO dto = new AddCartItemDTO();
+            WechatAddCartItemDTO dto = new WechatAddCartItemDTO();
             dto.setTableId(tableId);
             dto.setCuisineId(data.getInteger("cuisineId"));
             dto.setQuantity(data.getInteger("quantity"));
             dto.setRemark(data.getString("remark"));
 
             // 调用服务添加商品
-            CartDTO cart = cartService.addItem(dto);
+            WechatCartDTO cart = cartService.addItem(dto);
 
             // 推送更新给该桌号的所有连接
             pushCartUpdateToAll(tableId, cart);
@@ -219,7 +219,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
             }
 
             // 调用服务更新数量
-            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+            WechatCartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
 
             // 推送更新给该桌号的所有连接
             pushCartUpdateToAll(tableId, cart);
@@ -251,7 +251,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
             }
 
             // 调用服务删除商品
-            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+            WechatCartDTO cart = cartService.removeItem(tableId, cuisineId);
 
             // 推送更新给该桌号的所有连接
             pushCartUpdateToAll(tableId, cart);
@@ -274,7 +274,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
             cartService.clearCart(tableId);
 
             // 创建空的购物车对象
-            CartDTO cart = new CartDTO();
+            WechatCartDTO cart = new WechatCartDTO();
             cart.setTableId(tableId);
             cart.setItems(java.util.Collections.emptyList());
             cart.setTotalAmount(java.math.BigDecimal.ZERO);
@@ -295,7 +295,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
     /**
      * 推送购物车更新给该桌号的所有连接
      */
-    private void pushCartUpdateToAll(Integer tableId, CartDTO cart) {
+    private void pushCartUpdateToAll(Integer tableId, WechatCartDTO cart) {
         ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
         if (tableConnections != null && !tableConnections.isEmpty()) {
             tableConnections.forEach((connId, ws) -> {
@@ -359,7 +359,7 @@ public class CartWebSocketProcess implements ApplicationContextAware {
     /**
      * 静态方法:推送购物车更新(供外部调用,如HTTP接口)
      */
-    public static void pushCartUpdate(Integer tableId, CartDTO cart) {
+    public static void pushCartUpdate(Integer tableId, WechatCartDTO cart) {
         ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
         if (tableConnections != null && !tableConnections.isEmpty()) {
             String message = createStaticMessage("cart_update", "购物车更新", cart);

+ 6 - 4
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -8,6 +8,8 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.vo.TableDiningStatusVO;
 import shop.alien.entity.store.vo.*;
+import shop.alien.entity.store.vo.wechat.WechatOrderConfirmVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderSettlementVO;
 import shop.alien.dining.service.DiningService;
 import shop.alien.dining.util.TokenUtil;
 
@@ -167,7 +169,7 @@ public class DiningController {
 
     @ApiOperation(value = "获取订单确认页面信息", notes = "获取订单确认页面的所有信息")
     @GetMapping("/order/confirm")
-    public R<OrderConfirmVO> getOrderConfirmInfo(
+    public R<WechatOrderConfirmVO> getOrderConfirmInfo(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
         log.info("DiningController.getOrderConfirmInfo?tableId={}, dinerCount={}", tableId, dinerCount);
@@ -177,7 +179,7 @@ public class DiningController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            OrderConfirmVO vo = diningService.getOrderConfirmInfo(tableId, dinerCount, userId);
+            WechatOrderConfirmVO vo = diningService.getOrderConfirmInfo(tableId, dinerCount, userId);
             return R.data(vo);
         } catch (Exception e) {
             log.error("获取订单确认页面信息失败: {}", e.getMessage(), e);
@@ -247,7 +249,7 @@ public class DiningController {
 
     @ApiOperation(value = "获取订单结算确认页面信息", notes = "获取订单结算确认页面的所有信息")
     @GetMapping("/order/settlement")
-    public R<shop.alien.entity.store.vo.OrderSettlementVO> getOrderSettlementInfo(
+    public R<WechatOrderSettlementVO> getOrderSettlementInfo(
             @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
         log.info("DiningController.getOrderSettlementInfo?orderId={}", orderId);
         try {
@@ -256,7 +258,7 @@ public class DiningController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            shop.alien.entity.store.vo.OrderSettlementVO vo = diningService.getOrderSettlementInfo(orderId, userId);
+            WechatOrderSettlementVO vo = diningService.getOrderSettlementInfo(orderId, userId);
             return R.data(vo);
         } catch (Exception e) {
             log.error("获取订单结算确认页面信息失败: {}", e.getMessage(), e);

+ 1 - 1
alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java

@@ -44,7 +44,7 @@ public class PaymentController {
             @ApiImplicitParam(name = "payType", value = "支付类型(alipay:支付宝, wechatPay:微信支付)", required = true, paramType = "query", dataType = "String"),
             @ApiImplicitParam(name = "payer", value = "支付用户", required = true, paramType = "query", dataType = "String"),
             @ApiImplicitParam(name = "orderNo", value = "订单号", required = true, paramType = "query", dataType = "String"),
-            @ApiImplicitParam(name = "storeId", value = "店铺ID,用于从 MySQL 获取该店铺支付配置(StorePaymentConfig)", required = true, paramType = "query", dataType = "Integer"),
+            @ApiImplicitParam(name = "storeId", value = "店铺ID,用于从 MySQL 获取该店铺支付配置(WechatStorePaymentConfig)", required = true, paramType = "query", dataType = "Integer"),
             @ApiImplicitParam(name ="couponId", value = "优惠券Id"),
             @ApiImplicitParam(name = "payerId", value = "payerId"),
             @ApiImplicitParam(name = "tablewareFee", value = "餐具费"),

+ 15 - 15
alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java

@@ -5,11 +5,11 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
-import shop.alien.entity.store.StoreCuisine;
-import shop.alien.entity.store.StoreCuisineCategory;
-import shop.alien.entity.store.StoreTable;
-import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCategory;
+import shop.alien.entity.store.wechat.WechatStoreTable;
+import shop.alien.entity.store.dto.wechat.WechatStoreInfoWithHomepageCuisinesDTO;
+import shop.alien.entity.store.vo.wechat.WechatCategoryWithCuisinesVO;
 import shop.alien.dining.service.StoreInfoService;
 
 import java.util.List;
@@ -32,14 +32,14 @@ public class StoreInfoController {
 
     @ApiOperation(value = "根据门店ID查询桌号列表", notes = "查询指定门店下的所有桌号")
     @GetMapping("/tables")
-    public R<List<StoreTable>> getTablesByStoreId(
+    public R<List<WechatStoreTable>> getTablesByStoreId(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
         log.info("StoreInfoController.getTablesByStoreId?storeId={}", storeId);
         try {
             if (storeId == null) {
                 return R.fail("门店ID不能为空");
             }
-            List<StoreTable> tables = storeInfoService.getTablesByStoreId(storeId);
+            List<WechatStoreTable> tables = storeInfoService.getTablesByStoreId(storeId);
             return R.data(tables);
         } catch (Exception e) {
             log.error("查询桌号列表失败: {}", e.getMessage(), e);
@@ -49,14 +49,14 @@ public class StoreInfoController {
 
     @ApiOperation(value = "根据门店ID查询菜品种类列表", notes = "查询指定门店下的所有菜品种类")
     @GetMapping("/categories")
-    public R<List<StoreCuisineCategory>> getCategoriesByStoreId(
+    public R<List<WechatStoreCuisineCategory>> getCategoriesByStoreId(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
         log.info("StoreInfoController.getCategoriesByStoreId?storeId={}", storeId);
         try {
             if (storeId == null) {
                 return R.fail("门店ID不能为空");
             }
-            List<StoreCuisineCategory> categories = storeInfoService.getCategoriesByStoreId(storeId);
+            List<WechatStoreCuisineCategory> categories = storeInfoService.getCategoriesByStoreId(storeId);
             return R.data(categories);
         } catch (Exception e) {
             log.error("查询菜品种类列表失败: {}", e.getMessage(), e);
@@ -66,7 +66,7 @@ public class StoreInfoController {
 
     @ApiOperation(value = "根据门店ID查询菜品种类及各类别下菜品", notes = "一次返回所有菜品种类及每个分类下的菜品列表;可选 keyword 按菜品名称模糊查询。返回中 category 与 /store/info/categories 单条结构一致,cuisines 与 /store/info/cuisines 返回结构一致,保证字段完整一致。")
     @GetMapping("/categories-with-cuisines")
-    public R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
+    public R<List<WechatCategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
             @ApiParam(value = "菜品名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword) {
         log.info("StoreInfoController.getCategoriesWithCuisinesByStoreId?storeId={}, keyword={}", storeId, keyword);
@@ -74,7 +74,7 @@ public class StoreInfoController {
             if (storeId == null) {
                 return R.fail("门店ID不能为空");
             }
-            List<CategoryWithCuisinesVO> list = storeInfoService.getCategoriesWithCuisinesByStoreId(storeId, keyword);
+            List<WechatCategoryWithCuisinesVO> list = storeInfoService.getCategoriesWithCuisinesByStoreId(storeId, keyword);
             return R.data(list);
         } catch (Exception e) {
             log.error("查询菜品种类及菜品失败: {}", e.getMessage(), e);
@@ -101,14 +101,14 @@ public class StoreInfoController {
 
     @ApiOperation(value = "根据菜品种类ID查询菜品信息列表", notes = "查询指定分类下的所有菜品信息")
     @GetMapping("/cuisines")
-    public R<List<StoreCuisine>> getCuisinesByCategoryId(
+    public R<List<WechatStoreCuisine>> getCuisinesByCategoryId(
             @ApiParam(value = "菜品种类ID", required = true) @RequestParam Integer categoryId) {
         log.info("StoreInfoController.getCuisinesByCategoryId?categoryId={}", categoryId);
         try {
             if (categoryId == null) {
                 return R.fail("菜品种类ID不能为空");
             }
-            List<StoreCuisine> cuisines = storeInfoService.getCuisinesByCategoryId(categoryId);
+            List<WechatStoreCuisine> cuisines = storeInfoService.getCuisinesByCategoryId(categoryId);
             return R.data(cuisines);
         } catch (Exception e) {
             log.error("查询菜品信息列表失败: {}", e.getMessage(), e);
@@ -118,14 +118,14 @@ public class StoreInfoController {
 
     @ApiOperation(value = "根据商铺ID查询店铺信息和首页展示美食价目表", notes = "查询店铺信息和当前店铺绑定的首页展示美食价目表信息")
     @GetMapping("/detail/{storeId}")
-    public R<StoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(
+    public R<WechatStoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(
             @ApiParam(value = "商铺ID", required = true) @PathVariable Integer storeId) {
         log.info("StoreInfoController.getStoreInfoWithHomepageCuisines?storeId={}", storeId);
         try {
             if (storeId == null) {
                 return R.fail("商铺ID不能为空");
             }
-            StoreInfoWithHomepageCuisinesDTO result = storeInfoService.getStoreInfoWithHomepageCuisines(storeId);
+            WechatStoreInfoWithHomepageCuisinesDTO result = storeInfoService.getStoreInfoWithHomepageCuisines(storeId);
             return R.data(result);
         } catch (Exception e) {
             log.error("查询店铺信息和首页展示美食价目表失败: {}", e.getMessage(), e);

+ 58 - 56
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -16,17 +16,19 @@ import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.util.TokenUtil;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
-import shop.alien.entity.store.dto.AddCartItemDTO;
-import shop.alien.entity.store.dto.CartDTO;
-import shop.alien.entity.store.dto.ChangeTableDTO;
-import shop.alien.entity.store.dto.CreateOrderDTO;
-import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
-import shop.alien.entity.store.vo.OrderInfoVO;
-import shop.alien.entity.store.vo.OrderDetailWithChangeLogVO;
-import shop.alien.entity.store.vo.StoreOrderPageVO;
+import shop.alien.entity.store.wechat.*;
+import shop.alien.entity.store.dto.wechat.WechatAddCartItemDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
+import shop.alien.entity.store.dto.wechat.WechatChangeTableDTO;
+import shop.alien.entity.store.dto.wechat.WechatCreateOrderDTO;
+import shop.alien.entity.store.dto.wechat.WechatUpdateOrderCouponDTO;
+import shop.alien.entity.store.vo.wechat.WechatOrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderInfoVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderDetailWithChangeLogVO;
+import shop.alien.entity.store.vo.wechat.WechatStoreOrderPageVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderSuccessVO;
 import shop.alien.mapper.StoreInfoMapper;
-import shop.alien.mapper.StoreOrderDetailMapper;
-import shop.alien.mapper.StoreTableMapper;
+import shop.alien.mapper.wechat.*;
 
 import javax.validation.Valid;
 import java.util.List;
@@ -48,20 +50,20 @@ public class StoreOrderController {
     private final StoreOrderService orderService;
     private final CartService cartService;
     private final SseService sseService;
-    private final StoreOrderDetailMapper orderDetailMapper;
-    private final StoreTableMapper storeTableMapper;
+    private final WechatStoreOrderDetailMapper orderDetailMapper;
+    private final WechatStoreTableMapper storeTableMapper;
     private final StoreInfoMapper storeInfoMapper;
 
     @ApiOperation(value = "获取购物车", notes = "根据桌号ID获取购物车信息")
     @GetMapping("/cart/{tableId}")
-    public R<CartDTO> getCart(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+    public R<WechatCartDTO> getCart(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
         log.info("StoreOrderController.getCart?tableId={}", tableId);
         try {
             // 验证 token
             if (!TokenUtil.hasValidToken()) {
                 return R.fail("用户未登录");
             }
-            CartDTO cart = cartService.getCart(tableId);
+            WechatCartDTO cart = cartService.getCart(tableId);
             return R.data(cart);
         } catch (Exception e) {
             log.error("获取购物车失败: {}", e.getMessage(), e);
@@ -71,14 +73,14 @@ public class StoreOrderController {
 
     @ApiOperation(value = "添加商品到购物车", notes = "添加商品到购物车,并推送SSE和WebSocket消息")
     @PostMapping("/cart/add")
-    public R<CartDTO> addCartItem(@Valid @RequestBody AddCartItemDTO dto) {
+    public R<WechatCartDTO> addCartItem(@Valid @RequestBody WechatAddCartItemDTO dto) {
         log.info("StoreOrderController.addCartItem?dto={}", dto);
         try {
             // 验证 token(用户信息在 CartService 中从 token 获取)
             if (!TokenUtil.hasValidToken()) {
                 return R.fail("用户未登录");
             }
-            CartDTO cart = cartService.addItem(dto);
+            WechatCartDTO cart = cartService.addItem(dto);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(dto.getTableId(), cart);
             // 推送购物车更新消息(WebSocket)
@@ -92,7 +94,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "更新购物车商品数量", notes = "更新购物车中商品的数量,并推送SSE和WebSocket消息")
     @PutMapping("/cart/update")
-    public R<CartDTO> updateCartItem(
+    public R<WechatCartDTO> updateCartItem(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId,
             @ApiParam(value = "数量", required = true) @RequestParam Integer quantity) {
@@ -102,7 +104,7 @@ public class StoreOrderController {
             if (!TokenUtil.hasValidToken()) {
                 return R.fail("用户未登录");
             }
-            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+            WechatCartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
             // 推送购物车更新消息(WebSocket)
@@ -116,7 +118,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "删除购物车商品", notes = "从购物车中删除商品,并推送SSE和WebSocket消息")
     @DeleteMapping("/cart/remove")
-    public R<CartDTO> removeCartItem(
+    public R<WechatCartDTO> removeCartItem(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId) {
         log.info("StoreOrderController.removeCartItem?tableId={}, cuisineId={}", tableId, cuisineId);
@@ -125,7 +127,7 @@ public class StoreOrderController {
             if (!TokenUtil.hasValidToken()) {
                 return R.fail("用户未登录");
             }
-            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+            WechatCartDTO cart = cartService.removeItem(tableId, cuisineId);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
             // 推送购物车更新消息(WebSocket)
@@ -139,7 +141,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "清空购物车", notes = "清空购物车中所有商品(保留餐具和已下单商品),并推送SSE和WebSocket消息")
     @DeleteMapping("/cart/clear")
-    public R<CartDTO> clearCart(
+    public R<WechatCartDTO> clearCart(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         log.info("StoreOrderController.clearCart?tableId={}", tableId);
         try {
@@ -152,7 +154,7 @@ public class StoreOrderController {
             cartService.clearCart(tableId);
             
             // 获取清空后的购物车(包含保留的餐具和已下单商品)
-            CartDTO cart = cartService.getCart(tableId);
+            WechatCartDTO cart = cartService.getCart(tableId);
             
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
@@ -168,7 +170,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "设置用餐人数", notes = "设置用餐人数,自动添加或更新餐具到购物车")
     @PostMapping("/cart/set-diner-count")
-    public R<CartDTO> setDinerCount(
+    public R<WechatCartDTO> setDinerCount(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "用餐人数", required = true) @RequestParam Integer dinerCount) {
         log.info("StoreOrderController.setDinerCount?tableId={}, dinerCount={}", tableId, dinerCount);
@@ -180,7 +182,7 @@ public class StoreOrderController {
             if (dinerCount == null || dinerCount <= 0) {
                 return R.fail("用餐人数必须大于0");
             }
-            CartDTO cart = cartService.setDinerCount(tableId, dinerCount);
+            WechatCartDTO cart = cartService.setDinerCount(tableId, dinerCount);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
             // 推送购物车更新消息(WebSocket)
@@ -194,7 +196,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "更新餐具数量", notes = "更新购物车中餐具的数量,并推送SSE和WebSocket消息。餐具数量为0或未传时不往购物车加餐具(0会移除已有餐具项)")
     @PutMapping("/cart/update-tableware")
-    public R<CartDTO> updateTablewareQuantity(
+    public R<WechatCartDTO> updateTablewareQuantity(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "餐具数量,0或未传则不往购物车加餐具") @RequestParam(required = false) Integer quantity) {
         log.info("StoreOrderController.updateTablewareQuantity?tableId={}, quantity={}", tableId, quantity);
@@ -208,16 +210,16 @@ public class StoreOrderController {
             }
             // 餐具数量为 0 或 null 时不往购物车加餐具:0 则移除已有餐具项,null 则直接返回当前购物车
             if (quantity == null) {
-                CartDTO cart = cartService.getCart(tableId);
+                WechatCartDTO cart = cartService.getCart(tableId);
                 sseService.pushCartUpdate(tableId, cart);
                 return R.data(cart);
             }
             if (quantity == 0) {
-                CartDTO cart = cartService.updateTablewareQuantity(tableId, 0);
+                WechatCartDTO cart = cartService.updateTablewareQuantity(tableId, 0);
                 sseService.pushCartUpdate(tableId, cart);
                 return R.data(cart);
             }
-            CartDTO cart = cartService.updateTablewareQuantity(tableId, quantity);
+            WechatCartDTO cart = cartService.updateTablewareQuantity(tableId, quantity);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
             // 推送购物车更新消息(WebSocket)
@@ -231,7 +233,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "创建订单(下单)", notes = "从购物车创建订单,不立即支付。备注:创建/更新时传入,更新订单(加餐)时为覆盖")
     @PostMapping("/create")
-    public R<shop.alien.entity.store.vo.OrderSuccessVO> createOrder(@Valid @RequestBody CreateOrderDTO dto) {
+    public R<WechatOrderSuccessVO> createOrder(@Valid @RequestBody WechatCreateOrderDTO dto) {
         log.info("StoreOrderController.createOrder?dto={}", dto);
         try {
             // 验证 token(用户信息在 StoreOrderService 中从 token 获取)
@@ -245,10 +247,10 @@ public class StoreOrderController {
 
             // 设置不立即支付
             dto.setImmediatePay(0);
-            StoreOrder order = orderService.createOrder(dto);
+            WechatStoreOrder order = orderService.createOrder(dto);
 
             // 转换为OrderSuccessVO
-            shop.alien.entity.store.vo.OrderSuccessVO vo = new shop.alien.entity.store.vo.OrderSuccessVO();
+            WechatOrderSuccessVO vo = new WechatOrderSuccessVO();
             vo.setOrderId(order.getId());
             vo.setOrderNo(order.getOrderNo());
             vo.setTableNumber(order.getTableNumber());
@@ -281,7 +283,7 @@ public class StoreOrderController {
                 return R.fail("用户未登录");
             }
             
-            StoreOrder order = orderService.getOrderById(orderId);
+            WechatStoreOrder order = orderService.getOrderById(orderId);
             if (order == null) {
                 return R.fail("订单不存在");
             }
@@ -329,7 +331,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "查询订单详情", notes = "根据订单ID查询订单详情,包含订单基本信息和按批次分组的变更记录,用于展示每次下单/加餐都加了什么商品")
     @GetMapping("/detail/{orderId}")
-    public R<OrderDetailWithChangeLogVO> getOrderDetail(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+    public R<WechatOrderDetailWithChangeLogVO> getOrderDetail(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         log.info("StoreOrderController.getOrderDetail?orderId={}", orderId);
         try {
             // 从 token 获取用户信息
@@ -337,7 +339,7 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            OrderDetailWithChangeLogVO orderDetail = orderService.getOrderDetailWithChangeLog(orderId);
+            WechatOrderDetailWithChangeLogVO orderDetail = orderService.getOrderDetailWithChangeLog(orderId);
             return R.data(orderDetail);
         } catch (Exception e) {
             log.error("查询订单详情失败: {}", e.getMessage(), e);
@@ -347,7 +349,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "查询订单明细", notes = "根据订单ID查询订单明细列表")
     @GetMapping("/detail/list/{orderId}")
-    public R<List<StoreOrderDetail>> getOrderDetailList(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+    public R<List<WechatStoreOrderDetail>> getOrderDetailList(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         log.info("StoreOrderController.getOrderDetailList?orderId={}", orderId);
         try {
             // 从 token 获取用户信息
@@ -355,12 +357,12 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StoreOrderDetail> wrapper =
+            com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<WechatStoreOrderDetail> wrapper =
                     new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
-            wrapper.eq(StoreOrderDetail::getOrderId, orderId);
-            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
-            wrapper.orderByDesc(StoreOrderDetail::getCreatedTime);
-            List<StoreOrderDetail> details = orderDetailMapper.selectList(wrapper);
+            wrapper.eq(WechatStoreOrderDetail::getOrderId, orderId);
+            wrapper.eq(WechatStoreOrderDetail::getDeleteFlag, 0);
+            wrapper.orderByDesc(WechatStoreOrderDetail::getCreatedTime);
+            List<WechatStoreOrderDetail> details = orderDetailMapper.selectList(wrapper);
             return R.data(details);
         } catch (Exception e) {
             log.error("查询订单明细失败: {}", e.getMessage(), e);
@@ -370,7 +372,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "查询订单信息", notes = "根据订单ID查询订单完整信息(包含订单基本信息、菜品清单、价格明细)")
     @GetMapping("/info/{orderId}")
-    public R<OrderInfoVO> getOrderInfo(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+    public R<WechatOrderInfoVO> getOrderInfo(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         log.info("StoreOrderController.getOrderInfo?orderId={}", orderId);
         try {
             // 从 token 获取用户信息
@@ -378,7 +380,7 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            OrderInfoVO orderInfo = orderService.getOrderInfo(orderId);
+            WechatOrderInfoVO orderInfo = orderService.getOrderInfo(orderId);
             return R.data(orderInfo);
         } catch (Exception e) {
             log.error("查询订单信息失败: {}", e.getMessage(), e);
@@ -388,7 +390,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "查询订单变更记录", notes = "根据订单ID查询订单的所有变更记录(按批次分组),用于展示每次下单/加餐都加了什么商品")
     @GetMapping("/change-log/{orderId}")
-    public R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+    public R<List<WechatOrderChangeLogBatchVO>> getOrderChangeLogs(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         log.info("StoreOrderController.getOrderChangeLogs?orderId={}", orderId);
         try {
             // 从 token 获取用户信息
@@ -396,7 +398,7 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            List<OrderChangeLogBatchVO> changeLogs = orderService.getOrderChangeLogs(orderId);
+            List<WechatOrderChangeLogBatchVO> changeLogs = orderService.getOrderChangeLogs(orderId);
             return R.data(changeLogs);
         } catch (Exception e) {
             log.error("查询订单变更记录失败: {}", e.getMessage(), e);
@@ -407,7 +409,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表,包含订单中的菜品数量、菜品名称、菜品图片。支持按订单编号或菜品名称搜索(限15字)")
     @GetMapping("/page")
-    public R<IPage<StoreOrderPageVO>> getOrderPage(
+    public R<IPage<WechatStoreOrderPageVO>> getOrderPage(
             @ApiParam(value = "页码", required = true) @RequestParam(defaultValue = "1") Long current,
             @ApiParam(value = "每页数量", required = true) @RequestParam(defaultValue = "10") Long size,
             @ApiParam(value = "门店ID") @RequestParam(required = false) Integer storeId,
@@ -421,8 +423,8 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Page<StoreOrder> page = new Page<>(current, size);
-            IPage<StoreOrderPageVO> result = orderService.getOrderPageWithCuisines(page, storeId, tableId, orderStatus, keyword);
+            Page<WechatStoreOrder> page = new Page<>(current, size);
+            IPage<WechatStoreOrderPageVO> result = orderService.getOrderPageWithCuisines(page, storeId, tableId, orderStatus, keyword);
             return R.data(result);
         } catch (Exception e) {
             log.error("分页查询订单列表失败: {}", e.getMessage(), e);
@@ -432,7 +434,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "查询我的订单", notes = "通过标识查询我的未支付订单或历史订单,包含订单中的菜品数量、菜品名称、菜品图片。type: 0或unpaid-未支付订单, 1或history-历史订单")
     @GetMapping("/my-orders")
-    public R<IPage<StoreOrderPageVO>> getMyOrders(
+    public R<IPage<WechatStoreOrderPageVO>> getMyOrders(
             @ApiParam(value = "页码", required = true) @RequestParam(defaultValue = "1") Long current,
             @ApiParam(value = "每页数量", required = true) @RequestParam(defaultValue = "10") Long size,
             @ApiParam(value = "订单类型(0或unpaid:未支付订单, 1或history:历史订单)", required = true) @RequestParam String type) {
@@ -443,8 +445,8 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Page<StoreOrder> page = new Page<>(current, size);
-            IPage<StoreOrderPageVO> result = orderService.getMyOrdersWithCuisines(page, type);
+            Page<WechatStoreOrder> page = new Page<>(current, size);
+            IPage<WechatStoreOrderPageVO> result = orderService.getMyOrdersWithCuisines(page, type);
             return R.data(result);
         } catch (Exception e) {
             log.error("查询我的订单失败: {}", e.getMessage(), e);
@@ -454,7 +456,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "换桌", notes = "换桌并迁移购物车、未完成的订单以及其他关联表数据")
     @PostMapping("/change-table")
-    public R<CartDTO> changeTable(@Valid @RequestBody ChangeTableDTO dto) {
+    public R<WechatCartDTO> changeTable(@Valid @RequestBody WechatChangeTableDTO dto) {
         log.info("StoreOrderController.changeTable?dto={}", dto);
         try {
             // 从 token 获取用户信息
@@ -464,7 +466,7 @@ public class StoreOrderController {
             }
 
             // 调用Service层处理换桌业务逻辑
-            CartDTO cart = orderService.changeTable(dto.getFromTableId(), dto.getToTableId(), dto.getChangeReason(), userId);
+            WechatCartDTO cart = orderService.changeTable(dto.getFromTableId(), dto.getToTableId(), dto.getChangeReason(), userId);
 
             return R.data(cart);
         } catch (Exception e) {
@@ -523,7 +525,7 @@ public class StoreOrderController {
 
     @ApiOperation(value = "更新订单优惠券", notes = "重新选择优惠券")
     @PostMapping("/update-coupon")
-    public R<StoreOrder> updateOrderCoupon(@Valid @RequestBody shop.alien.entity.store.dto.UpdateOrderCouponDTO dto) {
+    public R<WechatStoreOrder> updateOrderCoupon(@Valid @RequestBody WechatUpdateOrderCouponDTO dto) {
         log.info("StoreOrderController.updateOrderCoupon?dto={}", dto);
         try {
             // 从 token 获取用户信息
@@ -531,7 +533,7 @@ public class StoreOrderController {
             if (userId == null) {
                 return R.fail("用户未登录");
             }
-            StoreOrder order = orderService.updateOrderCoupon(dto.getOrderId(), dto.getCouponId());
+            WechatStoreOrder order = orderService.updateOrderCoupon(dto.getOrderId(), dto.getCouponId());
             return R.data(order);
         } catch (Exception e) {
             log.error("更新订单优惠券失败: {}", e.getMessage(), e);
@@ -552,14 +554,14 @@ public class StoreOrderController {
             boolean result = orderService.resetTable(tableId);
             if (result) {
                 // 推送购物车更新消息(清空购物车)
-                CartDTO emptyCart = new CartDTO();
+                WechatCartDTO emptyCart = new WechatCartDTO();
                 emptyCart.setTableId(tableId);
                 emptyCart.setItems(java.util.Collections.emptyList());
                 emptyCart.setTotalAmount(java.math.BigDecimal.ZERO);
                 emptyCart.setTotalQuantity(0);
                 
                 // 查询桌号信息
-                StoreTable table = storeTableMapper.selectById(tableId);
+                WechatStoreTable table = storeTableMapper.selectById(tableId);
                 if (table != null) {
                     emptyCart.setTableNumber(table.getTableNumber());
                     emptyCart.setStoreId(table.getStoreId());

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

@@ -1,7 +1,7 @@
 package shop.alien.dining.service;
 
-import shop.alien.entity.store.dto.AddCartItemDTO;
-import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.wechat.WechatAddCartItemDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
 
 /**
  * 购物车服务接口
@@ -17,7 +17,7 @@ public interface CartService {
      * @param tableId 桌号ID
      * @return 购物车信息
      */
-    CartDTO getCart(Integer tableId);
+    WechatCartDTO getCart(Integer tableId);
 
     /**
      * 添加商品到购物车
@@ -25,7 +25,7 @@ public interface CartService {
      * @param dto 添加购物车商品DTO
      * @return 购物车信息
      */
-    CartDTO addItem(AddCartItemDTO dto);
+    WechatCartDTO addItem(WechatAddCartItemDTO dto);
 
     /**
      * 更新购物车商品数量
@@ -35,7 +35,7 @@ public interface CartService {
      * @param quantity  数量
      * @return 购物车信息
      */
-    CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity);
+    WechatCartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity);
 
     /**
      * 删除购物车商品
@@ -44,7 +44,7 @@ public interface CartService {
      * @param cuisineId 菜品ID
      * @return 购物车信息
      */
-    CartDTO removeItem(Integer tableId, Integer cuisineId);
+    WechatCartDTO removeItem(Integer tableId, Integer cuisineId);
 
     /**
      * 清空购物车
@@ -60,7 +60,7 @@ public interface CartService {
      * @param toTableId   目标桌号ID
      * @return 迁移后的购物车信息
      */
-    CartDTO migrateCart(Integer fromTableId, Integer toTableId);
+    WechatCartDTO migrateCart(Integer fromTableId, Integer toTableId);
 
     /**
      * 检查桌号是否已使用优惠券
@@ -92,7 +92,7 @@ public interface CartService {
      * @param dinerCount 用餐人数
      * @return 购物车信息
      */
-    CartDTO setDinerCount(Integer tableId, Integer dinerCount);
+    WechatCartDTO setDinerCount(Integer tableId, Integer dinerCount);
 
     /**
      * 更新餐具数量
@@ -101,7 +101,7 @@ public interface CartService {
      * @param quantity 餐具数量
      * @return 购物车信息
      */
-    CartDTO updateTablewareQuantity(Integer tableId, Integer quantity);
+    WechatCartDTO updateTablewareQuantity(Integer tableId, Integer quantity);
 
     /**
      * 锁定购物车商品数量(下单时调用,将当前数量锁定,不允许减少或删除)
@@ -109,7 +109,7 @@ public interface CartService {
      * @param tableId 桌号ID
      * @return 购物车信息
      */
-    CartDTO lockCartItems(Integer tableId);
+    WechatCartDTO lockCartItems(Integer tableId);
 
     /**
      * 解锁购物车商品数量(取消订单时调用,清除已下单数量,允许重新下单)
@@ -117,5 +117,5 @@ public interface CartService {
      * @param tableId 桌号ID
      * @return 购物车信息
      */
-    CartDTO unlockCartItems(Integer tableId);
+    WechatCartDTO unlockCartItems(Integer tableId);
 }

+ 2 - 2
alien-dining/src/main/java/shop/alien/dining/service/DiningService.java

@@ -87,7 +87,7 @@ public interface DiningService {
      * @param userId 用户ID
      * @return 订单确认页面信息
      */
-    OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId);
+    shop.alien.entity.store.vo.wechat.WechatOrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId);
 
     /**
      * 锁定订单(防止多人同时下单)
@@ -121,7 +121,7 @@ public interface DiningService {
      * @param userId 用户ID
      * @return 订单结算确认页面信息
      */
-    shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId);
+    shop.alien.entity.store.vo.wechat.WechatOrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId);
 
     /**
      * 锁定订单结算(防止多人同时结算)

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

@@ -1,10 +1,10 @@
 package shop.alien.dining.service;
 
-import shop.alien.entity.store.StoreCuisine;
-import shop.alien.entity.store.StoreCuisineCategory;
-import shop.alien.entity.store.StoreTable;
-import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCategory;
+import shop.alien.entity.store.wechat.WechatStoreTable;
+import shop.alien.entity.store.dto.wechat.WechatStoreInfoWithHomepageCuisinesDTO;
+import shop.alien.entity.store.vo.wechat.WechatCategoryWithCuisinesVO;
 
 import java.util.List;
 
@@ -22,7 +22,7 @@ public interface StoreInfoService {
      * @param storeId 门店ID
      * @return 桌号列表
      */
-    List<StoreTable> getTablesByStoreId(Integer storeId);
+    List<WechatStoreTable> getTablesByStoreId(Integer storeId);
 
     /**
      * 根据门店ID查询菜品种类列表
@@ -30,7 +30,7 @@ public interface StoreInfoService {
      * @param storeId 门店ID
      * @return 菜品种类列表
      */
-    List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId);
+    List<WechatStoreCuisineCategory> getCategoriesByStoreId(Integer storeId);
 
     /**
      * 根据菜品种类ID查询菜品信息列表
@@ -38,7 +38,7 @@ public interface StoreInfoService {
      * @param categoryId 菜品种类ID
      * @return 菜品信息列表
      */
-    List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId);
+    List<WechatStoreCuisine> getCuisinesByCategoryId(Integer categoryId);
 
     /**
      * 根据门店ID查询菜品种类及每个分类下的菜品列表(一次返回种类+菜品)
@@ -47,7 +47,7 @@ public interface StoreInfoService {
      * @param keyword 菜品名称模糊查询关键词(可选,为空则不按名称筛选)
      * @return 菜品种类及下属菜品列表
      */
-    List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword);
+    List<WechatCategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword);
 
     /**
      * 删除菜品种类:仅逻辑删除分类并解除菜品与该分类的绑定关系,价目表(菜品)本身不改动
@@ -63,5 +63,5 @@ public interface StoreInfoService {
      * @param storeId 商铺ID
      * @return 店铺信息和首页展示美食价目表信息
      */
-    StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId);
+    WechatStoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId);
 }

+ 21 - 19
alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java

@@ -3,11 +3,13 @@ package shop.alien.dining.service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
-import shop.alien.entity.store.StoreOrder;
-import shop.alien.entity.store.dto.CreateOrderDTO;
-import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
-import shop.alien.entity.store.vo.OrderInfoVO;
-import shop.alien.entity.store.vo.StoreOrderPageVO;
+import shop.alien.entity.store.wechat.WechatStoreOrder;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
+import shop.alien.entity.store.dto.wechat.WechatCreateOrderDTO;
+import shop.alien.entity.store.vo.wechat.WechatOrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderInfoVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderDetailWithChangeLogVO;
+import shop.alien.entity.store.vo.wechat.WechatStoreOrderPageVO;
 
 import java.util.List;
 
@@ -17,7 +19,7 @@ import java.util.List;
  * @author system
  * @since 2025-01-XX
  */
-public interface StoreOrderService extends IService<StoreOrder> {
+public interface StoreOrderService extends IService<WechatStoreOrder> {
 
     /**
      * 创建订单
@@ -25,7 +27,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param dto 创建订单DTO
      * @return 订单信息
      */
-    StoreOrder createOrder(CreateOrderDTO dto);
+    WechatStoreOrder createOrder(WechatCreateOrderDTO dto);
 
     /**
      * 支付订单
@@ -34,7 +36,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param payType 支付方式
      * @return 订单信息
      */
-    StoreOrder payOrder(Integer orderId, Integer payType);
+    WechatStoreOrder payOrder(Integer orderId, Integer payType);
 
     /**
      * 取消订单
@@ -50,7 +52,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param orderNo 订单号
      * @return 订单信息
      */
-    StoreOrder getOrderByOrderNo(String orderNo);
+    WechatStoreOrder getOrderByOrderNo(String orderNo);
 
     /**
      * 根据ID查询订单
@@ -58,7 +60,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param orderId 订单ID
      * @return 订单信息
      */
-    StoreOrder getOrderById(Integer orderId);
+    WechatStoreOrder getOrderById(Integer orderId);
 
     /**
      * 分页查询订单列表
@@ -70,7 +72,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param keyword  搜索关键词(订单编号或菜品名称,限15字)
      * @return 订单分页列表
      */
-    IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
+    IPage<WechatStoreOrder> getOrderPage(Page<WechatStoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
 
     /**
      * 分页查询订单列表(包含菜品信息)
@@ -82,7 +84,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param keyword  搜索关键词(订单编号或菜品名称,限15字)
      * @return 订单分页列表(包含菜品信息)
      */
-    IPage<StoreOrderPageVO> getOrderPageWithCuisines(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
+    IPage<WechatStoreOrderPageVO> getOrderPageWithCuisines(Page<WechatStoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
 
     /**
      * 完成订单(支付完成后调用)
@@ -107,7 +109,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param couponId 优惠券ID(可为空,表示不使用优惠券)
      * @return 订单信息
      */
-    StoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
+    WechatStoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
 
     /**
      * 管理员重置餐桌(删除购物车数据、未支付/已取消的订单数据,并重置餐桌表)
@@ -124,7 +126,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param orderId 订单ID
      * @return 订单信息
      */
-    OrderInfoVO getOrderInfo(Integer orderId);
+    WechatOrderInfoVO getOrderInfo(Integer orderId);
 
     /**
      * 查询订单的所有变更记录(按批次分组)
@@ -132,7 +134,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param orderId 订单ID
      * @return 变更记录批次列表
      */
-    List<OrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId);
+    List<WechatOrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId);
 
     /**
      * 查询订单详情(包含订单基本信息和按批次分组的变更记录)
@@ -140,7 +142,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param orderId 订单ID
      * @return 订单详情(包含变更记录)
      */
-    shop.alien.entity.store.vo.OrderDetailWithChangeLogVO getOrderDetailWithChangeLog(Integer orderId);
+    WechatOrderDetailWithChangeLogVO getOrderDetailWithChangeLog(Integer orderId);
 
     /**
      * 支付完成后重置餐桌(保留订单数据,只重置餐桌绑定关系)
@@ -156,7 +158,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param type 订单类型(0或"unpaid":未支付订单, 1或"history":历史订单)
      * @return 订单分页列表
      */
-    IPage<StoreOrder> getMyOrders(Page<StoreOrder> page, String type);
+    IPage<WechatStoreOrder> getMyOrders(Page<WechatStoreOrder> page, String type);
 
     /**
      * 查询我的订单(包含菜品信息)
@@ -165,7 +167,7 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param type 订单类型(0或"unpaid":未支付订单, 1或"history":历史订单)
      * @return 订单分页列表(包含菜品信息)
      */
-    IPage<StoreOrderPageVO> getMyOrdersWithCuisines(Page<StoreOrder> page, String type);
+    IPage<WechatStoreOrderPageVO> getMyOrdersWithCuisines(Page<WechatStoreOrder> page, String type);
 
     /**
      * 换桌时迁移所有关联数据(订单、订单变更记录、优惠券使用记录等)
@@ -185,5 +187,5 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @param userId       操作用户ID
      * @return 迁移后的购物车信息
      */
-    shop.alien.entity.store.dto.CartDTO changeTable(Integer fromTableId, Integer toTableId, String changeReason, Integer userId);
+    WechatCartDTO changeTable(Integer fromTableId, Integer toTableId, String changeReason, Integer userId);
 }

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

@@ -9,19 +9,19 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
-import shop.alien.entity.store.StoreCart;
-import shop.alien.entity.store.StoreCouponUsage;
-import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.wechat.WechatStoreCart;
+import shop.alien.entity.store.wechat.WechatStoreCouponUsage;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
 import shop.alien.entity.store.StoreInfo;
-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.StoreCuisineMapper;
+import shop.alien.entity.store.wechat.WechatStoreTable;
+import shop.alien.entity.store.dto.wechat.WechatAddCartItemDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartItemDTO;
+import shop.alien.mapper.wechat.WechatStoreCartMapper;
+import shop.alien.mapper.wechat.WechatStoreCouponUsageMapper;
+import shop.alien.mapper.wechat.WechatStoreCuisineMapper;
 import shop.alien.mapper.StoreInfoMapper;
-import shop.alien.mapper.StoreTableMapper;
+import shop.alien.mapper.wechat.WechatStoreTableMapper;
 import shop.alien.dining.util.TokenUtil;
 
 import java.math.BigDecimal;
@@ -44,8 +44,8 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class CartServiceImpl implements CartService {
 
-    private static final String CART_KEY_PREFIX = "cart:table:";
-    private static final String COUPON_USED_KEY_PREFIX = "coupon:used:table:";
+    private static final String CART_KEY_PREFIX = "wechat:cart:table:";
+    private static final String COUPON_USED_KEY_PREFIX = "wechat:coupon:used:table:";
     private static final int CART_EXPIRE_SECONDS = 24 * 60 * 60; // 24小时过期
 
     // 异步写入数据库的线程池(专门用于购物车数据库写入)
@@ -56,23 +56,23 @@ public class CartServiceImpl implements CartService {
     });
 
     private final BaseRedisService baseRedisService;
-    private final StoreTableMapper storeTableMapper;
-    private final StoreCuisineMapper storeCuisineMapper;
-    private final StoreCartMapper storeCartMapper;
-    private final StoreCouponUsageMapper storeCouponUsageMapper;
+    private final WechatStoreTableMapper storeTableMapper;
+    private final WechatStoreCuisineMapper storeCuisineMapper;
+    private final WechatStoreCartMapper storeCartMapper;
+    private final WechatStoreCouponUsageMapper storeCouponUsageMapper;
     private final StoreInfoMapper storeInfoMapper;
 
     @Override
-    public CartDTO getCart(Integer tableId) {
+    public WechatCartDTO getCart(Integer tableId) {
         log.info("获取购物车, tableId={}", tableId);
         String cartKey = CART_KEY_PREFIX + tableId;
         String cartJson = baseRedisService.getString(cartKey);
 
-        CartDTO cart = new CartDTO();
+        WechatCartDTO cart = new WechatCartDTO();
         cart.setTableId(tableId);
 
         // 查询桌号信息
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable table = storeTableMapper.selectById(tableId);
         if (table != null) {
             cart.setTableNumber(table.getTableNumber());
             cart.setStoreId(table.getStoreId());
@@ -81,15 +81,15 @@ public class CartServiceImpl implements CartService {
         if (StringUtils.hasText(cartJson)) {
             try {
                 JSONObject cartObj = JSON.parseObject(cartJson);
-                List<CartItemDTO> items = cartObj.getList("items", CartItemDTO.class);
+                List<WechatCartItemDTO> items = cartObj.getList("items", WechatCartItemDTO.class);
                 if (items != null) {
                     cart.setItems(items);
                     // 计算总金额和总数量
                     BigDecimal totalAmount = items.stream()
-                            .map(CartItemDTO::getSubtotalAmount)
+                            .map(WechatCartItemDTO::getSubtotalAmount)
                             .reduce(BigDecimal.ZERO, BigDecimal::add);
                     Integer totalQuantity = items.stream()
-                            .mapToInt(CartItemDTO::getQuantity)
+                            .mapToInt(WechatCartItemDTO::getQuantity)
                             .sum();
                     cart.setTotalAmount(totalAmount);
                     cart.setTotalQuantity(totalQuantity);
@@ -115,30 +115,30 @@ public class CartServiceImpl implements CartService {
     /**
      * 从数据库加载购物车
      */
-    private CartDTO loadCartFromDatabase(Integer tableId) {
+    private WechatCartDTO loadCartFromDatabase(Integer tableId) {
         log.info("从数据库加载购物车, tableId={}", tableId);
-        CartDTO cart = new CartDTO();
+        WechatCartDTO cart = new WechatCartDTO();
         cart.setTableId(tableId);
         cart.setItems(new ArrayList<>());
         cart.setTotalAmount(BigDecimal.ZERO);
         cart.setTotalQuantity(0);
 
         // 查询桌号信息
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable 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);
-        List<StoreCart> cartList = storeCartMapper.selectList(wrapper);
+        LambdaQueryWrapper<WechatStoreCart> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCart::getTableId, tableId);
+        wrapper.eq(WechatStoreCart::getDeleteFlag, 0);
+        List<WechatStoreCart> cartList = storeCartMapper.selectList(wrapper);
 
         if (cartList != null && !cartList.isEmpty()) {
-            List<CartItemDTO> items = cartList.stream().map(cartItem -> {
-                CartItemDTO item = new CartItemDTO();
+            List<WechatCartItemDTO> items = cartList.stream().map(cartItem -> {
+                WechatCartItemDTO item = new WechatCartItemDTO();
                 item.setCuisineId(cartItem.getCuisineId());
                 item.setCuisineName(cartItem.getCuisineName());
                 item.setCuisineImage(cartItem.getCuisineImage());
@@ -154,10 +154,10 @@ public class CartServiceImpl implements CartService {
 
             cart.setItems(items);
             BigDecimal totalAmount = items.stream()
-                    .map(CartItemDTO::getSubtotalAmount)
+                    .map(WechatCartItemDTO::getSubtotalAmount)
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
             Integer totalQuantity = items.stream()
-                    .mapToInt(CartItemDTO::getQuantity)
+                    .mapToInt(WechatCartItemDTO::getQuantity)
                     .sum();
             cart.setTotalAmount(totalAmount);
             cart.setTotalQuantity(totalQuantity);
@@ -170,16 +170,16 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO addItem(AddCartItemDTO dto) {
+    public WechatCartDTO addItem(WechatAddCartItemDTO dto) {
         log.info("添加商品到购物车, dto={}", dto);
         // 验证桌号
-        StoreTable table = storeTableMapper.selectById(dto.getTableId());
+        WechatStoreTable table = storeTableMapper.selectById(dto.getTableId());
         if (table == null) {
             throw new RuntimeException("桌号不存在");
         }
 
         // 验证菜品
-        StoreCuisine cuisine = storeCuisineMapper.selectById(dto.getCuisineId());
+        WechatStoreCuisine cuisine = storeCuisineMapper.selectById(dto.getCuisineId());
         if (cuisine == null) {
             throw new RuntimeException("菜品不存在");
         }
@@ -192,11 +192,11 @@ public class CartServiceImpl implements CartService {
         String userPhone = TokenUtil.getCurrentUserPhone();
 
         // 获取购物车
-        CartDTO cart = getCart(dto.getTableId());
+        WechatCartDTO cart = getCart(dto.getTableId());
 
         // 查找是否已存在该商品
-        List<CartItemDTO> items = cart.getItems();
-        CartItemDTO existingItem = items.stream()
+        List<WechatCartItemDTO> items = cart.getItems();
+        WechatCartItemDTO existingItem = items.stream()
                 .filter(item -> item.getCuisineId().equals(dto.getCuisineId()))
                 .findFirst()
                 .orElse(null);
@@ -220,7 +220,7 @@ public class CartServiceImpl implements CartService {
             }
         } else {
             // 添加新商品
-            CartItemDTO newItem = new CartItemDTO();
+            WechatCartItemDTO newItem = new WechatCartItemDTO();
             newItem.setCuisineId(cuisine.getId());
             newItem.setCuisineName(cuisine.getName());
             newItem.setCuisineType(cuisine.getCuisineType());
@@ -237,10 +237,10 @@ public class CartServiceImpl implements CartService {
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
-                .map(CartItemDTO::getSubtotalAmount)
+                .map(WechatCartItemDTO::getSubtotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
         Integer totalQuantity = items.stream()
-                .mapToInt(CartItemDTO::getQuantity)
+                .mapToInt(WechatCartItemDTO::getQuantity)
                 .sum();
         cart.setTotalAmount(totalAmount);
         cart.setTotalQuantity(totalQuantity);
@@ -252,7 +252,7 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity) {
+    public WechatCartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity) {
         log.info("更新购物车商品数量, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
         
         // 如果数量为0或小于0,删除该商品
@@ -261,9 +261,9 @@ public class CartServiceImpl implements CartService {
             return removeItem(tableId, cuisineId);
         }
 
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
-        CartItemDTO item = items.stream()
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
+        WechatCartItemDTO item = items.stream()
                 .filter(i -> i.getCuisineId().equals(cuisineId))
                 .findFirst()
                 .orElse(null);
@@ -284,10 +284,10 @@ public class CartServiceImpl implements CartService {
 
             // 重新计算总金额和总数量
             BigDecimal totalAmount = items.stream()
-                    .map(CartItemDTO::getSubtotalAmount)
+                    .map(WechatCartItemDTO::getSubtotalAmount)
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
             Integer totalQuantity = items.stream()
-                    .mapToInt(CartItemDTO::getQuantity)
+                    .mapToInt(WechatCartItemDTO::getQuantity)
                     .sum();
             cart.setTotalAmount(totalAmount);
             cart.setTotalQuantity(totalQuantity);
@@ -298,7 +298,7 @@ public class CartServiceImpl implements CartService {
             log.info("商品不在购物车中,自动添加, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
             
             // 验证菜品
-            StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+            WechatStoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
             if (cuisine == null) {
                 throw new RuntimeException("菜品不存在");
             }
@@ -311,7 +311,7 @@ public class CartServiceImpl implements CartService {
             String userPhone = TokenUtil.getCurrentUserPhone();
 
             // 创建新的购物车商品项
-            CartItemDTO newItem = new CartItemDTO();
+            WechatCartItemDTO newItem = new WechatCartItemDTO();
             newItem.setCuisineId(cuisine.getId());
             newItem.setCuisineName(cuisine.getName());
             newItem.setCuisineType(cuisine.getCuisineType());
@@ -326,10 +326,10 @@ public class CartServiceImpl implements CartService {
 
             // 重新计算总金额和总数量
             BigDecimal totalAmount = items.stream()
-                    .map(CartItemDTO::getSubtotalAmount)
+                    .map(WechatCartItemDTO::getSubtotalAmount)
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
             Integer totalQuantity = items.stream()
-                    .mapToInt(CartItemDTO::getQuantity)
+                    .mapToInt(WechatCartItemDTO::getQuantity)
                     .sum();
             cart.setTotalAmount(totalAmount);
             cart.setTotalQuantity(totalQuantity);
@@ -343,13 +343,13 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO removeItem(Integer tableId, Integer cuisineId) {
+    public WechatCartDTO removeItem(Integer tableId, Integer cuisineId) {
         log.info("删除购物车商品, tableId={}, cuisineId={}", tableId, cuisineId);
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
         
         // 检查是否有已下单数量,如果有则不允许删除
-        CartItemDTO item = items.stream()
+        WechatCartItemDTO item = items.stream()
                 .filter(i -> i.getCuisineId().equals(cuisineId))
                 .findFirst()
                 .orElse(null);
@@ -365,10 +365,10 @@ public class CartServiceImpl implements CartService {
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
-                .map(CartItemDTO::getSubtotalAmount)
+                .map(WechatCartItemDTO::getSubtotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
         Integer totalQuantity = items.stream()
-                .mapToInt(CartItemDTO::getQuantity)
+                .mapToInt(WechatCartItemDTO::getQuantity)
                 .sum();
         cart.setTotalAmount(totalAmount);
         cart.setTotalQuantity(totalQuantity);
@@ -382,8 +382,8 @@ public class CartServiceImpl implements CartService {
         log.info("清空购物车(保留已下单商品), tableId={}", tableId);
         
         // 获取购物车
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
         
         if (items == null || items.isEmpty()) {
             log.info("购物车为空,无需清空, tableId={}", tableId);
@@ -391,13 +391,13 @@ public class CartServiceImpl implements CartService {
         }
         
         // 分离已下单的商品、未下单的商品和餐具
-        List<CartItemDTO> orderedItems = new ArrayList<>(); // 已下单的商品(保留,数量恢复为已下单数量)
+        List<WechatCartItemDTO> orderedItems = new ArrayList<>(); // 已下单的商品(保留,数量恢复为已下单数量)
         List<Integer> orderedCuisineIds = new ArrayList<>(); // 已下单的商品ID列表
-        List<CartItemDTO> unorderedItems = new ArrayList<>(); // 未下单的商品(删除)
-        CartItemDTO tablewareItem = null; // 餐具项(始终保留)
+        List<WechatCartItemDTO> unorderedItems = new ArrayList<>(); // 未下单的商品(删除)
+        WechatCartItemDTO tablewareItem = null; // 餐具项(始终保留)
         boolean hasChanges = false; // 是否有变化(需要更新)
         
-        for (CartItemDTO item : items) {
+        for (WechatCartItemDTO item : items) {
             // 餐具始终保留,不清空
             if (TABLEWARE_CUISINE_ID.equals(item.getCuisineId())) {
                 tablewareItem = item;
@@ -449,10 +449,10 @@ public class CartServiceImpl implements CartService {
             cart.setItems(orderedItems);
             // 重新计算总金额和总数量(只计算保留的商品,数量已恢复为已下单数量)
             BigDecimal totalAmount = orderedItems.stream()
-                    .map(CartItemDTO::getSubtotalAmount)
+                    .map(WechatCartItemDTO::getSubtotalAmount)
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
             Integer totalQuantity = orderedItems.stream()
-                    .mapToInt(CartItemDTO::getQuantity)
+                    .mapToInt(WechatCartItemDTO::getQuantity)
                     .sum();
             cart.setTotalAmount(totalAmount);
             cart.setTotalQuantity(totalQuantity);
@@ -469,19 +469,19 @@ public class CartServiceImpl implements CartService {
             
             // 2. 从数据库中逻辑删除未下单的商品(排除餐具)
             if (!unorderedItems.isEmpty()) {
-                LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
-                wrapper.eq(StoreCart::getTableId, tableId);
-                wrapper.eq(StoreCart::getDeleteFlag, 0);
+                LambdaQueryWrapper<WechatStoreCart> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(WechatStoreCart::getTableId, tableId);
+                wrapper.eq(WechatStoreCart::getDeleteFlag, 0);
                 // 排除餐具(cuisineId = -1)
-                wrapper.ne(StoreCart::getCuisineId, TABLEWARE_CUISINE_ID);
+                wrapper.ne(WechatStoreCart::getCuisineId, TABLEWARE_CUISINE_ID);
                 if (!orderedCuisineIds.isEmpty()) {
                     // 排除已下单的商品ID(包括餐具)
-                    wrapper.notIn(StoreCart::getCuisineId, orderedCuisineIds);
+                    wrapper.notIn(WechatStoreCart::getCuisineId, orderedCuisineIds);
                 }
-                List<StoreCart> cartListToDelete = storeCartMapper.selectList(wrapper);
+                List<WechatStoreCart> cartListToDelete = storeCartMapper.selectList(wrapper);
                 if (cartListToDelete != null && !cartListToDelete.isEmpty()) {
                     List<Integer> cartIds = cartListToDelete.stream()
-                            .map(StoreCart::getId)
+                            .map(WechatStoreCart::getId)
                             .collect(Collectors.toList());
                     // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
                     storeCartMapper.deleteBatchIds(cartIds);
@@ -496,7 +496,7 @@ public class CartServiceImpl implements CartService {
             }
             
             // 4. 更新桌号表的购物车统计
-            StoreTable table = storeTableMapper.selectById(tableId);
+            WechatStoreTable table = storeTableMapper.selectById(tableId);
             if (table != null) {
                 table.setCartItemCount(totalQuantity);
                 table.setCartTotalAmount(totalAmount);
@@ -511,24 +511,24 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO migrateCart(Integer fromTableId, Integer toTableId) {
+    public WechatCartDTO migrateCart(Integer fromTableId, Integer toTableId) {
         log.info("迁移购物车, fromTableId={}, toTableId={}", fromTableId, toTableId);
         // 获取原购物车
-        CartDTO fromCart = getCart(fromTableId);
+        WechatCartDTO fromCart = getCart(fromTableId);
 
         // 验证目标桌号
-        StoreTable toTable = storeTableMapper.selectById(toTableId);
+        WechatStoreTable toTable = storeTableMapper.selectById(toTableId);
         if (toTable == null) {
             throw new RuntimeException("目标桌号不存在");
         }
 
         // 获取目标购物车
-        CartDTO toCart = getCart(toTableId);
+        WechatCartDTO toCart = getCart(toTableId);
 
         // 合并购物车(如果目标桌号已有商品,则合并)
-        List<CartItemDTO> mergedItems = new ArrayList<>(toCart.getItems());
-        for (CartItemDTO fromItem : fromCart.getItems()) {
-            CartItemDTO existingItem = mergedItems.stream()
+        List<WechatCartItemDTO> mergedItems = new ArrayList<>(toCart.getItems());
+        for (WechatCartItemDTO fromItem : fromCart.getItems()) {
+            WechatCartItemDTO existingItem = mergedItems.stream()
                     .filter(item -> item.getCuisineId().equals(fromItem.getCuisineId()))
                     .findFirst()
                     .orElse(null);
@@ -556,10 +556,10 @@ public class CartServiceImpl implements CartService {
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = mergedItems.stream()
-                .map(CartItemDTO::getSubtotalAmount)
+                .map(WechatCartItemDTO::getSubtotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
         Integer totalQuantity = mergedItems.stream()
-                .mapToInt(CartItemDTO::getQuantity)
+                .mapToInt(WechatCartItemDTO::getQuantity)
                 .sum();
         toCart.setTotalAmount(totalAmount);
         toCart.setTotalQuantity(totalQuantity);
@@ -593,13 +593,13 @@ public class CartServiceImpl implements CartService {
         }
 
         // 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);
+        LambdaQueryWrapper<WechatStoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCouponUsage::getTableId, tableId);
+        wrapper.eq(WechatStoreCouponUsage::getDeleteFlag, 0);
+        wrapper.in(WechatStoreCouponUsage::getUsageStatus, 0, 1, 2); // 已标记使用、已下单、已支付
+        wrapper.orderByDesc(WechatStoreCouponUsage::getCreatedTime);
         wrapper.last("LIMIT 1");
-        StoreCouponUsage usage = storeCouponUsageMapper.selectOne(wrapper);
+        WechatStoreCouponUsage usage = storeCouponUsageMapper.selectOne(wrapper);
         return usage != null;
     }
 
@@ -610,22 +610,22 @@ public class CartServiceImpl implements CartService {
         baseRedisService.setString(couponUsedKey, String.valueOf(couponId), (long) CART_EXPIRE_SECONDS);
 
         // 保存到数据库
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable 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);
+        LambdaQueryWrapper<WechatStoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCouponUsage::getTableId, tableId);
+        wrapper.eq(WechatStoreCouponUsage::getCouponId, couponId);
+        wrapper.eq(WechatStoreCouponUsage::getDeleteFlag, 0);
+        WechatStoreCouponUsage existing = storeCouponUsageMapper.selectOne(wrapper);
 
         if (existing == null) {
             Date now = new Date();
-            StoreCouponUsage usage = new StoreCouponUsage();
+            WechatStoreCouponUsage usage = new WechatStoreCouponUsage();
             usage.setTableId(tableId);
             usage.setStoreId(table.getStoreId());
             usage.setCouponId(couponId);
@@ -651,21 +651,21 @@ public class CartServiceImpl implements CartService {
         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);
+        LambdaQueryWrapper<WechatStoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCouponUsage::getTableId, tableId);
+        wrapper.eq(WechatStoreCouponUsage::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCouponUsage::getUsageStatus, 0); // 只删除已标记使用但未下单的
+        List<WechatStoreCouponUsage> usageList = storeCouponUsageMapper.selectList(wrapper);
         if (usageList != null && !usageList.isEmpty()) {
             List<Integer> usageIds = usageList.stream()
-                    .map(StoreCouponUsage::getId)
+                    .map(WechatStoreCouponUsage::getId)
                     .collect(Collectors.toList());
             // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
             storeCouponUsageMapper.deleteBatchIds(usageIds);
         }
 
         // 更新桌号表的优惠券ID
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable table = storeTableMapper.selectById(tableId);
         if (table != null) {
             table.setCurrentCouponId(null);
             storeTableMapper.updateById(table);
@@ -703,7 +703,7 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO setDinerCount(Integer tableId, Integer dinerCount) {
+    public WechatCartDTO setDinerCount(Integer tableId, Integer dinerCount) {
         log.info("设置用餐人数, tableId={}, dinerCount={}", tableId, dinerCount);
         
         if (dinerCount == null || dinerCount <= 0) {
@@ -711,14 +711,14 @@ public class CartServiceImpl implements CartService {
         }
 
         // 获取购物车
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
 
         // 获取门店ID和餐具单价
         Integer storeId = cart.getStoreId();
         if (storeId == null) {
             // 如果购物车中没有门店ID,从桌号获取
-            StoreTable table = storeTableMapper.selectById(tableId);
+            WechatStoreTable table = storeTableMapper.selectById(tableId);
             if (table != null) {
                 storeId = table.getStoreId();
             }
@@ -728,7 +728,7 @@ public class CartServiceImpl implements CartService {
         // 商铺未设置餐具费时,不往购物车加餐具;若已有餐具项则移除
         if (tablewareUnitPrice == null || tablewareUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
             log.info("门店未设置餐具费, storeId={},设置就餐人数时不添加餐具", storeId);
-            CartItemDTO existing = items.stream()
+            WechatCartItemDTO existing = items.stream()
                     .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
                     .findFirst()
                     .orElse(null);
@@ -737,7 +737,7 @@ public class CartServiceImpl implements CartService {
             }
         } else {
             // 查找是否已存在餐具项
-            CartItemDTO tablewareItem = items.stream()
+            WechatCartItemDTO tablewareItem = items.stream()
                     .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
                     .findFirst()
                     .orElse(null);
@@ -754,7 +754,7 @@ public class CartServiceImpl implements CartService {
                 tablewareItem.setUnitPrice(tablewareUnitPrice);
                 tablewareItem.setSubtotalAmount(tablewareUnitPrice.multiply(BigDecimal.valueOf(dinerCount)));
             } else {
-                CartItemDTO newTablewareItem = new CartItemDTO();
+                WechatCartItemDTO newTablewareItem = new WechatCartItemDTO();
                 newTablewareItem.setCuisineId(TABLEWARE_CUISINE_ID);
                 newTablewareItem.setCuisineName(TABLEWARE_NAME);
                 newTablewareItem.setCuisineType(0);
@@ -770,10 +770,10 @@ public class CartServiceImpl implements CartService {
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
-                .map(CartItemDTO::getSubtotalAmount)
+                .map(WechatCartItemDTO::getSubtotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
         Integer totalQuantity = items.stream()
-                .mapToInt(CartItemDTO::getQuantity)
+                .mapToInt(WechatCartItemDTO::getQuantity)
                 .sum();
         cart.setTotalAmount(totalAmount);
         cart.setTotalQuantity(totalQuantity);
@@ -785,7 +785,7 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO updateTablewareQuantity(Integer tableId, Integer quantity) {
+    public WechatCartDTO updateTablewareQuantity(Integer tableId, Integer quantity) {
         log.info("更新餐具数量, tableId={}, quantity={}", tableId, quantity);
         
         if (quantity == null || quantity < 0) {
@@ -798,13 +798,13 @@ public class CartServiceImpl implements CartService {
         }
 
         // 获取购物车
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
 
         // 获取门店ID和餐具单价
         Integer storeId = cart.getStoreId();
         if (storeId == null) {
-            StoreTable table = storeTableMapper.selectById(tableId);
+            WechatStoreTable table = storeTableMapper.selectById(tableId);
             if (table != null) {
                 storeId = table.getStoreId();
             }
@@ -814,17 +814,17 @@ public class CartServiceImpl implements CartService {
         // 商铺未设置餐具费(单价为0或未配置)时,不往购物车加餐具;若已有餐具项则移除
         if (tablewareUnitPrice == null || tablewareUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
             log.info("门店未设置餐具费, storeId={},不添加餐具到购物车", storeId);
-            CartItemDTO existing = items.stream()
+            WechatCartItemDTO existing = items.stream()
                     .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
                     .findFirst()
                     .orElse(null);
             if (existing != null) {
                 items.remove(existing);
                 BigDecimal totalAmount = items.stream()
-                        .map(CartItemDTO::getSubtotalAmount)
+                        .map(WechatCartItemDTO::getSubtotalAmount)
                         .reduce(BigDecimal.ZERO, BigDecimal::add);
                 Integer totalQuantity = items.stream()
-                        .mapToInt(CartItemDTO::getQuantity)
+                        .mapToInt(WechatCartItemDTO::getQuantity)
                         .sum();
                 cart.setTotalAmount(totalAmount);
                 cart.setTotalQuantity(totalQuantity);
@@ -834,7 +834,7 @@ public class CartServiceImpl implements CartService {
         }
 
         // 查找餐具项
-        CartItemDTO tablewareItem = items.stream()
+        WechatCartItemDTO tablewareItem = items.stream()
                 .filter(item -> TABLEWARE_CUISINE_ID.equals(item.getCuisineId()))
                 .findFirst()
                 .orElse(null);
@@ -844,7 +844,7 @@ public class CartServiceImpl implements CartService {
             Integer userId = TokenUtil.getCurrentUserId();
             String userPhone = TokenUtil.getCurrentUserPhone();
             
-            tablewareItem = new CartItemDTO();
+            tablewareItem = new WechatCartItemDTO();
             tablewareItem.setCuisineId(TABLEWARE_CUISINE_ID);
             tablewareItem.setCuisineName(TABLEWARE_NAME);
             tablewareItem.setCuisineType(0); // 0表示餐具
@@ -869,10 +869,10 @@ public class CartServiceImpl implements CartService {
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
-                .map(CartItemDTO::getSubtotalAmount)
+                .map(WechatCartItemDTO::getSubtotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
         Integer totalQuantity = items.stream()
-                .mapToInt(CartItemDTO::getQuantity)
+                .mapToInt(WechatCartItemDTO::getQuantity)
                 .sum();
         cart.setTotalAmount(totalAmount);
         cart.setTotalQuantity(totalQuantity);
@@ -884,12 +884,12 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO lockCartItems(Integer tableId) {
+    public WechatCartDTO lockCartItems(Integer tableId) {
         log.info("锁定购物车商品数量(设置已下单数量), tableId={}", tableId);
         
         // 获取购物车
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
         
         if (items == null || items.isEmpty()) {
             log.warn("购物车为空,无需锁定, tableId={}", tableId);
@@ -898,7 +898,7 @@ public class CartServiceImpl implements CartService {
         
         // 遍历所有商品,将当前数量设置为已下单数量
         boolean hasChanges = false;
-        for (CartItemDTO item : items) {
+        for (WechatCartItemDTO item : items) {
             Integer currentQuantity = item.getQuantity();
             Integer lockedQuantity = item.getLockedQuantity();
             
@@ -928,12 +928,12 @@ public class CartServiceImpl implements CartService {
     }
 
     @Override
-    public CartDTO unlockCartItems(Integer tableId) {
+    public WechatCartDTO unlockCartItems(Integer tableId) {
         log.info("解锁购物车商品数量(清除已下单数量), tableId={}", tableId);
         
         // 获取购物车
-        CartDTO cart = getCart(tableId);
-        List<CartItemDTO> items = cart.getItems();
+        WechatCartDTO cart = getCart(tableId);
+        List<WechatCartItemDTO> items = cart.getItems();
         
         if (items == null || items.isEmpty()) {
             log.info("购物车为空,无需解锁, tableId={}", tableId);
@@ -942,7 +942,7 @@ public class CartServiceImpl implements CartService {
         
         // 遍历所有商品,清除已下单数量(lockedQuantity)
         boolean hasChanges = false;
-        for (CartItemDTO item : items) {
+        for (WechatCartItemDTO item : items) {
             if (item.getLockedQuantity() != null && item.getLockedQuantity() > 0) {
                 // 清除已下单数量,允许重新下单
                 item.setLockedQuantity(null);
@@ -966,7 +966,7 @@ public class CartServiceImpl implements CartService {
      * 保存购物车到Redis和数据库(优化后的双写策略)
      * Redis同步写入(保证实时性),数据库异步批量写入(提高性能)
      */
-    private void saveCart(CartDTO cart) {
+    private void saveCart(WechatCartDTO cart) {
         // 1. 同步保存到Redis(保证实时性)
         saveCartToRedis(cart);
 
@@ -983,7 +983,7 @@ public class CartServiceImpl implements CartService {
     /**
      * 保存购物车到Redis
      */
-    private void saveCartToRedis(CartDTO cart) {
+    private void saveCartToRedis(WechatCartDTO cart) {
         String cartKey = CART_KEY_PREFIX + cart.getTableId();
         String cartJson = JSON.toJSONString(cart);
         baseRedisService.setString(cartKey, cartJson, (long) CART_EXPIRE_SECONDS);
@@ -993,19 +993,19 @@ public class CartServiceImpl implements CartService {
      * 保存购物车到数据库(优化后的批量操作版本)
      * 使用批量逻辑删除和批量插入,提高性能
      */
-    private void saveCartToDatabase(CartDTO cart) {
+    private void saveCartToDatabase(WechatCartDTO 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);
-            List<StoreCart> existingCartList = storeCartMapper.selectList(queryWrapper);
+            LambdaQueryWrapper<WechatStoreCart> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(WechatStoreCart::getTableId, cart.getTableId())
+                    .eq(WechatStoreCart::getDeleteFlag, 0);
+            List<WechatStoreCart> existingCartList = storeCartMapper.selectList(queryWrapper);
             if (existingCartList != null && !existingCartList.isEmpty()) {
                 List<Integer> cartIds = existingCartList.stream()
-                        .map(StoreCart::getId)
+                        .map(WechatStoreCart::getId)
                         .collect(Collectors.toList());
                 // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
                 storeCartMapper.deleteBatchIds(cartIds);
@@ -1013,9 +1013,9 @@ public class CartServiceImpl implements CartService {
 
             // 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();
+                List<WechatStoreCart> cartList = new ArrayList<>(cart.getItems().size());
+                for (WechatCartItemDTO item : cart.getItems()) {
+                    WechatStoreCart storeCart = new WechatStoreCart();
                     storeCart.setTableId(cart.getTableId());
                     storeCart.setStoreId(cart.getStoreId());
                     storeCart.setCuisineId(item.getCuisineId());
@@ -1038,7 +1038,7 @@ public class CartServiceImpl implements CartService {
                 // 批量插入(如果数量较少,直接循环插入;如果数量较多,可以考虑分批插入)
                 if (cartList.size() <= 50) {
                     // 小批量直接插入
-                    for (StoreCart storeCart : cartList) {
+                    for (WechatStoreCart storeCart : cartList) {
                         storeCartMapper.insert(storeCart);
                     }
                 } else {
@@ -1046,8 +1046,8 @@ public class CartServiceImpl implements CartService {
                     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) {
+                        List<WechatStoreCart> batch = cartList.subList(i, end);
+                        for (WechatStoreCart storeCart : batch) {
                             storeCartMapper.insert(storeCart);
                         }
                     }
@@ -1055,7 +1055,7 @@ public class CartServiceImpl implements CartService {
             }
 
             // 3. 更新桌号表的购物车统计
-            StoreTable table = storeTableMapper.selectById(cart.getTableId());
+            WechatStoreTable table = storeTableMapper.selectById(cart.getTableId());
             if (table != null) {
                 table.setCartItemCount(cart.getTotalQuantity());
                 table.setCartTotalAmount(cart.getTotalAmount());

+ 71 - 67
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java

@@ -7,10 +7,14 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.store.*;
-import shop.alien.entity.store.dto.CartDTO;
-import shop.alien.entity.store.dto.CartItemDTO;
+import shop.alien.entity.store.wechat.*;
+import shop.alien.entity.store.dto.wechat.WechatCartDTO;
+import shop.alien.entity.store.dto.wechat.WechatCartItemDTO;
 import shop.alien.entity.store.vo.*;
+import shop.alien.entity.store.vo.wechat.WechatOrderConfirmVO;
+import shop.alien.entity.store.vo.wechat.WechatOrderSettlementVO;
 import shop.alien.mapper.*;
+import shop.alien.mapper.wechat.*;
 import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.DiningService;
@@ -31,18 +35,18 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class DiningServiceImpl implements DiningService {
 
-    private final StoreTableMapper storeTableMapper;
+    private final WechatStoreTableMapper storeTableMapper;
     private final StoreInfoMapper storeInfoMapper;
-    private final StoreCuisineMapper storeCuisineMapper;
-    private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
-    private final StoreCuisineComboMapper storeCuisineComboMapper;
-    private final StoreOrderDetailMapper storeOrderDetailMapper;
+    private final WechatStoreCuisineMapper storeCuisineMapper;
+    private final WechatStoreCuisineCategoryMapper storeCuisineCategoryMapper;
+    private final WechatStoreCuisineComboMapper storeCuisineComboMapper;
+    private final WechatStoreOrderDetailMapper storeOrderDetailMapper;
     private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
     private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
     private final CartService cartService;
     private final BaseRedisService baseRedisService;
     private final shop.alien.dining.service.StoreOrderService storeOrderService;
-    private final shop.alien.mapper.StoreOrderMapper storeOrderMapper;
+    private final WechatStoreOrderMapper storeOrderMapper;
     private final shop.alien.dining.service.OrderLockService orderLockService;
 
     @Override
@@ -50,7 +54,7 @@ public class DiningServiceImpl implements DiningService {
         if (tableId == null) {
             return new TableDiningStatusVO(null, false, null);
         }
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable table = storeTableMapper.selectById(tableId);
         if (table == null) {
             return new TableDiningStatusVO(tableId, false, null);
         }
@@ -65,7 +69,7 @@ public class DiningServiceImpl implements DiningService {
     @Override
     public DiningPageInfoVO getDiningPageInfo(Integer tableId, Integer dinerCount) {
         log.info("获取点餐页面信息, tableId={}, dinerCount={}", tableId, dinerCount);
-        StoreTable table = storeTableMapper.selectById(tableId);
+        WechatStoreTable table = storeTableMapper.selectById(tableId);
         if (table == null) {
             throw new RuntimeException("桌号不存在");
         }
@@ -111,16 +115,16 @@ public class DiningServiceImpl implements DiningService {
             keyword = keyword.substring(0, 10);
         }
 
-        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisine::getStoreId, storeId);
-        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        LambdaQueryWrapper<WechatStoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisine::getStoreId, storeId);
+        wrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCuisine::getShelfStatus, 1); // 只查询上架的
         if (StringUtils.hasText(keyword)) {
-            wrapper.like(StoreCuisine::getName, keyword);
+            wrapper.like(WechatStoreCuisine::getName, keyword);
         }
-        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+        wrapper.orderByDesc(WechatStoreCuisine::getCreatedTime);
 
-        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        List<WechatStoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
         return convertToCuisineListVO(cuisines, tableId);
     }
 
@@ -135,21 +139,21 @@ public class DiningServiceImpl implements DiningService {
             size = 12;
         }
 
-        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisine::getStoreId, storeId);
-        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        LambdaQueryWrapper<WechatStoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisine::getStoreId, storeId);
+        wrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCuisine::getShelfStatus, 1); // 只查询上架的
         if (categoryId != null) {
             // 这里假设菜品表有category_id字段,如果没有需要关联查询
-            // wrapper.eq(StoreCuisine::getCategoryId, categoryId);
+            // wrapper.eq(WechatStoreCuisine::getCategoryId, categoryId);
         }
-        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+        wrapper.orderByDesc(WechatStoreCuisine::getCreatedTime);
 
         // 分页查询
         int offset = (page - 1) * size;
         wrapper.last("LIMIT " + offset + ", " + size);
 
-        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        List<WechatStoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
         return convertToCuisineListVO(cuisines, tableId);
     }
 
@@ -157,7 +161,7 @@ public class DiningServiceImpl implements DiningService {
     public CuisineDetailVO getCuisineDetail(Integer cuisineId, Integer tableId) {
         log.info("获取菜品详情, cuisineId={}, tableId={}", cuisineId, tableId);
 
-        StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+        WechatStoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
         if (cuisine == null) {
             throw new RuntimeException("菜品不存在");
         }
@@ -193,22 +197,22 @@ public class DiningServiceImpl implements DiningService {
         vo.setMonthlySales(getMonthlySales(cuisineId));
 
         // 获取购物车数量
-        CartDTO cart = cartService.getCart(tableId);
+        WechatCartDTO cart = cartService.getCart(tableId);
         if (cart.getItems() != null) {
-            Optional<CartItemDTO> cartItem = cart.getItems().stream()
+            Optional<WechatCartItemDTO> cartItem = cart.getItems().stream()
                     .filter(item -> item.getCuisineId().equals(cuisineId))
                     .findFirst();
-            vo.setCartQuantity(cartItem.map(CartItemDTO::getQuantity).orElse(0));
+            vo.setCartQuantity(cartItem.map(WechatCartItemDTO::getQuantity).orElse(0));
         } else {
             vo.setCartQuantity(0);
         }
 
         // 如果是套餐,获取套餐包含的菜品
         if (cuisine.getCuisineType() != null && cuisine.getCuisineType() == 2) {
-            LambdaQueryWrapper<StoreCuisineCombo> comboWrapper = new LambdaQueryWrapper<>();
-            comboWrapper.eq(StoreCuisineCombo::getCid, cuisineId);
-            comboWrapper.eq(StoreCuisineCombo::getDeleteFlag, 0);
-            List<StoreCuisineCombo> combos = storeCuisineComboMapper.selectList(comboWrapper);
+            LambdaQueryWrapper<WechatStoreCuisineCombo> comboWrapper = new LambdaQueryWrapper<>();
+            comboWrapper.eq(WechatStoreCuisineCombo::getCid, cuisineId);
+            comboWrapper.eq(WechatStoreCuisineCombo::getDeleteFlag, 0);
+            List<WechatStoreCuisineCombo> combos = storeCuisineComboMapper.selectList(comboWrapper);
 
             List<CuisineComboItemVO> comboItems = combos.stream().map(combo -> {
                 CuisineComboItemVO item = new CuisineComboItemVO();
@@ -216,7 +220,7 @@ public class DiningServiceImpl implements DiningService {
                 item.setQuantity(combo.getSnum());
                 item.setCategory(combo.getCategory());
                 // 查询菜品名称
-                StoreCuisine comboCuisine = storeCuisineMapper.selectById(combo.getSid());
+                WechatStoreCuisine comboCuisine = storeCuisineMapper.selectById(combo.getSid());
                 if (comboCuisine != null) {
                     item.setCuisineName(comboCuisine.getName());
                 }
@@ -322,7 +326,7 @@ public class DiningServiceImpl implements DiningService {
     }
 
     @Override
-    public OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId) {
+    public WechatOrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId) {
         log.info("获取订单确认页面信息, tableId={}, dinerCount={}, userId={}", tableId, dinerCount, userId);
 
         // 获取点餐页面信息(就餐中且未传人数时,内部会使用桌台已保存的就餐人数)
@@ -331,26 +335,26 @@ public class DiningServiceImpl implements DiningService {
                 ? pageInfo.getDinerCount() : (dinerCount != null ? dinerCount : 0);
 
         // 获取购物车
-        CartDTO cart = cartService.getCart(tableId);
+        WechatCartDTO cart = cartService.getCart(tableId);
 
         // 为购物车项补全菜品标签(购物车从 DB 加载时可能无 tags)
-        List<CartItemDTO> items = cart.getItems();
+        List<WechatCartItemDTO> items = cart.getItems();
         if (items != null && !items.isEmpty()) {
             Set<Integer> cuisineIds = items.stream()
-                    .map(CartItemDTO::getCuisineId)
+                    .map(WechatCartItemDTO::getCuisineId)
                     .filter(Objects::nonNull)
                     .collect(Collectors.toSet());
             if (!cuisineIds.isEmpty()) {
-                List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+                List<WechatStoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
                 Map<Integer, String> tagsMap = new HashMap<>();
                 if (cuisines != null) {
-                    for (StoreCuisine c : cuisines) {
+                    for (WechatStoreCuisine c : cuisines) {
                         if (c.getTags() != null) {
                             tagsMap.put(c.getId(), c.getTags());
                         }
                     }
                 }
-                for (CartItemDTO item : items) {
+                for (WechatCartItemDTO item : items) {
                     if (item.getCuisineId() != null && item.getTags() == null) {
                         item.setTags(tagsMap.get(item.getCuisineId()));
                     }
@@ -362,7 +366,7 @@ public class DiningServiceImpl implements DiningService {
         Integer lockUserId = orderLockService.checkOrderLock(tableId);
         boolean isLocked = lockUserId != null && !lockUserId.equals(userId);
 
-        OrderConfirmVO vo = new OrderConfirmVO();
+        WechatOrderConfirmVO vo = new WechatOrderConfirmVO();
         vo.setStoreName(pageInfo.getStoreName());
         vo.setTableNumber(pageInfo.getTableNumber());
         vo.setDinerCount(pageInfo.getDinerCount() != null ? pageInfo.getDinerCount() : dinerCount);
@@ -454,18 +458,18 @@ public class DiningServiceImpl implements DiningService {
     /**
      * 转换为菜品列表VO
      */
-    private List<CuisineListVO> convertToCuisineListVO(List<StoreCuisine> cuisines, Integer tableId) {
+    private List<CuisineListVO> convertToCuisineListVO(List<WechatStoreCuisine> cuisines, Integer tableId) {
         // 获取购物车
-        CartDTO cart = cartService.getCart(tableId);
+        WechatCartDTO cart = cartService.getCart(tableId);
         Map<Integer, Integer> cartQuantityMap = new HashMap<>();
         if (cart.getItems() != null) {
             cartQuantityMap = cart.getItems().stream()
-                    .collect(Collectors.toMap(CartItemDTO::getCuisineId, CartItemDTO::getQuantity));
+                    .collect(Collectors.toMap(WechatCartItemDTO::getCuisineId, WechatCartItemDTO::getQuantity));
         }
 
         // 批量查询月售数量
         Map<Integer, Integer> monthlySalesMap = new HashMap<>();
-        for (StoreCuisine cuisine : cuisines) {
+        for (WechatStoreCuisine cuisine : cuisines) {
             monthlySalesMap.put(cuisine.getId(), getMonthlySales(cuisine.getId()));
         }
 
@@ -508,16 +512,16 @@ public class DiningServiceImpl implements DiningService {
             Date monthStart = calendar.getTime();
 
             // 查询订单明细中该菜品的销售数量
-            LambdaQueryWrapper<StoreOrderDetail> wrapper = new LambdaQueryWrapper<>();
-            wrapper.eq(StoreOrderDetail::getCuisineId, cuisineId);
-            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
-            wrapper.ge(StoreOrderDetail::getCreatedTime, monthStart);
+            LambdaQueryWrapper<WechatStoreOrderDetail> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(WechatStoreOrderDetail::getCuisineId, cuisineId);
+            wrapper.eq(WechatStoreOrderDetail::getDeleteFlag, 0);
+            wrapper.ge(WechatStoreOrderDetail::getCreatedTime, monthStart);
             // 只统计已支付的订单
-            wrapper.inSql(StoreOrderDetail::getOrderId,
-                    "SELECT id FROM store_order WHERE pay_status = 1 AND delete_flag = 0");
+            wrapper.inSql(WechatStoreOrderDetail::getOrderId,
+                    "SELECT id FROM wechat_store_order WHERE pay_status = 1 AND delete_flag = 0");
 
-            List<StoreOrderDetail> details = storeOrderDetailMapper.selectList(wrapper);
-            return details.stream().mapToInt(StoreOrderDetail::getQuantity).sum();
+            List<WechatStoreOrderDetail> details = storeOrderDetailMapper.selectList(wrapper);
+            return details.stream().mapToInt(WechatStoreOrderDetail::getQuantity).sum();
         } catch (Exception e) {
             log.error("获取月售数量失败, cuisineId={}", cuisineId, e);
             return 0;
@@ -525,32 +529,32 @@ public class DiningServiceImpl implements DiningService {
     }
 
     @Override
-    public shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId) {
+    public WechatOrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId) {
         log.info("获取订单结算确认页面信息, orderId={}, userId={}", orderId, userId);
 
         // 查询订单
-        shop.alien.entity.store.StoreOrder order = storeOrderService.getOrderById(orderId);
+        shop.alien.entity.store.wechat.WechatStoreOrder order = storeOrderService.getOrderById(orderId);
         if (order == null) {
             throw new RuntimeException("订单不存在");
         }
 
         // 查询订单明细
-        LambdaQueryWrapper<shop.alien.entity.store.StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
-        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getOrderId, orderId);
-        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getDeleteFlag, 0);
-        detailWrapper.orderByDesc(shop.alien.entity.store.StoreOrderDetail::getCreatedTime);
-        List<shop.alien.entity.store.StoreOrderDetail> details = storeOrderDetailMapper.selectList(detailWrapper);
+        LambdaQueryWrapper<shop.alien.entity.store.wechat.WechatStoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(shop.alien.entity.store.wechat.WechatStoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(shop.alien.entity.store.wechat.WechatStoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByDesc(shop.alien.entity.store.wechat.WechatStoreOrderDetail::getCreatedTime);
+        List<shop.alien.entity.store.wechat.WechatStoreOrderDetail> details = storeOrderDetailMapper.selectList(detailWrapper);
 
         // 批量查询菜品标签
         Set<Integer> cuisineIds = details.stream()
-                .map(shop.alien.entity.store.StoreOrderDetail::getCuisineId)
+                .map(shop.alien.entity.store.wechat.WechatStoreOrderDetail::getCuisineId)
                 .filter(Objects::nonNull)
                 .collect(Collectors.toSet());
         Map<Integer, String> cuisineIdToTags = new HashMap<>();
         if (!cuisineIds.isEmpty()) {
-            List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+            List<WechatStoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
             if (cuisines != null) {
-                for (StoreCuisine c : cuisines) {
+                for (WechatStoreCuisine c : cuisines) {
                     if (c.getTags() != null) {
                         cuisineIdToTags.put(c.getId(), c.getTags());
                     }
@@ -558,10 +562,10 @@ public class DiningServiceImpl implements DiningService {
             }
         }
 
-        // 转换为CartItemDTO(含菜品标签)
+        // 转换为 WechatCartItemDTO(含菜品标签)
         Map<Integer, String> finalTagsMap = cuisineIdToTags;
-        List<shop.alien.entity.store.dto.CartItemDTO> items = details.stream().map(detail -> {
-            shop.alien.entity.store.dto.CartItemDTO item = new shop.alien.entity.store.dto.CartItemDTO();
+        List<WechatCartItemDTO> items = details.stream().map(detail -> {
+            WechatCartItemDTO item = new WechatCartItemDTO();
             item.setCuisineId(detail.getCuisineId());
             item.setCuisineName(detail.getCuisineName());
             item.setCuisineType(detail.getCuisineType());
@@ -583,7 +587,7 @@ public class DiningServiceImpl implements DiningService {
         Integer lockUserId = orderLockService.checkSettlementLock(orderId);
         boolean isLocked = lockUserId != null && !lockUserId.equals(userId);
 
-        shop.alien.entity.store.vo.OrderSettlementVO vo = new shop.alien.entity.store.vo.OrderSettlementVO();
+        WechatOrderSettlementVO vo = new WechatOrderSettlementVO();
         vo.setOrderId(order.getId());
         vo.setOrderNo(order.getOrderNo());
         vo.setStoreName(storeInfo != null ? storeInfo.getStoreName() : null);

+ 2 - 2
alien-dining/src/main/java/shop/alien/dining/service/impl/OrderLockServiceImpl.java

@@ -18,8 +18,8 @@ import shop.alien.dining.service.OrderLockService;
 @RequiredArgsConstructor
 public class OrderLockServiceImpl implements OrderLockService {
 
-    private static final String ORDER_LOCK_KEY_PREFIX = "order:lock:table:";
-    private static final String SETTLEMENT_LOCK_KEY_PREFIX = "settlement:lock:order:";
+    private static final String ORDER_LOCK_KEY_PREFIX = "wechat:order:lock:table:";
+    private static final String SETTLEMENT_LOCK_KEY_PREFIX = "wechat:settlement:lock:order:";
     private static final int ORDER_LOCK_EXPIRE_SECONDS = 300; // 5分钟过期
 
     private final BaseRedisService baseRedisService;

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

@@ -5,16 +5,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
-import shop.alien.entity.store.StoreCuisine;
-import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCategory;
 import shop.alien.entity.store.StoreInfo;
-import shop.alien.entity.store.StoreTable;
-import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
-import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
-import shop.alien.mapper.StoreCuisineCategoryMapper;
-import shop.alien.mapper.StoreCuisineMapper;
+import shop.alien.entity.store.wechat.WechatStoreTable;
+import shop.alien.entity.store.dto.wechat.WechatStoreInfoWithHomepageCuisinesDTO;
+import shop.alien.entity.store.vo.wechat.WechatCategoryWithCuisinesVO;
+import shop.alien.mapper.wechat.WechatStoreCuisineCategoryMapper;
+import shop.alien.mapper.wechat.WechatStoreCuisineMapper;
 import shop.alien.mapper.StoreInfoMapper;
-import shop.alien.mapper.StoreTableMapper;
+import shop.alien.mapper.wechat.WechatStoreTableMapper;
 import shop.alien.dining.service.StoreInfoService;
 import org.apache.commons.lang3.StringUtils;
 
@@ -33,55 +33,55 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class StoreInfoServiceImpl implements StoreInfoService {
 
-    private final StoreTableMapper storeTableMapper;
-    private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
-    private final StoreCuisineMapper storeCuisineMapper;
+    private final WechatStoreTableMapper storeTableMapper;
+    private final WechatStoreCuisineCategoryMapper storeCuisineCategoryMapper;
+    private final WechatStoreCuisineMapper storeCuisineMapper;
     private final StoreInfoMapper storeInfoMapper;
 
     @Override
-    public List<StoreTable> getTablesByStoreId(Integer storeId) {
+    public List<WechatStoreTable> getTablesByStoreId(Integer storeId) {
         log.info("根据门店ID查询桌号列表, storeId={}", storeId);
         
-        LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreTable::getStoreId, storeId);
-        wrapper.eq(StoreTable::getDeleteFlag, 0);
-        wrapper.orderByAsc(StoreTable::getTableNumber);
+        LambdaQueryWrapper<WechatStoreTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreTable::getStoreId, storeId);
+        wrapper.eq(WechatStoreTable::getDeleteFlag, 0);
+        wrapper.orderByAsc(WechatStoreTable::getTableNumber);
         
         return storeTableMapper.selectList(wrapper);
     }
 
     @Override
-    public List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId) {
+    public List<WechatStoreCuisineCategory> getCategoriesByStoreId(Integer storeId) {
         log.info("根据门店ID查询菜品种类列表, storeId={}", storeId);
         
-        LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisineCategory::getStoreId, storeId);
-        wrapper.eq(StoreCuisineCategory::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisineCategory::getStatus, 1); // 只查询启用的分类
-        wrapper.orderByAsc(StoreCuisineCategory::getSort); // 按排序字段排序
+        LambdaQueryWrapper<WechatStoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisineCategory::getStoreId, storeId);
+        wrapper.eq(WechatStoreCuisineCategory::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCuisineCategory::getStatus, 1); // 只查询启用的分类
+        wrapper.orderByAsc(WechatStoreCuisineCategory::getSort); // 按排序字段排序
         
         return storeCuisineCategoryMapper.selectList(wrapper);
     }
 
     @Override
-    public List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId) {
+    public List<WechatStoreCuisine> getCuisinesByCategoryId(Integer categoryId) {
         log.info("根据菜品种类ID查询菜品信息列表, categoryId={}", categoryId);
         
         // 先查询分类信息,获取 storeId
-        StoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
+        WechatStoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
         if (category == null) {
             log.warn("菜品种类不存在, categoryId={}", categoryId);
             return new java.util.ArrayList<>();
         }
         
         // 查询该门店下所有上架的菜品(与 getCategoriesWithCuisinesByStoreId 中 cuisines 查询条件、排序一致)
-        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisine::getStoreId, category.getStoreId());
-        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的菜品
-        wrapper.orderByAsc(StoreCuisine::getId); // 与 categories-with-cuisines 中 cuisines 顺序一致
+        LambdaQueryWrapper<WechatStoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisine::getStoreId, category.getStoreId());
+        wrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCuisine::getShelfStatus, 1); // 只查询上架的菜品
+        wrapper.orderByAsc(WechatStoreCuisine::getId); // 与 categories-with-cuisines 中 cuisines 顺序一致
         
-        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
+        List<WechatStoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
         
         // 过滤出包含该分类ID的菜品
         // categoryIds 是 JSON 数组格式,如:[1,2,3]
@@ -106,37 +106,37 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     }
 
     @Override
-    public List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword) {
+    public List<WechatCategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword) {
         log.info("根据门店ID查询菜品种类及下属菜品, storeId={}, keyword={}", storeId, keyword);
-        List<StoreCuisineCategory> categories = getCategoriesByStoreId(storeId);
+        List<WechatStoreCuisineCategory> categories = getCategoriesByStoreId(storeId);
         if (categories == null || categories.isEmpty()) {
             return new ArrayList<>();
         }
         // 与 getCuisinesByCategoryId 相同的查询条件与排序,保证 cuisines 字段与 /store/info/cuisines 一致
-        LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
-        cuisineWrapper.eq(StoreCuisine::getStoreId, storeId);
-        cuisineWrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        cuisineWrapper.eq(StoreCuisine::getShelfStatus, 1);
-        cuisineWrapper.orderByAsc(StoreCuisine::getId);
-        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(cuisineWrapper);
+        LambdaQueryWrapper<WechatStoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
+        cuisineWrapper.eq(WechatStoreCuisine::getStoreId, storeId);
+        cuisineWrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        cuisineWrapper.eq(WechatStoreCuisine::getShelfStatus, 1);
+        cuisineWrapper.orderByAsc(WechatStoreCuisine::getId);
+        List<WechatStoreCuisine> allCuisines = storeCuisineMapper.selectList(cuisineWrapper);
         if (allCuisines == null) {
             allCuisines = new ArrayList<>();
         }
         boolean filterByName = StringUtils.isNotBlank(keyword);
         String keywordLower = filterByName ? keyword.trim().toLowerCase() : null;
-        List<CategoryWithCuisinesVO> result = new ArrayList<>();
-        for (StoreCuisineCategory category : categories) {
+        List<WechatCategoryWithCuisinesVO> result = new ArrayList<>();
+        for (WechatStoreCuisineCategory category : categories) {
             Integer categoryId = category.getId();
-            List<StoreCuisine> cuisines = allCuisines.stream()
+            List<WechatStoreCuisine> cuisines = allCuisines.stream()
                     .filter(c -> belongsToCategory(c, categoryId))
                     .filter(c -> !filterByName || (c.getName() != null && c.getName().toLowerCase().contains(keywordLower)))
                     .collect(Collectors.toList());
-            result.add(new CategoryWithCuisinesVO(category, cuisines));
+            result.add(new WechatCategoryWithCuisinesVO(category, cuisines));
         }
         return result;
     }
 
-    private boolean belongsToCategory(StoreCuisine cuisine, Integer categoryId) {
+    private boolean belongsToCategory(WechatStoreCuisine cuisine, Integer categoryId) {
         String categoryIdsStr = cuisine.getCategoryIds();
         if (StringUtils.isBlank(categoryIdsStr)) {
             return false;
@@ -156,19 +156,19 @@ public class StoreInfoServiceImpl implements StoreInfoService {
             log.warn("删除菜品种类失败:分类ID为空");
             return false;
         }
-        StoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
+        WechatStoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
         if (category == null) {
             log.warn("删除菜品种类失败:分类不存在, categoryId={}", categoryId);
             return false;
         }
         Integer storeId = category.getStoreId();
         // 1. 解除绑定:将该分类ID从所有关联菜品的 category_ids 中移除,价目表菜品其它字段不变
-        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisine::getStoreId, storeId);
-        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
+        LambdaQueryWrapper<WechatStoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisine::getStoreId, storeId);
+        wrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        List<WechatStoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
         if (allCuisines != null) {
-            for (StoreCuisine cuisine : allCuisines) {
+            for (WechatStoreCuisine cuisine : allCuisines) {
                 if (!belongsToCategory(cuisine, categoryId)) {
                     continue;
                 }
@@ -205,7 +205,7 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     }
 
     @Override
-    public StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId) {
+    public WechatStoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId) {
         log.info("根据商铺ID查询店铺信息和首页展示美食价目表, storeId={}", storeId);
         
         // 1. 查询店铺信息
@@ -215,21 +215,21 @@ public class StoreInfoServiceImpl implements StoreInfoService {
         }
         
         // 2. 查询首页展示的美食价目表(is_homepage_display = 1,上架状态 = 1,审核通过 = 1)
-        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCuisine::getStoreId, storeId);
-        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
-        wrapper.eq(StoreCuisine::getIsHomepageDisplay, 1); // 首页展示
-        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 上架状态
-        wrapper.eq(StoreCuisine::getStatus, 1); // 审核通过
-        wrapper.orderByDesc(StoreCuisine::getCreatedTime); // 按创建时间倒序
+        LambdaQueryWrapper<WechatStoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStoreCuisine::getStoreId, storeId);
+        wrapper.eq(WechatStoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(WechatStoreCuisine::getIsHomepageDisplay, 1); // 首页展示
+        wrapper.eq(WechatStoreCuisine::getShelfStatus, 1); // 上架状态
+        wrapper.eq(WechatStoreCuisine::getStatus, 1); // 审核通过
+        wrapper.orderByDesc(WechatStoreCuisine::getCreatedTime); // 按创建时间倒序
         
-        List<StoreCuisine> homepageCuisines = storeCuisineMapper.selectList(wrapper);
+        List<WechatStoreCuisine> homepageCuisines = storeCuisineMapper.selectList(wrapper);
         if (homepageCuisines == null) {
             homepageCuisines = new ArrayList<>();
         }
         
         // 3. 构建返回DTO
-        StoreInfoWithHomepageCuisinesDTO dto = new StoreInfoWithHomepageCuisinesDTO();
+        WechatStoreInfoWithHomepageCuisinesDTO dto = new WechatStoreInfoWithHomepageCuisinesDTO();
         dto.setStoreInfo(storeInfo);
         dto.setHomepageCuisines(homepageCuisines);
         

Datei-Diff unterdrückt, da er zu groß ist
+ 256 - 254
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java


+ 1 - 1
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java

@@ -19,7 +19,7 @@ public interface PaymentStrategy {
      * @param subject 订单标题
      * @param payer 支付者 openid
      * @param orderNo 订单号
-     * @param storeId 店铺ID,本系统调用时必传,用于获取 StorePaymentConfig
+     * @param storeId 店铺ID,本系统调用时必传,用于获取 WechatStorePaymentConfig
      * @return 预支付订单信息
      * @throws Exception 生成异常
      */

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

@@ -24,10 +24,10 @@ import shop.alien.dining.util.WXPayUtility;
 import shop.alien.dining.util.WeChatPayUtil;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeDiscountCouponUser;
-import shop.alien.entity.store.StoreOrder;
-import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.wechat.WechatStoreOrder;
+import shop.alien.entity.store.wechat.WechatStorePaymentConfig;
 import shop.alien.mapper.LifeDiscountCouponUserMapper;
-import shop.alien.mapper.StorePaymentConfigMapper;
+import shop.alien.mapper.wechat.WechatStorePaymentConfigMapper;
 import shop.alien.util.common.constant.DiscountCouponEnum;
 import shop.alien.util.common.constant.PaymentEnum;
 
@@ -85,7 +85,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     private String searchOrderByOutTradeNoPath;
 
     private final StoreOrderService storeOrderService;
-    private final StorePaymentConfigMapper storePaymentConfigMapper;
+    private final WechatStorePaymentConfigMapper storePaymentConfigMapper;
     private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
     private final ObjectMapper objectMapper;
 
@@ -95,25 +95,25 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     /**
      * 根据店铺ID从 MySQL 获取支付配置(本系统内部调用时使用)
      */
-    private StorePaymentConfig getConfigByStoreId(Integer storeId) {
+    private WechatStorePaymentConfig getConfigByStoreId(Integer storeId) {
         if (storeId == null) {
             return null;
         }
-        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StorePaymentConfig::getStoreId, storeId);
+        LambdaQueryWrapper<WechatStorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStorePaymentConfig::getStoreId, storeId);
         return storePaymentConfigMapper.selectOne(wrapper);
     }
 
     /**
      * 根据微信支付公钥序列号获取支付配置(外部回调如支付通知时使用,通过请求头 Wechatpay-Serial 判断)
      */
-    private StorePaymentConfig getConfigByWechatPayPublicKeyId(String wechatPayPublicKeyId) {
+    private WechatStorePaymentConfig getConfigByWechatPayPublicKeyId(String wechatPayPublicKeyId) {
         if (!StringUtils.hasText(wechatPayPublicKeyId)) {
             return null;
         }
-        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StorePaymentConfig::getWechatPayPublicKeyId, wechatPayPublicKeyId);
-        List<StorePaymentConfig> storePaymentConfigs = storePaymentConfigMapper.selectList(wrapper);
+        LambdaQueryWrapper<WechatStorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WechatStorePaymentConfig::getWechatPayPublicKeyId, wechatPayPublicKeyId);
+        List<WechatStorePaymentConfig> storePaymentConfigs = storePaymentConfigMapper.selectList(wrapper);
         if (storePaymentConfigs.isEmpty()) {
             return null;
         }
@@ -121,9 +121,9 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     /**
-     * 从 StorePaymentConfig 加载商户私钥
+     * 从 WechatStorePaymentConfig 加载商户私钥
      */
-    private PrivateKey loadPrivateKeyFromConfig(StorePaymentConfig config) {
+    private PrivateKey loadPrivateKeyFromConfig(WechatStorePaymentConfig config) {
         if (config == null) {
             return null;
         }
@@ -138,9 +138,9 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     /**
-     * 从 StorePaymentConfig 加载微信支付公钥
+     * 从 WechatStorePaymentConfig 加载微信支付公钥
      */
-    private PublicKey loadPublicKeyFromConfig(StorePaymentConfig config) {
+    private PublicKey loadPublicKeyFromConfig(WechatStorePaymentConfig config) {
         if (config == null) {
             return null;
         }
@@ -161,7 +161,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
             log.warn("createPrePayOrder 缺少 storeId,无法获取支付配置");
             return R.fail("店铺ID不能为空");
         }
-        StorePaymentConfig config = getConfigByStoreId(storeId);
+        WechatStorePaymentConfig config = getConfigByStoreId(storeId);
         if (config == null) {
             log.warn("未找到店铺支付配置 storeId={}", storeId);
             return R.fail("未找到该店铺的微信支付配置");
@@ -193,7 +193,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
         request.payer.openid = payer;
 
         String wechatOutTradeNo = orderNo;
-        StoreOrder storeOrder = storeOrderService.getOrderByOrderNo(orderNo);
+        WechatStoreOrder storeOrder = storeOrderService.getOrderByOrderNo(orderNo);
         log.info("createPrePayOrder, orderNo: {}, storeOrder: {}", orderNo, storeOrder);
         if (storeOrder != null ) {
             log.info("createPrePayOrder, orderNo: {}, storeOrder: {}", orderNo, storeOrder);
@@ -284,7 +284,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
             return R.success("OK");
         }
 
-        StorePaymentConfig config = getConfigByWechatPayPublicKeyId(serial);
+        WechatStorePaymentConfig config = getConfigByWechatPayPublicKeyId(serial);
         if (config == null) {
             log.warn("微信支付回调:未找到公钥序列号对应的店铺配置 serial={}", serial);
             return R.fail("未找到对应支付配置");
@@ -306,7 +306,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
 
         if (sign.verify(signatureBytes)) {
             final String notifyDataCopy = notifyData;
-            final StorePaymentConfig configCopy = config;
+            final WechatStorePaymentConfig configCopy = config;
             CompletableFuture.runAsync(() -> processNotifyBusiness(notifyDataCopy, configCopy));
             return R.success("OK");
         } else {
@@ -318,7 +318,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
      * 异步处理回调业务:解密并更新订单状态(文档建议应答后再处理业务,避免超时)
      * @param config 已通过 Wechatpay-Serial 解析得到的店铺支付配置,内含 apiV3Key
      */
-    private void processNotifyBusiness(String notifyData, StorePaymentConfig config) {
+    private void processNotifyBusiness(String notifyData, WechatStorePaymentConfig config) {
         try {
             if (config == null || !StringUtils.hasText(config.getApiV3Key())) {
                 log.warn("微信支付回调:无可用 apiV3Key 无法解密");
@@ -346,9 +346,9 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
             String tradeState = jsonObject.getString("trade_state");
             if ("SUCCESS".equals(tradeState)) {
                 String outTradeNo = jsonObject.getString("out_trade_no");
-                StoreOrder storeOrder = storeOrderService.getOne(new QueryWrapper<StoreOrder>().eq("order_no", outTradeNo));
+                WechatStoreOrder storeOrder = storeOrderService.getOne(new QueryWrapper<WechatStoreOrder>().eq("order_no", outTradeNo));
                 if (storeOrder == null && StringUtils.hasText(outTradeNo)) {
-                    storeOrder = storeOrderService.getOne(new QueryWrapper<StoreOrder>().eq("pay_trade_no", outTradeNo));
+                    storeOrder = storeOrderService.getOne(new QueryWrapper<WechatStoreOrder>().eq("pay_trade_no", outTradeNo));
                 }
                 if (storeOrder != null && storeOrder.getPayStatus() != 1) {
                     storeOrder.setPayStatus(1);
@@ -395,7 +395,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
         if (storeId == null) {
             return R.fail("店铺ID不能为空");
         }
-        StorePaymentConfig config = getConfigByStoreId(storeId);
+        WechatStorePaymentConfig config = getConfigByStoreId(storeId);
         if (config == null) {
             return R.fail("未找到该店铺的微信支付配置");
         }
@@ -417,7 +417,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     public DirectAPIv3QueryResponse searchOrderRun(QueryByWxTradeNoRequest request,
-                                                   StorePaymentConfig config,
+                                                   WechatStorePaymentConfig config,
                                                    PrivateKey privateKey,
                                                    PublicKey wechatPayPublicKey) {
         String uri = searchOrderByOutTradeNoPath;
@@ -467,7 +467,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
         if (storeId == null) {
             return R.fail("店铺ID不能为空");
         }
-        StorePaymentConfig config = getConfigByStoreId(storeId);
+        WechatStorePaymentConfig config = getConfigByStoreId(storeId);
         if (config == null) {
             return R.fail("未找到该店铺的微信支付配置");
         }
@@ -540,7 +540,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     public Refund doSearchRefundRecordByOutRefundNo(QueryByOutRefundNoRequest request,
-                                                   StorePaymentConfig config,
+                                                   WechatStorePaymentConfig config,
                                                    PrivateKey privateKey,
                                                    PublicKey wechatPayPublicKey) {
         String uri = searchRefundStatusByOutRefundNoPath;
@@ -568,7 +568,7 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     public DirectAPIv3JsapiPrepayResponse doCreatePrePayOrder(DirectAPIv3JsapiPrepayRequest request,
-                                                             StorePaymentConfig config,
+                                                             WechatStorePaymentConfig config,
                                                              PrivateKey privateKey) {
         String uri = prePayPath;
         String reqBody = WXPayUtility.toJson(request);

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatAddCartItemDTO.java

@@ -0,0 +1,32 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Positive;
+
+/**
+ * 小程序点餐-添加购物车商品(自 {@link shop.alien.entity.store.dto.AddCartItemDTO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatAddCartItemDTO", description = "小程序添加购物车商品")
+public class WechatAddCartItemDTO {
+
+    @ApiModelProperty(value = "桌号ID", required = true)
+    @NotNull(message = "桌号ID不能为空")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "菜品ID", required = true)
+    @NotNull(message = "菜品ID不能为空")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "数量", required = true)
+    @NotNull(message = "数量不能为空")
+    @Positive(message = "数量必须大于0")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

+ 34 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCartDTO.java

@@ -0,0 +1,34 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 小程序点餐-购物车(字段自 {@link shop.alien.entity.store.dto.CartDTO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatCartDTO", description = "小程序点餐购物车")
+public class WechatCartDTO {
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "购物车商品列表")
+    private List<WechatCartItemDTO> items;
+
+    @ApiModelProperty(value = "总金额")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "商品总数量")
+    private Integer totalQuantity;
+}

+ 51 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCartItemDTO.java

@@ -0,0 +1,51 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 小程序点餐-购物车商品项(字段自 {@link shop.alien.entity.store.dto.CartItemDTO} 复制,可独立演进)
+ */
+@Data
+@ApiModel(value = "WechatCartItemDTO", description = "小程序点餐购物车商品项")
+public class WechatCartItemDTO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "已下单数量(下单时锁定的数量,不允许减少或删除)")
+    private Integer lockedQuantity;
+
+    @ApiModelProperty(value = "小计金额")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    private String addUserPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    private String tags;
+}

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatChangeTableDTO.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 小程序点餐-换桌(自 {@link shop.alien.entity.store.dto.ChangeTableDTO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatChangeTableDTO", description = "小程序换桌")
+public class WechatChangeTableDTO {
+
+    @ApiModelProperty(value = "原桌号ID", required = true)
+    @NotNull(message = "原桌号ID不能为空")
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "目标桌号ID", required = true)
+    @NotNull(message = "目标桌号ID不能为空")
+    private Integer toTableId;
+
+    @ApiModelProperty(value = "换桌原因")
+    private String changeReason;
+}

+ 47 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatCreateOrderDTO.java

@@ -0,0 +1,47 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * 小程序点餐-创建订单 DTO(自 {@link shop.alien.entity.store.dto.CreateOrderDTO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatCreateOrderDTO", description = "小程序创建订单")
+public class WechatCreateOrderDTO {
+
+    @ApiModelProperty(value = "桌号ID", required = true)
+    @NotNull(message = "桌号ID不能为空")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "优惠券ID(可选,不选择优惠券时传 null 或不传此字段)")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "订单总金额(前端传参,不做后台校验)")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费(前端传参,不做后台校验)")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠金额(前端传参,不做后台校验)")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "实付金额(前端传参,不做后台校验)")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注(限30字)。创建/更新订单时传入;更新订单(加餐)时为覆盖,不拼接")
+    private String remark;
+
+    @ApiModelProperty(value = "是否立即支付(0:否,创建订单但不支付; 1:是,创建订单并支付)")
+    private Integer immediatePay;
+}

+ 23 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatStoreInfoWithHomepageCuisinesDTO.java

@@ -0,0 +1,23 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+
+import java.util.List;
+
+/**
+ * 小程序点餐-店铺信息和首页展示美食价目表(自 {@link shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO} 复制,价目表为 {@link WechatStoreCuisine})
+ */
+@Data
+@ApiModel(value = "WechatStoreInfoWithHomepageCuisinesDTO", description = "小程序-店铺信息和首页展示美食价目表")
+public class WechatStoreInfoWithHomepageCuisinesDTO {
+
+    @ApiModelProperty(value = "店铺信息")
+    private StoreInfo storeInfo;
+
+    @ApiModelProperty(value = "首页展示的美食价目表列表(WechatStoreCuisine)")
+    private List<WechatStoreCuisine> homepageCuisines;
+}

+ 22 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/wechat/WechatUpdateOrderCouponDTO.java

@@ -0,0 +1,22 @@
+package shop.alien.entity.store.dto.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 小程序点餐-更新订单优惠券(自 {@link shop.alien.entity.store.dto.UpdateOrderCouponDTO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatUpdateOrderCouponDTO", description = "小程序更新订单优惠券")
+public class WechatUpdateOrderCouponDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    @NotNull(message = "订单ID不能为空")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "优惠券ID(可为空,表示不使用优惠券)")
+    private Integer couponId;
+}

+ 30 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatCategoryWithCuisinesVO.java

@@ -0,0 +1,30 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCategory;
+
+import java.util.List;
+
+/**
+ * 小程序点餐-菜品种类及其下属菜品 VO(自 {@link shop.alien.entity.store.vo.CategoryWithCuisinesVO} 复制,使用 wechat 实体类型)
+ * <p>
+ * category 与 GET /store/info/categories 单条字段一致(WechatStoreCuisineCategory);cuisines 与 GET /store/info/cuisines 列表元素一致(WechatStoreCuisine)。
+ * </p>
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel(value = "WechatCategoryWithCuisinesVO", description = "小程序-菜品种类及该分类下的菜品列表")
+public class WechatCategoryWithCuisinesVO {
+
+    @ApiModelProperty(value = "菜品种类信息(WechatStoreCuisineCategory)")
+    private WechatStoreCuisineCategory category;
+
+    @ApiModelProperty(value = "该分类下的菜品列表(WechatStoreCuisine)")
+    private List<WechatStoreCuisine> cuisines;
+}

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderChangeLogBatchVO.java

@@ -0,0 +1,50 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 小程序点餐-订单变更记录批次(自 {@link shop.alien.entity.store.vo.OrderChangeLogBatchVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderChangeLogBatchVO", description = "小程序订单变更记录批次")
+public class WechatOrderChangeLogBatchVO {
+
+    @ApiModelProperty(value = "批次号")
+    private String batchNo;
+
+    @ApiModelProperty(value = "操作类型(1:首次下单, 3:更新订单)")
+    private Integer operationType;
+
+    @ApiModelProperty(value = "操作类型文本")
+    private String operationTypeText;
+
+    @ApiModelProperty(value = "操作时间")
+    private Date operationTime;
+
+    @ApiModelProperty(value = "操作人ID")
+    private Integer operatorUserId;
+
+    @ApiModelProperty(value = "操作人手机号")
+    private String operatorUserPhone;
+
+    @ApiModelProperty(value = "备注(该批次对应的备注,如下单/更新订单时的备注)")
+    private String remark;
+
+    @ApiModelProperty(value = "该批次商品数量变化总和")
+    private Integer totalQuantityChange;
+
+    @ApiModelProperty(value = "该批次金额变化总和")
+    private BigDecimal totalAmountChange;
+
+    @ApiModelProperty(value = "该批次商品数量")
+    private Integer itemCount;
+
+    @ApiModelProperty(value = "该批次的商品明细列表")
+    private List<WechatOrderChangeLogItemVO> items;
+}

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderChangeLogItemVO.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 小程序点餐-订单变更记录商品项(自 {@link shop.alien.entity.store.vo.OrderChangeLogItemVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderChangeLogItemVO", description = "小程序订单变更记录商品项")
+public class WechatOrderChangeLogItemVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量变化(新增的数量,正数表示增加,负数表示减少)")
+    private Integer quantityChange;
+
+    @ApiModelProperty(value = "变化前数量")
+    private Integer quantityBefore;
+
+    @ApiModelProperty(value = "变化后数量")
+    private Integer quantityAfter;
+
+    @ApiModelProperty(value = "金额变化")
+    private BigDecimal amountChange;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    private String tags;
+}

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderConfirmVO.java

@@ -0,0 +1,66 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.wechat.WechatCartItemDTO;
+import shop.alien.entity.store.vo.AvailableCouponVO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 小程序点餐-订单确认页(自 {@link shop.alien.entity.store.vo.OrderConfirmVO} 复制,购物车项为 {@link WechatCartItemDTO})
+ */
+@Data
+@ApiModel(value = "WechatOrderConfirmVO", description = "小程序订单确认页面")
+public class WechatOrderConfirmVO {
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注(限30字)")
+    private String remark;
+
+    @ApiModelProperty(value = "购物车商品列表")
+    private List<WechatCartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "餐具费单价")
+    private BigDecimal tablewareUnitPrice;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额(菜品总价+餐具费-优惠券)")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "可用优惠券列表")
+    private List<AvailableCouponVO> availableCoupons;
+
+    @ApiModelProperty(value = "是否已锁定(有人正在下单)")
+    private Boolean isLocked;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    private Integer lockUserId;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderCuisineItemVO.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 小程序点餐-订单菜品项(自 {@link shop.alien.entity.store.vo.OrderCuisineItemVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderCuisineItemVO", description = "小程序订单菜品项")
+public class WechatOrderCuisineItemVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "数量")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    private String tags;
+}

+ 74 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderDetailWithChangeLogVO.java

@@ -0,0 +1,74 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 小程序点餐-订单详情含变更记录(自 {@link shop.alien.entity.store.vo.OrderDetailWithChangeLogVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderDetailWithChangeLogVO", description = "小程序订单详情(含变更记录)")
+public class WechatOrderDetailWithChangeLogVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "支付状态(0:未支付, 1:已支付, 2:已退款)")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付方式(1:微信, 2:支付宝, 3:现金)")
+    private Integer payType;
+
+    @ApiModelProperty(value = "创建时间")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "支付时间")
+    private Date payTime;
+
+    @ApiModelProperty(value = "订单变更记录批次列表")
+    private List<WechatOrderChangeLogBatchVO> changeLogBatches;
+}

+ 105 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderInfoVO.java

@@ -0,0 +1,105 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.wechat.WechatCartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 小程序点餐-订单信息(自 {@link shop.alien.entity.store.vo.OrderInfoVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderInfoVO", description = "小程序订单信息")
+public class WechatOrderInfoVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "店铺ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "门店电话")
+    private String storeTel;
+
+    @ApiModelProperty(value = "门店地址")
+    private String storeAddress;
+
+    @ApiModelProperty(value = "门店简介")
+    private String storeBlurb;
+
+    @ApiModelProperty(value = "门店类型(1:中餐, 2:烧烤, 3:饮品, 4:甜点, 5:火锅, 6:宵夜, 7:西餐, 8:轻食, 9:水果)")
+    private String storeType;
+
+    @ApiModelProperty(value = "营业状态(-1:注销中, 0:正常营业, 1:暂停营业, 2:筹建中, 99:永久关门)")
+    private Integer businessStatus;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品清单")
+    private List<WechatCartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @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;
+
+    @ApiModelProperty(value = "应付金额")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "支付状态(0:未支付, 1:已支付, 2:已退款)")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付方式(1:微信, 2:支付宝, 3:现金)")
+    private Integer payType;
+
+    @ApiModelProperty(value = "创建时间")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "支付时间")
+    private Date payTime;
+}

+ 65 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderSettlementVO.java

@@ -0,0 +1,65 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.wechat.WechatCartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 小程序点餐-结算确认页(自 {@link shop.alien.entity.store.vo.OrderSettlementVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderSettlementVO", description = "小程序订单结算确认页面")
+public class WechatOrderSettlementVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品清单")
+    private List<WechatCartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "是否已锁定(有人正在结算)")
+    private Boolean isLocked;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    private Integer lockUserId;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatOrderSuccessVO.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 小程序点餐-下单成功页(自 {@link shop.alien.entity.store.vo.OrderSuccessVO} 复制)
+ */
+@Data
+@ApiModel(value = "WechatOrderSuccessVO", description = "小程序下单成功")
+public class WechatOrderSuccessVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    private Integer orderStatus;
+}

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/wechat/WechatStoreOrderPageVO.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.store.vo.wechat;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.wechat.WechatStoreOrder;
+
+import java.util.List;
+
+/**
+ * 小程序点餐-订单分页 VO(自 {@link shop.alien.entity.store.vo.StoreOrderPageVO} 拆分,订单实体为 {@link WechatStoreOrder})
+ */
+@Data
+@ApiModel(value = "WechatStoreOrderPageVO", description = "小程序订单分页(含菜品)")
+public class WechatStoreOrderPageVO {
+
+    @ApiModelProperty(value = "订单信息")
+    private WechatStoreOrder order;
+
+    @ApiModelProperty(value = "门店名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "订单菜品列表")
+    private List<WechatOrderCuisineItemVO> cuisineItems;
+}

+ 99 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCart.java

@@ -0,0 +1,99 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 购物车表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_cart")
+@ApiModel(value = "WechatStoreCart对象", description = "购物车表")
+public class WechatStoreCart {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    @TableField("quantity")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "已下单数量(下单时锁定的数量,不允许减少或删除)")
+    @TableField("locked_quantity")
+    private Integer lockedQuantity;
+
+    @ApiModelProperty(value = "小计金额")
+    @TableField("subtotal_amount")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    @TableField("add_user_id")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    @TableField("add_user_phone")
+    private String addUserPhone;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 88 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCouponUsage.java

@@ -0,0 +1,88 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 优惠券使用记录表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_coupon_usage")
+@ApiModel(value = "WechatStoreCouponUsage对象", description = "优惠券使用记录表")
+public class WechatStoreCouponUsage {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "订单ID(下单时关联)")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "优惠券ID")
+    @TableField("coupon_id")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    @TableField("coupon_name")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    @TableField("discount_amount")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "使用状态(0:已标记使用, 1:已下单, 2:已支付, 3:已取消)")
+    @TableField("usage_status")
+    private Integer usageStatus;
+
+    @ApiModelProperty(value = "换桌前的桌号ID(如果是换桌迁移)")
+    @TableField("from_table_id")
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "换桌迁移时间")
+    @TableField("migrate_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date migrateTime;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 143 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisine.java

@@ -0,0 +1,143 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 美食价目表
+ *
+ * @author auto-generated
+ * @since 2025-01-01
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_cuisine")
+@ApiModel(value = "WechatStoreCuisine对象", description = "美食价目表")
+public class WechatStoreCuisine {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "商户id")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "美食类型: 1-单品,2-套餐")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+    @TableField("category_ids")
+    private String categoryIds;
+
+    @ApiModelProperty(value = "菜名")
+    @TableField("name")
+    private String name;
+
+    @ApiModelProperty(value = "总价")
+    @TableField("total_price")
+    private BigDecimal totalPrice;
+
+    @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+    @TableField("is_homepage_display")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    @TableField("tags")
+    private String tags;
+
+    @ApiModelProperty(value = "菜品短评")
+    @TableField("dish_review")
+    private String dishReview;
+
+    @ApiModelProperty(value = "图片列表,最多 9 张 URL")
+    @TableField("images")
+    private String images;
+
+    @ApiModelProperty(value = "图文详情-图片")
+    @TableField("image_content")
+    private String imageContent;
+
+    @ApiModelProperty(value = "菜品原料json(原料名称:name,所需重量:height,成本价:cost,推荐价格:suggest)")
+    @TableField("raw_json")
+    private String rawJson;
+
+    @ApiModelProperty(value = "图文详情-文字")
+    @TableField("detail_content")
+    private String detailContent;
+
+    @ApiModelProperty(value = "菜品描述")
+    @TableField("description")
+    private String description;
+
+    @ApiModelProperty(value = "补充说明")
+    @TableField("extra_note")
+    private String extraNote;
+
+    @ApiModelProperty(value = "是否需要预约:0=否,1=是")
+    @TableField("need_reserve")
+    private Integer needReserve;
+
+    @ApiModelProperty(value = "预约规则")
+    @TableField("reserve_rule")
+    private String reserveRule;
+
+    @ApiModelProperty(value = "适用人数")
+    @TableField("people_limit")
+    private String peopleLimit;
+
+    @ApiModelProperty(value = "使用规则")
+    @TableField("usage_rule")
+    private String usageRule;
+
+    @ApiModelProperty(value = "状态:0-待审核 1-审核通过 2-审核拒绝")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "上下架状态:1-上架,2-下架")
+    @TableField("shelf_status")
+    private Integer shelfStatus;
+
+    @ApiModelProperty(value = "拒绝原因(审核失败原因)")
+    @TableField("rejection_reason")
+    private String rejectionReason;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建人")
+    @TableField(value = "created_user_id", fill = FieldFill.INSERT)
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新人")
+    @TableField(value = "updated_user_id", fill = FieldFill.UPDATE)
+    private Integer updatedUserId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "AI审核时间")
+    @TableField(value = "audit_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date auditTime;
+
+}
+
+

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisineCategory.java

@@ -0,0 +1,66 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 菜品分类表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_cuisine_category")
+@ApiModel(value = "WechatStoreCuisineCategory对象", description = "菜品分类表")
+public class WechatStoreCuisineCategory {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类名称")
+    @TableField("category_name")
+    private String categoryName;
+
+    @ApiModelProperty(value = "状态(0:禁用, 1:启用)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "排序")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreCuisineCombo.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 美食套餐表(辅助详情查询)
+ *
+ * @author auto-generated
+ * @since 2025-01-01
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_cuisine_combo")
+@ApiModel(value = "WechatStoreCuisineCombo对象", description = "美食套餐表(辅助详情查询)")
+public class WechatStoreCuisineCombo {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "单品id")
+    @TableField("sid")
+    private Integer sid;
+
+    @ApiModelProperty(value = "套餐id")
+    @TableField("cid")
+    private Integer cid;
+
+    @ApiModelProperty(value = "套餐包含该单品的数量")
+    @TableField("snum")
+    private Integer snum;
+
+    @ApiModelProperty(value = "类别")
+    @TableField("category")
+    private String category;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建人")
+    @TableField(value = "created_user_id", fill = FieldFill.INSERT)
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新人")
+    @TableField(value = "updated_user_id", fill = FieldFill.UPDATE)
+    private Integer updatedUserId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}
+
+
+

+ 141 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrder.java

@@ -0,0 +1,141 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_order")
+@ApiModel(value = "WechatStoreOrder对象", description = "订单表")
+public class WechatStoreOrder {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    @TableField("table_number")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    @TableField("diner_count")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "支付用户ID")
+    @TableField("pay_user_id")
+    private Integer payUserId;
+
+    @ApiModelProperty(value = "支付用户手机号")
+    @TableField("pay_user_phone")
+    private String payUserPhone;
+
+    @ApiModelProperty(value = "联系电话")
+    @TableField("contact_phone")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "餐具费")
+    @TableField("tableware_fee")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    @TableField("order_status")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "订单总金额")
+    @TableField("total_amount")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "优惠券ID")
+    @TableField("coupon_id")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "当前使用的优惠券ID(用于换桌场景)")
+    @TableField("current_coupon_id")
+    private Integer currentCouponId;
+
+    @ApiModelProperty(value = "优惠金额")
+    @TableField("discount_amount")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "锁定用户ID(下单或结算时锁定)")
+    @TableField("lock_user_id")
+    private Integer lockUserId;
+
+    @ApiModelProperty(value = "锁定过期时间")
+    @TableField("lock_expire_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lockExpireTime;
+
+    @ApiModelProperty(value = "实付金额")
+    @TableField("pay_amount")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "支付方式(1:微信, 2:支付宝, 3:现金)")
+    @TableField("pay_type")
+    private Integer payType;
+
+    @ApiModelProperty(value = "支付状态(0:未支付, 1:已支付, 2:已退款)")
+    @TableField("pay_status")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date payTime;
+
+    @ApiModelProperty(value = "支付交易号")
+    @TableField("pay_trade_no")
+    private String payTradeNo;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 120 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderChangeLog.java

@@ -0,0 +1,120 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单变更记录表(记录每次下单/更新订单的商品变化)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_order_change_log")
+@ApiModel(value = "WechatStoreOrderChangeLog对象", description = "订单变更记录表")
+public class WechatStoreOrderChangeLog {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单ID")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "批次号(同一时间点的操作使用同一批次号,用于分组展示)")
+    @TableField("batch_no")
+    private String batchNo;
+
+    @ApiModelProperty(value = "操作类型(1:首次下单, 3:更新订单)")
+    @TableField("operation_type")
+    private Integer operationType;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量变化(新增的数量,正数表示增加,负数表示减少)")
+    @TableField("quantity_change")
+    private Integer quantityChange;
+
+    @ApiModelProperty(value = "变化前数量(用于展示变化前后对比)")
+    @TableField("quantity_before")
+    private Integer quantityBefore;
+
+    @ApiModelProperty(value = "变化后数量(用于展示变化前后对比)")
+    @TableField("quantity_after")
+    private Integer quantityAfter;
+
+    @ApiModelProperty(value = "金额变化(新增的金额)")
+    @TableField("amount_change")
+    private BigDecimal amountChange;
+
+    @ApiModelProperty(value = "操作时间")
+    @TableField("operation_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date operationTime;
+
+    @ApiModelProperty(value = "操作人ID")
+    @TableField("operator_user_id")
+    private Integer operatorUserId;
+
+    @ApiModelProperty(value = "操作人手机号")
+    @TableField("operator_user_phone")
+    private String operatorUserPhone;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 108 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderDetail.java

@@ -0,0 +1,108 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单明细表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_order_detail")
+@ApiModel(value = "WechatStoreOrderDetail对象", description = "订单明细表")
+public class WechatStoreOrderDetail {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单ID")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    @TableField("quantity")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "小计金额")
+    @TableField("subtotal_amount")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    @TableField("add_user_id")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    @TableField("add_user_phone")
+    private String addUserPhone;
+
+    @ApiModelProperty(value = "是否加餐(0:初始下单, 1:加餐)")
+    @TableField("is_add_dish")
+    private Integer isAddDish;
+
+    @ApiModelProperty(value = "加餐时间")
+    @TableField("add_dish_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date addDishTime;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 76 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreOrderLock.java

@@ -0,0 +1,76 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 订单锁定记录表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_order_lock")
+@ApiModel(value = "WechatStoreOrderLock对象", description = "订单锁定记录表")
+public class WechatStoreOrderLock {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID(下单锁定)")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "订单ID(结算锁定)")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "锁定类型(1:下单锁定, 2:结算锁定)")
+    @TableField("lock_type")
+    private Integer lockType;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    @TableField("lock_user_id")
+    private Integer lockUserId;
+
+    @ApiModelProperty(value = "锁定用户手机号")
+    @TableField("lock_user_phone")
+    private String lockUserPhone;
+
+    @ApiModelProperty(value = "锁定过期时间")
+    @TableField("lock_expire_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lockExpireTime;
+
+    @ApiModelProperty(value = "锁定状态(0:已释放, 1:锁定中)")
+    @TableField("lock_status")
+    private Integer lockStatus;
+
+    @ApiModelProperty(value = "释放时间")
+    @TableField("release_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date releaseTime;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 177 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStorePaymentConfig.java

@@ -0,0 +1,177 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.util.typehandler.BlobByteArrayTypeHandler;
+
+import java.util.Date;
+
+/**
+ * 支付配置表(支付宝等)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_payment_config")
+@ApiModel(value = "WechatStorePaymentConfig对象", description = "支付配置表")
+public class WechatStorePaymentConfig {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "店铺ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "店铺用户id")
+    @TableField("store_user_id")
+    private Integer storeUserId;
+
+    @ApiModelProperty(value = "应用ID")
+    @TableField("app_id")
+    private String appId;
+
+    @ApiModelProperty(value = "应用私钥")
+    @TableField("app_secret_cert")
+    private String appSecretCert;
+
+    @ApiModelProperty(value = "应用公钥证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "app_public_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] appPublicCert;
+
+    @ApiModelProperty(value = "应用公钥证书路径")
+    @TableField("app_public_cert_path")
+    private String appPublicCertPath;
+
+    @ApiModelProperty(value = "应用公钥证书文件名称")
+    @TableField("app_public_cert_name")
+    private String appPublicCertName;
+
+    @ApiModelProperty(value = "支付宝公钥证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "alipay_public_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] alipayPublicCert;
+
+    @ApiModelProperty(value = "支付宝公钥证书路径")
+    @TableField("alipay_public_cert_path")
+    private String alipayPublicCertPath;
+
+    @ApiModelProperty(value = "支付宝公钥证书文件名称")
+    @TableField("alipay_public_cert_name")
+    private String alipayPublicCertName;
+
+    @ApiModelProperty(value = "支付宝根证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "alipay_root_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] alipayRootCert;
+
+    @ApiModelProperty(value = "支付宝根证书路径")
+    @TableField("alipay_root_cert_path")
+    private String alipayRootCertPath;
+
+    @ApiModelProperty(value = "支付宝根证书文件名称")
+    @TableField("alipay_root_cert_name")
+    private String alipayRootCertName;
+
+    @ApiModelProperty(value = "微信appId")
+    @TableField("wechat_app_id")
+    private String wechatAppId;
+
+    @ApiModelProperty(value = "微信支付mini appId")
+    @TableField("wechat_mini_app_id")
+    private String wechatMiniAppId;
+
+    @ApiModelProperty(value = "微信mchId")
+    @TableField("wechat_mch_id")
+    private String wechatMchId;
+
+    @ApiModelProperty(value = "微信商户API证书序列号")
+    @TableField("merchant_serial_number")
+    private String merchantSerialNumber;
+
+    @ApiModelProperty(value = "微信商户的 APIv3 Key")
+    @TableField("api_v3_key")
+    private String apiV3Key;
+
+    @ApiModelProperty(value = "微信支付公钥ID")
+    @TableField("wechat_pay_public_key_id")
+    private String wechatPayPublicKeyId;
+
+    @ApiModelProperty(value = "微信私钥路径")
+    @TableField("wechat_private_key_path")
+    private String wechatPrivateKeyPath;
+
+    @ApiModelProperty(value = "微信私钥文件名")
+    @TableField("wechat_private_key_name")
+    private String wechatPrivateKeyName;
+
+    @ApiModelProperty(value = "微信私钥文件(存储文件内容)BLOB,原样存取不经过字符编码")
+    @TableField(value = "wechat_private_key_file", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] wechatPrivateKeyFile;
+
+    @ApiModelProperty(value = "微信公钥路径")
+    @TableField("wechat_pay_public_key_file_path")
+    private String wechatPayPublicKeyFilePath;
+
+    @ApiModelProperty(value = "微信公钥文件名")
+    @TableField("wechat_pay_public_key_file_name")
+    private String wechatPayPublicKeyFileName;
+
+    @ApiModelProperty(value = "微信公钥文件(存储文件内容)BLOB,原样存取不经过字符编码")
+    @TableField(value = "wechat_pay_public_key_file", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] wechatPayPublicKeyFile;
+
+    @ApiModelProperty(value = "收款银行卡号")
+    @TableField("bank_card_no")
+    private String bankCardNo;
+
+    @ApiModelProperty(value = "开户银行名称")
+    @TableField("bank_name")
+    private String bankName;
+
+    @ApiModelProperty(value = "商家微信 appid")
+    @TableField("store_wechat_id")
+    private String storeWechatId;
+
+    @ApiModelProperty(value = "商家微信名称")
+    @TableField("store_wechat_name")
+    private String storeWechatName;
+
+    @ApiModelProperty(value = "商家支付宝 appid")
+    @TableField("store_ali_id")
+    private String storeAliId;
+
+    @ApiModelProperty(value = "商家支付宝名称")
+    @TableField("store_ali_name")
+    private String storeAliName;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 98 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreTable.java

@@ -0,0 +1,98 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 桌号表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_table")
+@ApiModel(value = "WechatStoreTable对象", description = "桌号表")
+public class WechatStoreTable {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号")
+    @TableField("table_number")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "分类ID(关联store_booking_category表)")
+    @TableField("category_id")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "座位数")
+    @TableField("seating_capacity")
+    private Integer seatingCapacity;
+
+    @ApiModelProperty(value = "当前进行中的订单ID(订单结账或取消后清空)")
+    @TableField("current_order_id")
+    private Integer currentOrderId;
+
+    @ApiModelProperty(value = "当前使用的优惠券ID")
+    @TableField("current_coupon_id")
+    private Integer currentCouponId;
+
+    @ApiModelProperty(value = "购物车商品数量(缓存)")
+    @TableField("cart_item_count")
+    private Integer cartItemCount;
+
+    @ApiModelProperty(value = "购物车总金额(缓存)")
+    @TableField("cart_total_amount")
+    private java.math.BigDecimal cartTotalAmount;
+
+    @ApiModelProperty(value = "二维码URL")
+    @TableField("qrcode_url")
+    private String qrcodeUrl;
+
+    @ApiModelProperty(value = "状态(0:空闲, 1:就餐中, 2:其他, 3:加餐)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "当前就餐人数(就餐中时由首客填写,后续用户共用)")
+    @TableField("diner_count")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 78 - 0
alien-entity/src/main/java/shop/alien/entity/store/wechat/WechatStoreTableLog.java

@@ -0,0 +1,78 @@
+package shop.alien.entity.store.wechat;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 桌号换桌记录表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("wechat_store_table_log")
+@ApiModel(value = "WechatStoreTableLog对象", description = "桌号换桌记录表")
+public class WechatStoreTableLog {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "订单ID")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "原桌号ID")
+    @TableField("from_table_id")
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "原桌号")
+    @TableField("from_table_number")
+    private String fromTableNumber;
+
+    @ApiModelProperty(value = "目标桌号ID")
+    @TableField("to_table_id")
+    private Integer toTableId;
+
+    @ApiModelProperty(value = "目标桌号")
+    @TableField("to_table_number")
+    private String toTableNumber;
+
+    @ApiModelProperty(value = "换桌原因")
+    @TableField("change_reason")
+    private String changeReason;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCartMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreCart;
+
+/**
+ * 小程序点餐专用表 WechatStoreCart Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreCartMapper extends BaseMapper<WechatStoreCart> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCouponUsageMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreCouponUsage;
+
+/**
+ * 小程序点餐专用表 WechatStoreCouponUsage Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreCouponUsageMapper extends BaseMapper<WechatStoreCouponUsage> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineCategoryMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCategory;
+
+/**
+ * 小程序点餐专用表 WechatStoreCuisineCategory Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreCuisineCategoryMapper extends BaseMapper<WechatStoreCuisineCategory> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineComboMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreCuisineCombo;
+
+/**
+ * 小程序点餐专用表 WechatStoreCuisineCombo Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreCuisineComboMapper extends BaseMapper<WechatStoreCuisineCombo> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreCuisineMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreCuisine;
+
+/**
+ * 小程序点餐专用表 WechatStoreCuisine Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreCuisineMapper extends BaseMapper<WechatStoreCuisine> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderChangeLogMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreOrderChangeLog;
+
+/**
+ * 小程序点餐专用表 WechatStoreOrderChangeLog Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreOrderChangeLogMapper extends BaseMapper<WechatStoreOrderChangeLog> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderDetailMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreOrderDetail;
+
+/**
+ * 小程序点餐专用表 WechatStoreOrderDetail Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreOrderDetailMapper extends BaseMapper<WechatStoreOrderDetail> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderLockMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreOrderLock;
+
+/**
+ * 小程序点餐专用表 WechatStoreOrderLock Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreOrderLockMapper extends BaseMapper<WechatStoreOrderLock> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreOrderMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreOrder;
+
+/**
+ * 小程序点餐专用表 WechatStoreOrder Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreOrderMapper extends BaseMapper<WechatStoreOrder> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStorePaymentConfigMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStorePaymentConfig;
+
+/**
+ * 小程序点餐专用表 WechatStorePaymentConfig Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStorePaymentConfigMapper extends BaseMapper<WechatStorePaymentConfig> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreTableLogMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreTableLog;
+
+/**
+ * 小程序点餐专用表 WechatStoreTableLog Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreTableLogMapper extends BaseMapper<WechatStoreTableLog> {
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/wechat/WechatStoreTableMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.wechat;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.wechat.WechatStoreTable;
+
+/**
+ * 小程序点餐专用表 WechatStoreTable Mapper(与 APP 侧物理表隔离)
+ */
+public interface WechatStoreTableMapper extends BaseMapper<WechatStoreTable> {
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/annotation/OperationLog.java

@@ -0,0 +1,26 @@
+package shop.alien.store.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志注解
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface OperationLog {
+    /**
+     * 操作描述
+     */
+    String value() default "";
+
+    /**
+     * 操作类型
+     */
+    String type() default "";
+}
+

+ 22 - 0
alien-store/src/main/java/shop/alien/store/aspect/DiningOperationLogAspect.java

@@ -0,0 +1,22 @@
+package shop.alien.store.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+import shop.alien.store.annotation.OperationLog;
+
+/**
+ * 点餐模块操作日志切面(与 store 全局 OperationLogAspect 区分 Bean 名)
+ */
+@Slf4j
+@Aspect
+@Component
+public class DiningOperationLogAspect {
+
+    @Before("@annotation(operationLog)")
+    public void before(JoinPoint joinPoint, OperationLog operationLog) {
+        log.info("操作日志: {}", operationLog.value());
+    }
+}

+ 48 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningCollectController.java

@@ -0,0 +1,48 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+import shop.alien.store.service.DiningCollectService;
+
+/**
+ * 点餐模块-收藏控制器(Feign 调 store,供小程序使用)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Api(tags = {"微信点餐-收藏(用户端)"})
+@ApiSort(4)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/collect")
+@RequiredArgsConstructor
+public class DiningCollectController {
+
+    private final DiningCollectService diningCollectService;
+
+    /**
+     * 添加收藏
+     */
+    @ApiOperation(value = "添加收藏", notes = "添加收藏,支持收藏店铺、商品等")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/addCollect")
+    public R<Boolean> addCollect(@ApiParam(value = "收藏对象", required = true) @RequestBody LifeCollect lifeCollect) {
+        log.info("DiningCollectController.addCollect?lifeCollect={}", lifeCollect);
+        try {
+            return diningCollectService.addCollect(lifeCollect);
+        } catch (Exception e) {
+            log.error("添加收藏失败: {}", e.getMessage(), e);
+            return R.fail("添加收藏失败: " + e.getMessage());
+        }
+    }
+}

+ 307 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningController.java

@@ -0,0 +1,307 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.TableDiningStatusVO;
+import shop.alien.entity.store.vo.*;
+import shop.alien.store.service.DiningService;
+import shop.alien.store.util.TokenUtil;
+
+import java.util.List;
+
+/**
+ * 点餐控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"小程序-点餐管理"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/dining")
+@RequiredArgsConstructor
+public class DiningController {
+
+    private final DiningService diningService;
+
+    @ApiOperation(value = "查询餐桌是否处于就餐中", notes = "免登录可调用,用于前端判断是否跳过选择用餐人数。就餐中(status=1)、加餐(status=3) 均视为就餐状态,且 diner_count 有值 时 inDining=true")
+    @GetMapping("/table-dining-status")
+    public R<TableDiningStatusVO> getTableDiningStatus(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.getTableDiningStatus?tableId={}", tableId);
+        try {
+            TableDiningStatusVO vo = diningService.getTableDiningStatus(tableId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("查询餐桌就餐状态失败: {}", e.getMessage(), e);
+            return R.fail("查询餐桌就餐状态失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取点餐页面信息", notes = "首客选桌时传就餐人数,餐桌置为就餐中并保存人数;后续用户选同一桌可不传就餐人数,将使用表中已保存人数")
+    @GetMapping("/page-info")
+    public R<DiningPageInfoVO> getDiningPageInfo(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数(首客必传;餐桌已就餐中时可省略,将使用已保存人数)") @RequestParam(required = false) Integer dinerCount) {
+        log.info("DiningController.getDiningPageInfo?tableId={}, dinerCount={}", tableId, dinerCount);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            DiningPageInfoVO vo = diningService.getDiningPageInfo(tableId, dinerCount);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取点餐页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取点餐页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "搜索菜品", notes = "模糊搜索菜品,关键词限10字")
+    @GetMapping("/search")
+    public R<List<CuisineListVO>> searchCuisines(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "搜索关键词", required = false) @RequestParam(required = false) String keyword,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.searchCuisines?storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            // 限制关键词长度
+            if (StringUtils.hasText(keyword) && keyword.length() > 10) {
+                keyword = keyword.substring(0, 10);
+            }
+            List<CuisineListVO> list = diningService.searchCuisines(storeId, keyword, tableId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("搜索菜品失败: {}", e.getMessage(), e);
+            return R.fail("搜索菜品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据分类获取菜品列表", notes = "分页获取菜品列表,默认每页12条")
+    @GetMapping("/cuisines")
+    public R<List<CuisineListVO>> getCuisinesByCategory(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "分类ID", required = false) @RequestParam(required = false) Integer categoryId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "页码", required = false) @RequestParam(defaultValue = "1") Integer page,
+            @ApiParam(value = "每页数量", required = false) @RequestParam(defaultValue = "12") Integer size) {
+        log.info("DiningController.getCuisinesByCategory?storeId={}, categoryId={}, tableId={}, page={}, size={}", storeId, categoryId, tableId, page, size);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            List<CuisineListVO> list = diningService.getCuisinesByCategory(storeId, categoryId, tableId, page, size);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取菜品列表失败: {}", e.getMessage(), e);
+            return R.fail("获取菜品列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取菜品详情", notes = "获取菜品详细信息,包含图片列表、月售数量等")
+    @GetMapping("/cuisine/{cuisineId}")
+    public R<CuisineDetailVO> getCuisineDetail(
+            @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.getCuisineDetail?cuisineId={}, tableId={}", cuisineId, tableId);
+        try {
+            CuisineDetailVO vo = diningService.getCuisineDetail(cuisineId, tableId);
+            
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取菜品详情失败: {}", e.getMessage(), e);
+            return R.fail("获取菜品详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取可领取的优惠券列表", notes = "获取用户可领取的优惠券")
+    @GetMapping("/coupons/available")
+    public R<List<AvailableCouponVO>> getAvailableCoupons(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        log.info("DiningController.getAvailableCoupons?storeId={}", storeId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            List<AvailableCouponVO> list = diningService.getAvailableCoupons(storeId, userId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取可领取优惠券列表失败: {}", e.getMessage(), e);
+            return R.fail("获取可领取优惠券列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "领取优惠券", notes = "用户领取优惠券")
+    @PostMapping("/coupon/receive")
+    public R<Boolean> receiveCoupon(
+            @ApiParam(value = "优惠券ID", required = true) @RequestParam Integer couponId) {
+        log.info("DiningController.receiveCoupon?couponId={}", couponId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = diningService.receiveCoupon(couponId, userId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("领取优惠券失败: {}", e.getMessage(), e);
+            return R.fail("领取优惠券失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单确认页面信息", notes = "获取订单确认页面的所有信息")
+    @GetMapping("/order/confirm")
+    public R<OrderConfirmVO> getOrderConfirmInfo(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
+        log.info("DiningController.getOrderConfirmInfo?tableId={}, dinerCount={}", tableId, dinerCount);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            OrderConfirmVO vo = diningService.getOrderConfirmInfo(tableId, dinerCount, userId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取订单确认页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取订单确认页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "锁定订单", notes = "锁定订单,防止多人同时下单")
+    @PostMapping("/order/lock")
+    public R<Boolean> lockOrder(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.lockOrder?tableId={}", tableId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = diningService.lockOrder(tableId, userId);
+            if (!result) {
+                return R.fail("订单已被其他用户锁定,无法下单");
+            }
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("锁定订单失败: {}", e.getMessage(), e);
+            return R.fail("锁定订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "解锁订单", notes = "解锁订单")
+    @PostMapping("/order/unlock")
+    public R<Boolean> unlockOrder(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.unlockOrder?tableId={}", tableId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            diningService.unlockOrder(tableId, userId);
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("解锁订单失败: {}", e.getMessage(), e);
+            return R.fail("解锁订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "检查订单锁定状态", notes = "检查订单是否被锁定")
+    @GetMapping("/order/check-lock")
+    public R<Integer> checkOrderLock(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("DiningController.checkOrderLock?tableId={}", tableId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            Integer lockUserId = diningService.checkOrderLock(tableId);
+            return R.data(lockUserId);
+        } catch (Exception e) {
+            log.error("检查订单锁定状态失败: {}", e.getMessage(), e);
+            return R.fail("检查订单锁定状态失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单结算确认页面信息", notes = "获取订单结算确认页面的所有信息")
+    @GetMapping("/order/settlement")
+    public R<shop.alien.entity.store.vo.OrderSettlementVO> getOrderSettlementInfo(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        log.info("DiningController.getOrderSettlementInfo?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            shop.alien.entity.store.vo.OrderSettlementVO vo = diningService.getOrderSettlementInfo(orderId, userId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取订单结算确认页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取订单结算确认页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "锁定订单结算", notes = "锁定订单结算,防止多人同时结算")
+    @PostMapping("/order/settlement/lock")
+    public R<Boolean> lockSettlement(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        log.info("DiningController.lockSettlement?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = diningService.lockSettlement(orderId, userId);
+            if (!result) {
+                return R.fail("订单已被其他用户锁定,无法结算");
+            }
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("锁定订单结算失败: {}", e.getMessage(), e);
+            return R.fail("锁定订单结算失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "解锁订单结算", notes = "解锁订单结算")
+    @PostMapping("/order/settlement/unlock")
+    public R<Boolean> unlockSettlement(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        log.info("DiningController.unlockSettlement?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            diningService.unlockSettlement(orderId, userId);
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("解锁订单结算失败: {}", e.getMessage(), e);
+            return R.fail("解锁订单结算失败: " + e.getMessage());
+        }
+    }
+}

+ 188 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningCouponController.java

@@ -0,0 +1,188 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.LifeDiscountCouponVo;
+import shop.alien.store.service.DiningCouponService;
+
+import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 点餐模块-优惠券控制器(Feign 调 store,供小程序:我的优惠券/详情/选券)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/1/29
+ */
+@Slf4j
+@Api(tags = {"微信点餐-优惠券(用户端)"})
+@ApiSort(3)
+@CrossOrigin
+@RestController
+@RequestMapping({"/dining/coupon"})
+@RequiredArgsConstructor
+public class DiningCouponController {
+
+    private final DiningCouponService diningCouponService;
+
+    /**
+     * 获取该用户所有的优惠券列表
+     */
+    @ApiOperation("获取该用户所有的优惠券列表。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/getUserCouponList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "分页页数", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "size", value = "分页条数", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "tabType", value = "分页类型(0:全部(未使用),1:即将过期,2:已使用,3:已过期)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "type", value = "券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "storeId", value = "商铺ID,可为空,传则仅返回该商铺的优惠券", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "storeName", value = "商铺名称模糊查询,可为空", dataType = "String", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getUserCouponList(
+            HttpServletRequest request,
+            @RequestParam(value = "page", defaultValue = "1") int page,
+            @RequestParam(value = "size", defaultValue = "10") int size,
+            @RequestParam("tabType") String tabType,
+            @RequestParam(value = "type", required = false) Integer type,
+            @RequestParam(value = "couponType", required = false) Integer couponType,
+            @RequestParam(value = "storeId", required = false) String storeId,
+            @RequestParam(value = "storeName", required = false) String storeName) {
+        log.info("DiningCouponController.getUserCouponList?page={}, size={}, tabType={}, type={}, couponType={}, storeId={}, storeName={}", page, size, tabType, type, couponType, storeId, storeName);
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getUserCouponList(authorization, page, size, tabType, type, couponType, storeId, storeName);
+    }
+
+    /**
+     * 根据优惠券 id 获取优惠券详情(规则、门槛等)
+     */
+    @ApiOperation(value = "获取优惠券详情", notes = "需登录,请求头带 Authorization。counponId 与 store 接口拼写一致")
+    @GetMapping("/detail")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "counponId", value = "优惠券id", dataType = "String", paramType = "query", required = true)
+    })
+    public R<LifeDiscountCouponVo> getCounponDetailById(
+            HttpServletRequest request,
+            @RequestParam("counponId") String counponId) {
+        log.info("DiningCouponController.getCounponDetailById?counponId={}", counponId);
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getCounponDetailById(authorization, counponId);
+    }
+
+    /**
+     * 获取该门店下用户可用/不可用优惠券列表(用于购物车/下单选券,按金额区分)
+     */
+    @ApiOperation(value = "获取门店可用优惠券列表", notes = "需登录。按 storeId+amount 返回可用券与不可用券及原因(满减门槛等)。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @GetMapping("/storeUsableList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "amount", value = "当前消费金额(满减门槛)", dataType = "BigDecimal", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<Map<String, Object>> getStoreUserUsableCouponList(
+            HttpServletRequest request,
+            @RequestParam("storeId") String storeId,
+            @RequestParam("amount") BigDecimal amount,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        log.info("DiningCouponController.getStoreUserUsableCouponList?storeId={}, amount={}, couponType={}", storeId, amount, couponType);
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreUserUsableCouponList(authorization, storeId, amount, couponType);
+    }
+
+    /**
+     * 查询用户拥有的优惠券(按符合支付条件优先,其次优惠力度大的优先)
+     */
+    @ApiOperation(value = "查询用户拥有的优惠券", notes = "需登录。查询用户拥有的优惠券,排序:符合支付条件的优先,其次优惠力度大的优先")
+    @GetMapping("/userOwned")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID(可选,如果提供则只查询该门店的优惠券)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "amount", value = "当前消费金额(用于判断是否符合支付条件)", dataType = "BigDecimal", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCoupons(
+            @ApiParam(value = "门店ID(可选)") @RequestParam(required = false) String storeId,
+            @ApiParam(value = "当前消费金额(可选,用于判断是否符合支付条件)") @RequestParam(required = false) BigDecimal amount) {
+        log.info("DiningCouponController.getUserOwnedCoupons?storeId={}, amount={}", storeId, amount);
+        return diningCouponService.getUserOwnedCoupons(storeId, amount);
+    }
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     */
+    @ApiOperation(value = "获取该用户该店铺优惠券列表", notes = "需登录,请求头带 Authorization。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/getStoreUserCouponList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "商户id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getStoreUserCouponList(
+            HttpServletRequest request,
+            @ApiParam(value = "商户id", required = true) @RequestParam("storeId") String storeId,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        log.info("DiningCouponController.getStoreUserCouponList?storeId={}, couponType={}", storeId, couponType);
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreUserCouponList(authorization, storeId, couponType);
+    }
+
+    /**
+     * 查询用户目前所拥有的优惠券(可通过商铺ID进行查询)
+     */
+    @ApiOperation(value = "查询用户目前所拥有的优惠券", notes = "需登录。查询用户目前所拥有的优惠券(未使用且未过期),可通过商铺ID进行筛选。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/userOwnedByStore")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "商铺ID(可选,如果提供则只查询该商铺的优惠券)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(
+            @ApiParam(value = "商铺ID(可选)") @RequestParam(required = false) String storeId,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        log.info("DiningCouponController.getUserOwnedCouponsByStore?storeId={}, couponType={}", storeId, couponType);
+        return diningCouponService.getUserOwnedCouponsByStore(storeId, couponType);
+    }
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     */
+    @ApiOperation(value = "获取该店铺所有优惠券(分页)", notes = "需登录,请求头带 Authorization。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券。tab: 0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/getStoreAllCouponList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "size", value = "每页条数", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "storeId", value = "商户id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponName", value = "优惠券名称", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "tab", value = "分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "couponsFromType", value = "查询类型(1:我的优惠券,2:好友的优惠券)", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponStatus", value = "优惠券状态(0:草稿,1:正式)", dataType = "int", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false)
+    })
+    public R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(
+            HttpServletRequest request,
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(value = "page", defaultValue = "1") int page,
+            @ApiParam(value = "每页条数", defaultValue = "10") @RequestParam(value = "size", defaultValue = "10") int size,
+            @ApiParam(value = "商户id", required = true) @RequestParam("storeId") String storeId,
+            @ApiParam(value = "优惠券名称") @RequestParam(value = "couponName", required = false) String couponName,
+            @ApiParam(value = "分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)", required = true) @RequestParam("tab") String tab,
+            @ApiParam(value = "查询类型(1:我的优惠券,2:好友的优惠券)", defaultValue = "1") @RequestParam(value = "couponsFromType", defaultValue = "1") int couponsFromType,
+            @ApiParam(value = "优惠券状态(0:草稿,1:正式)", defaultValue = "1") @RequestParam(value = "couponStatus", defaultValue = "1", required = false) int couponStatus,
+            @ApiParam(value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券") @RequestParam(value = "couponType", required = false) Integer couponType) {
+        log.info("DiningCouponController.getStoreAllCouponList?page={}, size={}, storeId={}, couponName={}, tab={}, couponsFromType={}, couponStatus={}, couponType={}", page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreAllCouponList(authorization, page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+    }
+}

+ 60 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningFileUploadController.java

@@ -0,0 +1,60 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.store.feign.AlienStoreFeign;
+
+/**
+ * 点餐模块-文件上传控制器
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Api(tags = {"小程序-文件上传"})
+@ApiSort(10)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/file")
+@RequiredArgsConstructor
+public class DiningFileUploadController {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    /**
+     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
+     *
+     * @param file 文件对象
+     * @return R.data 为文件路径(String)
+     */
+    @ApiOperation(value = "上传图片到OSS", notes = "支持图片、视频或PDF文件上传,返回OSS文件路径")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/upload")
+    public R<String> upload(@RequestParam("file") MultipartFile file) {
+        log.info("DiningFileUploadController.upload?fileName={}", file != null ? file.getOriginalFilename() : null);
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+            R<String> result = alienStoreFeign.uploadFile(file);
+            if (result.isSuccess()) {
+                log.info("文件上传成功,OSS路径:{}", result.getData());
+                return result;
+            } else {
+                log.error("文件上传失败:{}", result.getMsg());
+                return R.fail("文件上传失败:" + result.getMsg());
+            }
+        } catch (Exception e) {
+            log.error("文件上传异常:{}", e.getMessage(), e);
+            return R.fail("文件上传异常:" + e.getMessage());
+        }
+    }
+}

+ 59 - 57
alien-store/src/main/java/shop/alien/store/controller/DiningServiceController.java

@@ -9,22 +9,19 @@ import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreOrder;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.ChangeTableDTO;
 import shop.alien.entity.store.vo.*;
-import shop.alien.store.feign.DiningServiceFeign;
+import shop.alien.store.service.StoreOrderService;
+import shop.alien.store.util.TokenUtil;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 
 /**
- * 点餐服务 Controller
- * 供前端调用 alien-dining 模块的接口
- *
- * @author ssk
- * @version 1.0
- * @date 2025/01/XX
+ * APP 点餐聚合接口(原通过 Feign 调用 alien-dining,现与本模块 controller/service 同目录)。
  */
 @Slf4j
 @Api(tags = {"点餐服务接口"})
@@ -35,11 +32,13 @@ import java.util.List;
 @RequiredArgsConstructor
 public class DiningServiceController {
 
-    private final DiningServiceFeign diningServiceFeign;
+    private final DiningController diningController;
+    private final StoreOrderController storeOrderController;
+    private final StoreInfoController storeInfoController;
+    private final DiningUserController diningUserController;
+    private final DiningFileUploadController diningFileUploadController;
+    private final StoreOrderService storeOrderService;
 
-    /**
-     * 从请求头获取 Authorization token
-     */
     private String getAuthorization(HttpServletRequest request) {
         String authorization = request.getHeader("Authorization");
         if (authorization == null || authorization.isEmpty()) {
@@ -48,8 +47,6 @@ public class DiningServiceController {
         return authorization;
     }
 
-    // ==================== 点餐相关接口 ====================
-
     @ApiOperation(value = "查询餐桌是否处于就餐中", notes = "免登录可调用,返回 inDining、dinerCount,供前端判断是否跳过选择用餐人数")
     @ApiOperationSupport(order = 0)
     @GetMapping("/table-dining-status")
@@ -57,7 +54,7 @@ public class DiningServiceController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
             log.info("查询餐桌就餐状态: tableId={}", tableId);
-            return diningServiceFeign.getTableDiningStatus(tableId);
+            return diningController.getTableDiningStatus(tableId);
         } catch (Exception e) {
             log.error("查询餐桌就餐状态失败: {}", e.getMessage(), e);
             return R.fail("查询餐桌就餐状态失败: " + e.getMessage());
@@ -72,9 +69,9 @@ public class DiningServiceController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "就餐人数(首客必传;餐桌已就餐中时可省略)") @RequestParam(required = false) Integer dinerCount) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取点餐页面信息: tableId={}, dinerCount={}", tableId, dinerCount);
-            return diningServiceFeign.getDiningPageInfo(authorization, tableId, dinerCount);
+            return diningController.getDiningPageInfo(tableId, dinerCount);
         } catch (Exception e) {
             log.error("获取点餐页面信息失败: {}", e.getMessage(), e);
             return R.fail("获取点餐页面信息失败: " + e.getMessage());
@@ -90,9 +87,9 @@ public class DiningServiceController {
             @ApiParam(value = "搜索关键词", required = true) @RequestParam String keyword,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("搜索菜品: storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
-            return diningServiceFeign.searchCuisines(authorization, storeId, keyword, tableId);
+            return diningController.searchCuisines(storeId, keyword, tableId);
         } catch (Exception e) {
             log.error("搜索菜品失败: {}", e.getMessage(), e);
             return R.fail("搜索菜品失败: " + e.getMessage());
@@ -107,13 +104,13 @@ public class DiningServiceController {
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
             @ApiParam(value = "分类ID(可为空,查询所有)") @RequestParam(required = false) Integer categoryId,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
-            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Integer page,
-            @ApiParam(value = "每页数量", defaultValue = "12") @RequestParam(defaultValue = "12") Integer size) {
+            @ApiParam(value = "页码", defaultValue = "1") @RequestParam(defaultValue = "1") Integer pageParam,
+            @ApiParam(value = "每页数量", defaultValue = "12") @RequestParam(defaultValue = "12") Integer sizeParam) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("根据分类获取菜品列表: storeId={}, categoryId={}, tableId={}, page={}, size={}",
-                    storeId, categoryId, tableId, page, size);
-            return diningServiceFeign.getCuisinesByCategory(authorization, storeId, categoryId, tableId, page, size);
+                    storeId, categoryId, tableId, pageParam, sizeParam);
+            return diningController.getCuisinesByCategory(storeId, categoryId, tableId, pageParam, sizeParam);
         } catch (Exception e) {
             log.error("根据分类获取菜品列表失败: {}", e.getMessage(), e);
             return R.fail("根据分类获取菜品列表失败: " + e.getMessage());
@@ -128,9 +125,9 @@ public class DiningServiceController {
             @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取菜品详情: cuisineId={}, tableId={}", cuisineId, tableId);
-            return diningServiceFeign.getCuisineDetail(authorization, cuisineId, tableId);
+            return diningController.getCuisineDetail(cuisineId, tableId);
         } catch (Exception e) {
             log.error("获取菜品详情失败: {}", e.getMessage(), e);
             return R.fail("获取菜品详情失败: " + e.getMessage());
@@ -144,17 +141,15 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取可领取的优惠券列表: storeId={}", storeId);
-            return diningServiceFeign.getAvailableCoupons(authorization, storeId);
+            return diningController.getAvailableCoupons(storeId);
         } catch (Exception e) {
             log.error("获取可领取的优惠券列表失败: {}", e.getMessage(), e);
             return R.fail("获取可领取的优惠券列表失败: " + e.getMessage());
         }
     }
 
-    // ==================== 订单相关接口 ====================
-
     @ApiOperation(value = "获取购物车", notes = "根据桌号ID获取购物车信息")
     @ApiOperationSupport(order = 6)
     @GetMapping("/order/cart/{tableId}")
@@ -162,9 +157,9 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取购物车: tableId={}", tableId);
-            return diningServiceFeign.getCart(authorization, tableId);
+            return storeOrderController.getCart(tableId);
         } catch (Exception e) {
             log.error("获取购物车失败: {}", e.getMessage(), e);
             return R.fail("获取购物车失败: " + e.getMessage());
@@ -178,9 +173,9 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "换桌参数(原桌号ID、目标桌号ID、换桌原因)", required = true) @RequestBody ChangeTableDTO dto) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("换桌: fromTableId={}, toTableId={}, changeReason={}", dto.getFromTableId(), dto.getToTableId(), dto.getChangeReason());
-            return diningServiceFeign.changeTable(authorization, dto);
+            return storeOrderController.changeTable(dto);
         } catch (Exception e) {
             log.error("换桌失败: {}", e.getMessage(), e);
             return R.fail("换桌失败: " + e.getMessage());
@@ -194,9 +189,9 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取订单详情: orderId={}", orderId);
-            return diningServiceFeign.getOrderDetail(authorization, orderId);
+            return storeOrderController.getOrderDetail(orderId);
         } catch (Exception e) {
             log.error("获取订单详情失败: {}", e.getMessage(), e);
             return R.fail("获取订单详情失败: " + e.getMessage());
@@ -212,9 +207,15 @@ public class DiningServiceController {
             @ApiParam(value = "每页数量", defaultValue = "10") @RequestParam(defaultValue = "10") Integer size,
             @ApiParam(value = "订单状态(可选)") @RequestParam(required = false) Integer status) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取订单列表: page={}, size={}, status={}", page, size, status);
-            return diningServiceFeign.getOrderList(authorization, page, size, status);
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            Page<StoreOrder> p = new Page<>(page, size);
+            IPage<StoreOrderPageVO> data = storeOrderService.getOrderPageWithCuisines(p, null, null, status, null);
+            return R.data(data);
         } catch (Exception e) {
             log.error("获取订单列表失败: {}", e.getMessage(), e);
             return R.fail("获取订单列表失败: " + e.getMessage());
@@ -233,12 +234,10 @@ public class DiningServiceController {
             @ApiParam(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)") @RequestParam(required = false) Integer orderStatus,
             @ApiParam(value = "搜索关键词(订单编号或菜品名称,限15字)") @RequestParam(required = false) String keyword) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("分页查询订单列表: current={}, size={}, storeId={}, tableId={}, orderStatus={}, keyword={}",
                     current, size, storeId, tableId, orderStatus, keyword);
-            R<Page<StoreOrderPageVO>> result = diningServiceFeign.getOrderPage(authorization, current, size, storeId, tableId, orderStatus, keyword);
-            // 将 Page 转换为 IPage 返回(Page 实现了 IPage 接口,可以直接返回)
-            return R.data(result.getData());
+            return storeOrderController.getOrderPage(current, size, storeId, tableId, orderStatus, keyword);
         } catch (Exception e) {
             log.error("分页查询订单列表失败: {}", e.getMessage(), e);
             return R.fail("分页查询订单列表失败: " + e.getMessage());
@@ -252,9 +251,9 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("查询订单变更记录: orderId={}", orderId);
-            return diningServiceFeign.getOrderChangeLogs(authorization, orderId);
+            return storeOrderController.getOrderChangeLogs(orderId);
         } catch (Exception e) {
             log.error("查询订单变更记录失败: {}", e.getMessage(), e);
             return R.fail("查询订单变更记录失败: " + e.getMessage());
@@ -268,33 +267,38 @@ public class DiningServiceController {
             HttpServletRequest request,
             @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("商家手动完成订单: orderId={}", orderId);
-            return diningServiceFeign.completeOrderByMerchant(authorization, orderId);
+            return storeOrderController.completeOrderByMerchant(orderId);
         } catch (Exception e) {
             log.error("商家手动完成订单失败: {}", e.getMessage(), e);
             return R.fail("商家手动完成订单失败: " + e.getMessage());
         }
     }
 
-    // ==================== 用户相关接口 ====================
-
     @ApiOperation(value = "获取用户信息", notes = "获取当前登录用户的详细信息(从token中获取用户ID)")
     @ApiOperationSupport(order = 9)
     @GetMapping("/user/info")
     public R<Object> getUserInfo(HttpServletRequest request) {
         try {
-            String authorization = getAuthorization(request);
+            getAuthorization(request);
             log.info("获取用户信息");
-            return diningServiceFeign.getUserInfo(authorization);
+            R<?> inner = diningUserController.getUserInfo();
+            if (inner == null) {
+                return R.fail("获取用户信息失败");
+            }
+            if (!inner.isSuccess()) {
+                return R.fail(inner.getMsg());
+            }
+            @SuppressWarnings("unchecked")
+            R<Object> out = (R<Object>) (R<?>) inner;
+            return out;
         } catch (Exception e) {
             log.error("获取用户信息失败: {}", e.getMessage(), e);
             return R.fail("获取用户信息失败: " + e.getMessage());
         }
     }
 
-    // ==================== 门店信息接口 ====================
-
     @ApiOperation(value = "根据门店ID查询桌号列表", notes = "查询指定门店下的所有桌号")
     @ApiOperationSupport(order = 10)
     @GetMapping("/store/tables")
@@ -302,7 +306,7 @@ public class DiningServiceController {
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
         try {
             log.info("根据门店ID查询桌号列表: storeId={}", storeId);
-            return diningServiceFeign.getTablesByStoreId(storeId);
+            return storeInfoController.getTablesByStoreId(storeId);
         } catch (Exception e) {
             log.error("根据门店ID查询桌号列表失败: {}", e.getMessage(), e);
             return R.fail("根据门店ID查询桌号列表失败: " + e.getMessage());
@@ -317,7 +321,7 @@ public class DiningServiceController {
             @ApiParam(value = "菜品名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword) {
         try {
             log.info("根据门店ID查询菜品种类及菜品: storeId={}, keyword={}", storeId, keyword);
-            return diningServiceFeign.getCategoriesWithCuisinesByStoreId(storeId, keyword);
+            return storeInfoController.getCategoriesWithCuisinesByStoreId(storeId, keyword);
         } catch (Exception e) {
             log.error("查询菜品种类及菜品失败: {}", e.getMessage(), e);
             return R.fail("查询菜品种类及菜品失败: " + e.getMessage());
@@ -331,15 +335,13 @@ public class DiningServiceController {
             @ApiParam(value = "菜品种类ID", required = true) @PathVariable Integer categoryId) {
         try {
             log.info("删除菜品种类: categoryId={}", categoryId);
-            return diningServiceFeign.deleteCategory(categoryId);
+            return storeInfoController.deleteCategory(categoryId);
         } catch (Exception e) {
             log.error("删除菜品种类失败: {}", e.getMessage(), e);
             return R.fail("删除菜品种类失败: " + e.getMessage());
         }
     }
 
-    // ==================== 文件上传接口 ====================
-
     @ApiOperation(value = "上传图片到OSS", notes = "支持图片、视频或PDF文件上传,返回OSS文件路径")
     @ApiOperationSupport(order = 11)
     @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@@ -350,7 +352,7 @@ public class DiningServiceController {
                 return R.fail("文件不能为空");
             }
             log.info("上传文件: fileName={}, size={}", file.getOriginalFilename(), file.getSize());
-            return diningServiceFeign.uploadFile(file);
+            return diningFileUploadController.upload(file);
         } catch (Exception e) {
             log.error("上传文件失败: {}", e.getMessage(), e);
             return R.fail("上传文件失败: " + e.getMessage());

+ 188 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningUserController.java

@@ -0,0 +1,188 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.dto.ChangePhoneDto;
+import shop.alien.store.dto.UserProfileUpdateDto;
+import shop.alien.store.dto.VerifyTokenDto;
+import shop.alien.store.dto.WeChatLoginDto;
+import shop.alien.store.service.DiningUserService;
+import shop.alien.store.util.TokenUtil;
+import shop.alien.store.vo.DiningUserVo;
+import shop.alien.store.vo.TokenVerifyVo;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+
+/**
+ * 点餐用户控制器
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Api(tags = {"微信点餐-登录(用户端)"})
+@ApiSort(2)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/user")
+@RequiredArgsConstructor
+public class DiningUserController {
+
+    private final DiningUserService diningUserService;
+
+    @ApiOperation(value = "微信小程序登录", notes = "标准流程:1. 前端调用 wx.login() 获取 code;2. 后端调用 jscode2session 获取 openid;3. 可选:通过 wx.getPhoneNumber() 获取 phoneCode 绑定手机号")
+    @PostMapping("/wechatLogin")
+    public R<DiningUserVo> wechatLogin(@Valid @RequestBody WeChatLoginDto loginDto, HttpServletRequest request) {
+        log.info("微信小程序登录: code={}, phoneCode={}",
+                loginDto.getCode(),
+                loginDto.getPhoneCode());
+
+        String macIp = getClientIp(request);
+        DiningUserVo userVo = diningUserService.wechatLogin(
+                loginDto.getCode(),
+                loginDto.getPhoneCode(),
+                macIp);
+        if (userVo == null) {
+            return R.fail("登录失败,请检查 code 是否有效(code 有效期5分钟,仅能使用一次)");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "获取用户信息", notes = "获取当前登录用户的详细信息(从token中获取用户ID)")
+    @GetMapping("/getUserInfo")
+    public R<DiningUserVo> getUserInfo() {
+        // 从 token 获取当前用户ID
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId == null) {
+            return R.fail("用户未登录");
+        }
+
+        log.info("获取用户信息: userId={}", userId);
+
+        DiningUserVo userVo = diningUserService.getUserInfo(userId.longValue());
+        if (userVo == null) {
+            return R.fail("用户不存在或状态异常");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "更新用户个人信息", notes = "登录后完善资料,支持更新昵称、头像、性别、生日等(从token中获取用户ID)")
+    @PostMapping("/updateProfile")
+    public R<DiningUserVo> updateProfile(@Valid @RequestBody UserProfileUpdateDto dto) {
+        // 从 token 获取当前用户ID
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId == null) {
+            return R.fail("用户未登录");
+        }
+
+        // 使用 token 中的用户ID,忽略 DTO 中的 userId(防止用户修改其他用户的信息)
+        dto.setUserId(userId.longValue());
+        log.info("更新用户个人信息: userId={}", userId);
+
+        DiningUserVo userVo = diningUserService.updateProfile(dto);
+        if (userVo == null) {
+            return R.fail("更新失败,用户不存在");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "更换手机号", notes = "校验验证码后更新 user_phone,成功后需重新登录(从token中获取用户ID)")
+    @PostMapping("/changePhone")
+    public R<DiningUserVo> changePhone(@Valid @RequestBody ChangePhoneDto dto) {
+        // 从 token 获取当前用户ID
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId == null) {
+            return R.fail("用户未登录");
+        }
+
+        // 使用 token 中的用户ID,忽略 DTO 中的 userId(防止用户修改其他用户的手机号)
+        dto.setUserId(userId.longValue());
+        log.info("更换手机号: userId={}, newPhone={}", userId, dto.getNewPhone());
+
+        DiningUserVo userVo = diningUserService.changePhone(dto);
+        if (userVo == null) {
+            return R.fail("更换失败,请检查验证码或用户状态");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "校验Token", notes = "验证Token是否有效,支持从请求头Authorization或请求体获取Token")
+    @PostMapping("/verifyToken")
+    public R<TokenVerifyVo> verifyToken(@RequestBody(required = false) VerifyTokenDto dto, HttpServletRequest request) {
+        // 优先从请求体获取 token,如果没有则从请求头获取
+        String token = null;
+        if (dto != null && StringUtils.isNotBlank(dto.getToken())) {
+            token = dto.getToken();
+        } else {
+            // 从请求头获取
+            token = request.getHeader("Authorization");
+        }
+
+        if (StringUtils.isBlank(token)) {
+            return R.fail("Token 不能为空,请通过请求头 Authorization 或请求体传递");
+        }
+
+        log.info("校验Token: token={}", token.length() > 20 ? token.substring(0, 20) + "****" : token);
+
+        TokenVerifyVo verifyVo = diningUserService.verifyToken(token);
+        if (verifyVo == null) {
+            return R.fail("Token 校验失败");
+        }
+
+        if (verifyVo.getValid()) {
+            return R.data(verifyVo);
+        } else {
+            return R.fail(verifyVo.getReason() != null ? verifyVo.getReason() : "Token 无效");
+        }
+    }
+
+    @ApiOperation(value = "解析Token(调试用)", notes = "解析Token内容,用于调试,返回Token中的所有用户信息")
+    @PostMapping("/parseToken")
+    public R<com.alibaba.fastjson.JSONObject> parseToken(@RequestBody(required = false) VerifyTokenDto dto, HttpServletRequest request) {
+        // 优先从请求体获取 token,如果没有则从请求头获取
+        String token = null;
+        if (dto != null && StringUtils.isNotBlank(dto.getToken())) {
+            token = dto.getToken();
+        } else {
+            // 从请求头获取
+            token = request.getHeader("Authorization");
+        }
+
+        if (StringUtils.isBlank(token)) {
+            return R.fail("Token 不能为空");
+        }
+
+        log.info("DiningUserController.parseToken?token={}", token.length() > 20 ? token.substring(0, 20) + "****" : token);
+        com.alibaba.fastjson.JSONObject userInfo = shop.alien.store.util.TokenUtil.parseToken(token);
+        if (userInfo == null) {
+            return R.fail("Token 解析失败,请查看日志获取详细信息");
+        }
+
+        return R.data(userInfo);
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        return ip;
+    }
+}

+ 108 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreInfoDto;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
 import shop.alien.entity.store.vo.*;
 import shop.alien.entity.storePlatform.StoreLicenseHistory;
 import shop.alien.mapper.*;
@@ -82,6 +83,9 @@ public class StoreInfoController {
     private final PerformanceListService performanceListService;
     private final SportsEquipmentFacilityService sportsEquipmentFacilityService;
 
+    /** 点餐(扫码)侧门店桌台/菜品分类查询,见 {@link ScanOrderStoreInfoService} */
+    private final ScanOrderStoreInfoService diningStoreInfoQueryService;
+
     @ApiOperation("获取所有门店")
     @ApiOperationSupport(order = 1)
     @GetMapping("/getAll")
@@ -1928,6 +1932,110 @@ public class StoreInfoController {
         }
     }
 
+    // —— 以下为原 alien-dining 迁移的扫码/点餐门店查询接口(统一挂到本 Controller,路径仍为 /store/info) ——
+
+    @ApiOperation(value = "根据门店ID查询桌号列表", notes = "查询指定门店下的所有桌号(点餐)")
+    @ApiOperationSupport(order = 900)
+    @GetMapping("/tables")
+    public R<List<StoreTable>> getTablesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        log.info("StoreInfoController.getTablesByStoreId?storeId={}", storeId);
+        try {
+            if (storeId == null) {
+                return R.fail("门店ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.getTablesByStoreId(storeId));
+        } catch (Exception e) {
+            log.error("查询桌号列表失败: {}", e.getMessage(), e);
+            return R.fail("查询桌号列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据门店ID查询菜品种类列表", notes = "查询指定门店下的所有菜品种类(点餐)")
+    @ApiOperationSupport(order = 901)
+    @GetMapping("/categories")
+    public R<List<StoreCuisineCategory>> getCategoriesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        log.info("StoreInfoController.getCategoriesByStoreId?storeId={}", storeId);
+        try {
+            if (storeId == null) {
+                return R.fail("门店ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.getCategoriesByStoreId(storeId));
+        } catch (Exception e) {
+            log.error("查询菜品种类列表失败: {}", e.getMessage(), e);
+            return R.fail("查询菜品种类列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据门店ID查询菜品种类及各类别下菜品", notes = "点餐;可选 keyword 按菜品名称模糊查询")
+    @ApiOperationSupport(order = 902)
+    @GetMapping("/categories-with-cuisines")
+    public R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "菜品名称模糊查询关键词(可选)") @RequestParam(required = false) String keyword) {
+        log.info("StoreInfoController.getCategoriesWithCuisinesByStoreId?storeId={}, keyword={}", storeId, keyword);
+        try {
+            if (storeId == null) {
+                return R.fail("门店ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.getCategoriesWithCuisinesByStoreId(storeId, keyword));
+        } catch (Exception e) {
+            log.error("查询菜品种类及菜品失败: {}", e.getMessage(), e);
+            return R.fail("查询菜品种类及菜品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "删除菜品种类", notes = "点餐场景;仅删除绑定关系和菜品种类(逻辑删除)")
+    @ApiOperationSupport(order = 903)
+    @DeleteMapping("/category/{categoryId}")
+    public R<Boolean> deleteCategory(
+            @ApiParam(value = "菜品种类ID", required = true) @PathVariable Integer categoryId) {
+        log.info("StoreInfoController.deleteCategory?categoryId={}", categoryId);
+        try {
+            if (categoryId == null) {
+                return R.fail("菜品种类ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.deleteCategory(categoryId));
+        } catch (Exception e) {
+            log.error("删除菜品种类失败: {}", e.getMessage(), e);
+            return R.fail("删除菜品种类失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据菜品种类ID查询菜品信息列表", notes = "点餐")
+    @ApiOperationSupport(order = 904)
+    @GetMapping("/cuisines")
+    public R<List<StoreCuisine>> getCuisinesByCategoryId(
+            @ApiParam(value = "菜品种类ID", required = true) @RequestParam Integer categoryId) {
+        log.info("StoreInfoController.getCuisinesByCategoryId?categoryId={}", categoryId);
+        try {
+            if (categoryId == null) {
+                return R.fail("菜品种类ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.getCuisinesByCategoryId(categoryId));
+        } catch (Exception e) {
+            log.error("查询菜品信息列表失败: {}", e.getMessage(), e);
+            return R.fail("查询菜品信息列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据商铺ID查询店铺信息和首页展示美食价目表", notes = "点餐")
+    @ApiOperationSupport(order = 905)
+    @GetMapping("/detail/{storeId}")
+    public R<StoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(
+            @ApiParam(value = "商铺ID", required = true) @PathVariable Integer storeId) {
+        log.info("StoreInfoController.getStoreInfoWithHomepageCuisines?storeId={}", storeId);
+        try {
+            if (storeId == null) {
+                return R.fail("商铺ID不能为空");
+            }
+            return R.data(diningStoreInfoQueryService.getStoreInfoWithHomepageCuisines(storeId));
+        } catch (Exception e) {
+            log.error("查询店铺信息和首页展示美食价目表失败: {}", e.getMessage(), e);
+            return R.fail("查询店铺信息和首页展示美食价目表失败: " + e.getMessage());
+        }
+    }
 
     /**
      * 店铺信息(包含商户头像)

+ 578 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java

@@ -0,0 +1,578 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.SseService;
+import shop.alien.store.service.StoreOrderService;
+import shop.alien.store.util.TokenUtil;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.ChangeTableDTO;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.OrderInfoVO;
+import shop.alien.entity.store.vo.OrderDetailWithChangeLogVO;
+import shop.alien.entity.store.vo.StoreOrderPageVO;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreOrderDetailMapper;
+import shop.alien.mapper.StoreTableMapper;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 订单管理控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"小程序-订单管理"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/order")
+@RequiredArgsConstructor
+public class StoreOrderController {
+
+    private final StoreOrderService orderService;
+    private final CartService cartService;
+    private final SseService sseService;
+    private final StoreOrderDetailMapper orderDetailMapper;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @ApiOperation(value = "获取购物车", notes = "根据桌号ID获取购物车信息")
+    @GetMapping("/cart/{tableId}")
+    public R<CartDTO> getCart(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        log.info("StoreOrderController.getCart?tableId={}", tableId);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            CartDTO cart = cartService.getCart(tableId);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("获取购物车失败: {}", e.getMessage(), e);
+            return R.fail("获取购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "添加商品到购物车", notes = "添加商品到购物车,并推送SSE和WebSocket消息")
+    @PostMapping("/cart/add")
+    public R<CartDTO> addCartItem(@Valid @RequestBody AddCartItemDTO dto) {
+        log.info("StoreOrderController.addCartItem?dto={}", dto);
+        try {
+            // 验证 token(用户信息在 CartService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            CartDTO cart = cartService.addItem(dto);
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(dto.getTableId(), cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(dto.getTableId(), cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("添加商品到购物车失败: {}", e.getMessage(), e);
+            return R.fail("添加商品到购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "更新购物车商品数量", notes = "更新购物车中商品的数量,并推送SSE和WebSocket消息")
+    @PutMapping("/cart/update")
+    public R<CartDTO> updateCartItem(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId,
+            @ApiParam(value = "数量", required = true) @RequestParam Integer quantity) {
+        log.info("StoreOrderController.updateCartItem?tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("更新购物车商品数量失败: {}", e.getMessage(), e);
+            return R.fail("更新购物车商品数量失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "删除购物车商品", notes = "从购物车中删除商品,并推送SSE和WebSocket消息")
+    @DeleteMapping("/cart/remove")
+    public R<CartDTO> removeCartItem(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId) {
+        log.info("StoreOrderController.removeCartItem?tableId={}, cuisineId={}", tableId, cuisineId);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("删除购物车商品失败: {}", e.getMessage(), e);
+            return R.fail("删除购物车商品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "清空购物车", notes = "清空购物车中所有商品(保留餐具和已下单商品),并推送SSE和WebSocket消息")
+    @DeleteMapping("/cart/clear")
+    public R<CartDTO> clearCart(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        log.info("StoreOrderController.clearCart?tableId={}", tableId);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            
+            // 清空购物车(会自动保留已下单的商品和餐具)
+            cartService.clearCart(tableId);
+            
+            // 获取清空后的购物车(包含保留的餐具和已下单商品)
+            CartDTO cart = cartService.getCart(tableId);
+            
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
+            
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("清空购物车失败: {}", e.getMessage(), e);
+            return R.fail("清空购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "设置用餐人数", notes = "设置用餐人数,自动添加或更新餐具到购物车")
+    @PostMapping("/cart/set-diner-count")
+    public R<CartDTO> setDinerCount(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "用餐人数", required = true) @RequestParam Integer dinerCount) {
+        log.info("StoreOrderController.setDinerCount?tableId={}, dinerCount={}", tableId, dinerCount);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            if (dinerCount == null || dinerCount <= 0) {
+                return R.fail("用餐人数必须大于0");
+            }
+            CartDTO cart = cartService.setDinerCount(tableId, dinerCount);
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("设置用餐人数失败: {}", e.getMessage(), e);
+            return R.fail("设置用餐人数失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "更新餐具数量", notes = "更新购物车中餐具的数量,并推送SSE和WebSocket消息。餐具数量为0或未传时不往购物车加餐具(0会移除已有餐具项)")
+    @PutMapping("/cart/update-tableware")
+    public R<CartDTO> updateTablewareQuantity(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "餐具数量,0或未传则不往购物车加餐具") @RequestParam(required = false) Integer quantity) {
+        log.info("StoreOrderController.updateTablewareQuantity?tableId={}, quantity={}", tableId, quantity);
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            if (quantity != null && quantity < 0) {
+                return R.fail("餐具数量不能小于0");
+            }
+            // 餐具数量为 0 或 null 时不往购物车加餐具:0 则移除已有餐具项,null 则直接返回当前购物车
+            if (quantity == null) {
+                CartDTO cart = cartService.getCart(tableId);
+                sseService.pushCartUpdate(tableId, cart);
+                return R.data(cart);
+            }
+            if (quantity == 0) {
+                CartDTO cart = cartService.updateTablewareQuantity(tableId, 0);
+                sseService.pushCartUpdate(tableId, cart);
+                return R.data(cart);
+            }
+            CartDTO cart = cartService.updateTablewareQuantity(tableId, quantity);
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("更新餐具数量失败: {}", e.getMessage(), e);
+            return R.fail("更新餐具数量失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "创建订单(下单)", notes = "从购物车创建订单,不立即支付。备注:创建/更新时传入,更新订单(加餐)时为覆盖")
+    @PostMapping("/create")
+    public R<shop.alien.entity.store.vo.OrderSuccessVO> createOrder(@Valid @RequestBody CreateOrderDTO dto) {
+        log.info("StoreOrderController.createOrder?dto={}", dto);
+        try {
+            // 验证 token(用户信息在 StoreOrderService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            // 限制备注长度
+            if (dto.getRemark() != null && dto.getRemark().length() > 30) {
+                dto.setRemark(dto.getRemark().substring(0, 30));
+            }
+
+            // 设置不立即支付
+            dto.setImmediatePay(0);
+            StoreOrder order = orderService.createOrder(dto);
+
+            // 转换为OrderSuccessVO
+            shop.alien.entity.store.vo.OrderSuccessVO vo = new shop.alien.entity.store.vo.OrderSuccessVO();
+            vo.setOrderId(order.getId());
+            vo.setOrderNo(order.getOrderNo());
+            vo.setTableNumber(order.getTableNumber());
+            vo.setDinerCount(order.getDinerCount());
+            vo.setOrderStatus(order.getOrderStatus());
+
+            // 查询门店信息
+            StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+            vo.setStoreName(storeInfo != null ? storeInfo.getStoreName() : null);
+
+            // 推送订单创建消息
+            sseService.pushCartUpdate(dto.getTableId(), order);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("创建订单失败: {}", e.getMessage(), e);
+            return R.fail("创建订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "支付订单", notes = "支付订单,支付成功后订单状态变为已支付(1),需要单独调用完成订单接口将订单状态改为已完成(3)")
+    @PostMapping("/pay/{orderId}")
+    public R<Object> payOrder(
+            @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId,
+            @ApiParam(value = "支付方式(1:微信, 2:支付宝, 3:现金)", required = true) @RequestParam Integer payType) {
+        log.info("StoreOrderController.payOrder?orderId={}, payType={}", orderId, payType);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            
+            StoreOrder order = orderService.getOrderById(orderId);
+            if (order == null) {
+                return R.fail("订单不存在");
+            }
+
+            if (order.getOrderStatus() != 0) {
+                return R.fail("订单状态不正确,无法支付");
+            }
+
+            // 如果是微信支付,调用微信支付接口
+            if (payType == 1) {
+                // TODO: 调用微信支付接口,返回支付参数
+                // 这里需要集成微信支付SDK,返回支付参数给前端拉起支付
+                // 支付成功后通过回调接口更新订单状态
+                // 暂时先更新订单状态为已支付(实际应该等支付回调)
+                order = orderService.payOrder(orderId, payType);
+                return R.data(order);
+            } else {
+                // 其他支付方式(支付宝、现金)直接更新为已支付
+                order = orderService.payOrder(orderId, payType);
+                return R.data(order);
+            }
+        } catch (Exception e) {
+            log.error("支付订单失败: {}", e.getMessage(), e);
+            return R.fail("支付订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "取消订单", notes = "取消订单")
+    @PostMapping("/cancel/{orderId}")
+    public R<Boolean> cancelOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.cancelOrder?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = orderService.cancelOrder(orderId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("取消订单失败: {}", e.getMessage(), e);
+            return R.fail("取消订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单详情", notes = "根据订单ID查询订单详情,包含订单基本信息和按批次分组的变更记录,用于展示每次下单/加餐都加了什么商品")
+    @GetMapping("/detail/{orderId}")
+    public R<OrderDetailWithChangeLogVO> getOrderDetail(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.getOrderDetail?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            OrderDetailWithChangeLogVO orderDetail = orderService.getOrderDetailWithChangeLog(orderId);
+            return R.data(orderDetail);
+        } catch (Exception e) {
+            log.error("查询订单详情失败: {}", e.getMessage(), e);
+            return R.fail("查询订单详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单明细", notes = "根据订单ID查询订单明细列表")
+    @GetMapping("/detail/list/{orderId}")
+    public R<List<StoreOrderDetail>> getOrderDetailList(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.getOrderDetailList?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StoreOrderDetail> wrapper =
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+            wrapper.eq(StoreOrderDetail::getOrderId, orderId);
+            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+            wrapper.orderByDesc(StoreOrderDetail::getCreatedTime);
+            List<StoreOrderDetail> details = orderDetailMapper.selectList(wrapper);
+            return R.data(details);
+        } catch (Exception e) {
+            log.error("查询订单明细失败: {}", e.getMessage(), e);
+            return R.fail("查询订单明细失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单信息", notes = "根据订单ID查询订单完整信息(包含订单基本信息、菜品清单、价格明细)")
+    @GetMapping("/info/{orderId}")
+    public R<OrderInfoVO> getOrderInfo(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.getOrderInfo?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            OrderInfoVO orderInfo = orderService.getOrderInfo(orderId);
+            return R.data(orderInfo);
+        } catch (Exception e) {
+            log.error("查询订单信息失败: {}", e.getMessage(), e);
+            return R.fail("查询订单信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单变更记录", notes = "根据订单ID查询订单的所有变更记录(按批次分组),用于展示每次下单/加餐都加了什么商品")
+    @GetMapping("/change-log/{orderId}")
+    public R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.getOrderChangeLogs?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            List<OrderChangeLogBatchVO> changeLogs = orderService.getOrderChangeLogs(orderId);
+            return R.data(changeLogs);
+        } catch (Exception e) {
+            log.error("查询订单变更记录失败: {}", e.getMessage(), e);
+            return R.fail("查询订单变更记录失败: " + e.getMessage());
+        }
+    }
+
+
+    @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表,包含订单中的菜品数量、菜品名称、菜品图片。支持按订单编号或菜品名称搜索(限15字)")
+    @GetMapping("/page")
+    public R<IPage<StoreOrderPageVO>> getOrderPage(
+            @ApiParam(value = "页码", required = true) @RequestParam(defaultValue = "1") Long current,
+            @ApiParam(value = "每页数量", required = true) @RequestParam(defaultValue = "10") Long size,
+            @ApiParam(value = "门店ID") @RequestParam(required = false) Integer storeId,
+            @ApiParam(value = "桌号ID") @RequestParam(required = false) Integer tableId,
+            @ApiParam(value = "订单状态:0=进行中(待支付+已支付),3=已完成") @RequestParam(required = false) Integer orderStatus,
+            @ApiParam(value = "搜索关键词(订单编号或菜品名称,限15字)") @RequestParam(required = false) String keyword) {
+        log.info("StoreOrderController.getOrderPage?current={}, size={}, storeId={}, tableId={}, orderStatus={}, keyword={}", current, size, storeId, tableId, orderStatus, keyword);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            Page<StoreOrder> page = new Page<>(current, size);
+            IPage<StoreOrderPageVO> result = orderService.getOrderPageWithCuisines(page, storeId, tableId, orderStatus, keyword);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("分页查询订单列表失败: {}", e.getMessage(), e);
+            return R.fail("分页查询订单列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询我的订单", notes = "通过标识查询我的未支付订单或历史订单,包含订单中的菜品数量、菜品名称、菜品图片。type: 0或unpaid-未支付订单, 1或history-历史订单")
+    @GetMapping("/my-orders")
+    public R<IPage<StoreOrderPageVO>> getMyOrders(
+            @ApiParam(value = "页码", required = true) @RequestParam(defaultValue = "1") Long current,
+            @ApiParam(value = "每页数量", required = true) @RequestParam(defaultValue = "10") Long size,
+            @ApiParam(value = "订单类型(0或unpaid:未支付订单, 1或history:历史订单)", required = true) @RequestParam String type) {
+        log.info("StoreOrderController.getMyOrders?current={}, size={}, type={}", current, size, type);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            Page<StoreOrder> page = new Page<>(current, size);
+            IPage<StoreOrderPageVO> result = orderService.getMyOrdersWithCuisines(page, type);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("查询我的订单失败: {}", e.getMessage(), e);
+            return R.fail("查询我的订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "换桌", notes = "换桌并迁移购物车、未完成的订单以及其他关联表数据")
+    @PostMapping("/change-table")
+    public R<CartDTO> changeTable(@Valid @RequestBody ChangeTableDTO dto) {
+        log.info("StoreOrderController.changeTable?dto={}", dto);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+
+            // 调用Service层处理换桌业务逻辑
+            CartDTO cart = orderService.changeTable(dto.getFromTableId(), dto.getToTableId(), dto.getChangeReason(), userId);
+
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("换桌失败: {}", e.getMessage(), e);
+            return R.fail("换桌失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "建立SSE连接", notes = "建立SSE连接,用于接收购物车更新消息")
+    @GetMapping(value = "/sse/{tableId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter createSseConnection(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        // 验证 token
+        if (!TokenUtil.hasValidToken()) {
+            log.warn("建立SSE连接失败: 用户未登录, tableId={}", tableId);
+            return null;
+        }
+        log.info("建立SSE连接, tableId={}", tableId);
+        return sseService.createConnection(tableId);
+    }
+
+    @ApiOperation(value = "完成订单", notes = "完成订单,将已支付状态的订单改为已完成状态。订单状态:0-待支付,1-已支付,3-已完成。只有已支付状态的订单才能完成")
+    @PostMapping("/complete/{orderId}")
+    public R<Boolean> completeOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.completeOrder?orderId={}", orderId);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            boolean result = orderService.completeOrder(orderId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("完成订单失败: {}", e.getMessage(), e);
+            return R.fail("完成订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "商家手动完成订单", notes = "供商家使用,手动点击完成订单。不校验订单是否处于已支付状态,直接将订单状态改为已完成。订单状态:0-待支付,1-已支付,2-已取消,3-已完成")
+    @PostMapping("/complete-by-merchant/{orderId}")
+    public R<Boolean> completeOrderByMerchant(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        log.info("StoreOrderController.completeOrderByMerchant?orderId={}", 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) {
+        log.info("StoreOrderController.updateOrderCoupon?dto={}", dto);
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            StoreOrder order = orderService.updateOrderCoupon(dto.getOrderId(), dto.getCouponId());
+            return R.data(order);
+        } catch (Exception e) {
+            log.error("更新订单优惠券失败: {}", e.getMessage(), e);
+            return R.fail("更新订单优惠券失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "管理员重置餐桌", notes = "管理员重置餐桌:删除购物车数据、未支付/已取消的订单数据,并重置餐桌表初始化。已支付/已完成的订单会被保留,避免数据丢失")
+    @PostMapping("/admin/reset-table/{tableId}")
+    public R<Boolean> resetTable(@ApiParam(value = "餐桌ID", required = true) @PathVariable Integer tableId) {
+        log.info("StoreOrderController.resetTable?tableId={}", tableId);
+        try {
+            // TODO: 这里可以添加管理员权限验证
+            // if (!isAdmin(userId)) {
+            //     return R.fail("无权限执行此操作");
+            // }
+            
+            boolean result = orderService.resetTable(tableId);
+            if (result) {
+                // 推送购物车更新消息(清空购物车)
+                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());
+                }
+                
+                sseService.pushCartUpdate(tableId, emptyCart);
+                return R.data(true);
+            } else {
+                return R.fail("重置餐桌失败");
+            }
+        } catch (Exception e) {
+            log.error("重置餐桌失败: {}", e.getMessage(), e);
+            return R.fail("重置餐桌失败: " + e.getMessage());
+        }
+    }
+}

+ 34 - 0
alien-store/src/main/java/shop/alien/store/dto/ChangePhoneDto.java

@@ -0,0 +1,34 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+/**
+ * 更换手机号请求
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("更换手机号请求")
+public class ChangePhoneDto {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "新手机号", required = true)
+    @NotBlank(message = "新手机号不能为空")
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
+    private String newPhone;
+
+    @ApiModelProperty(value = "验证码", required = true)
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+}

+ 56 - 0
alien-store/src/main/java/shop/alien/store/dto/UserProfileUpdateDto.java

@@ -0,0 +1,56 @@
+package shop.alien.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Date;
+
+/**
+ * 用户个人信息更新DTO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("用户个人信息更新请求")
+public class UserProfileUpdateDto {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "头像URL")
+    private String avatarUrl;
+
+    @ApiModelProperty(value = "性别(男/女)")
+    private String gender;
+
+    @ApiModelProperty(value = "生日", example = "1990-01-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date birthday;
+
+    @ApiModelProperty(value = "真实姓名")
+    private String realName;
+
+    @ApiModelProperty(value = "省份")
+    private String province;
+
+    @ApiModelProperty(value = "城市")
+    private String city;
+
+    @ApiModelProperty(value = "区县")
+    private String district;
+
+    @ApiModelProperty(value = "详细地址")
+    private String address;
+
+    @ApiModelProperty(value = "个人简介")
+    private String jianjie;
+}

+ 20 - 0
alien-store/src/main/java/shop/alien/store/dto/VerifyTokenDto.java

@@ -0,0 +1,20 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * Token 校验请求DTO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("Token校验请求")
+public class VerifyTokenDto {
+
+    @ApiModelProperty(value = "Token(可选,如果不传则从请求头 Authorization 获取)", required = false)
+    private String token;
+}

+ 29 - 0
alien-store/src/main/java/shop/alien/store/dto/WeChatLoginDto.java

@@ -0,0 +1,29 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 微信小程序登录DTO
+ * 标准流程:通过 wx.login() 获取 code,调用 jscode2session 获取 openid
+ * 可选:通过 wx.getPhoneNumber() 获取 phoneCode,用于绑定手机号
+ *
+ * @author ssk
+ * @version 2.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("微信小程序登录请求")
+public class WeChatLoginDto {
+
+    @ApiModelProperty(value = "微信登录凭证 code(通过 wx.login() 获取,必填)", required = true)
+    @NotBlank(message = "code不能为空,请通过 wx.login() 获取")
+    private String code;
+
+    @ApiModelProperty(value = "手机号凭证 code(通过 wx.getPhoneNumber() 获取,可选,用于绑定手机号)", required = false)
+    private String phoneCode;
+
+}

+ 48 - 0
alien-store/src/main/java/shop/alien/store/enums/OrderStatus.java

@@ -0,0 +1,48 @@
+package shop.alien.store.enums;
+
+/**
+ * 订单状态枚举
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+public enum OrderStatus {
+    /**
+     * 待支付
+     */
+    PENDING_PAYMENT(0, "待支付"),
+    /**
+     * 已支付
+     */
+    PAID(1, "已支付"),
+    /**
+     * 制作中
+     */
+    PREPARING(2, "制作中"),
+    /**
+     * 已完成
+     */
+    COMPLETED(3, "已完成"),
+    /**
+     * 已取消
+     */
+    CANCELLED(4, "已取消");
+
+    private final Integer code;
+    private final String desc;
+
+    OrderStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}
+

+ 80 - 0
alien-store/src/main/java/shop/alien/store/feign/AlienStoreFeign.java

@@ -0,0 +1,80 @@
+package shop.alien.store.feign;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+import shop.alien.entity.store.vo.LifeDiscountCouponVo;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 嵌入在 alien-store 内的点餐子包通过 Feign 复用本机优惠券/短信/收藏等能力(与其它微服务部署方式一致时可走注册中心)。
+ */
+@FeignClient(name = "alienStoreFeign", url = "${feign.alienStore.url:}", contextId = "alienStoreFeignForDining")
+public interface AlienStoreFeign {
+
+    @GetMapping("ali/checkSmsCode")
+    R<?> checkSmsCode(
+            @RequestParam("phone") String phone,
+            @RequestParam("appType") Integer appType,
+            @RequestParam("businessType") Integer businessType,
+            @RequestParam("code") Integer code);
+
+    @GetMapping("life-discount-coupon/getUserCouponList")
+    R<List<LifeDiscountCouponVo>> getUserCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("tabType") String tabType,
+            @RequestParam(value = "page", defaultValue = "1") int page,
+            @RequestParam(value = "size", defaultValue = "10") int size,
+            @RequestParam(value = "type", required = false) Integer type,
+            @RequestParam(value = "couponType", required = false) Integer couponType,
+            @RequestParam(value = "storeId", required = false) String storeId,
+            @RequestParam(value = "storeName", required = false) String storeName);
+
+    @GetMapping("life-discount-coupon/getCounponDetailById")
+    R<LifeDiscountCouponVo> getCounponDetailById(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("counponId") String counponId);
+
+    @GetMapping("life-discount-coupon/getStoreUserUsableCouponList")
+    R<Map<String, Object>> getStoreUserUsableCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") String storeId,
+            @RequestParam("amount") BigDecimal amount,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    @GetMapping("life-discount-coupon/getStoreUserCouponList")
+    R<List<LifeDiscountCouponVo>> getStoreUserCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("storeId") String storeId,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    @GetMapping("life-discount-coupon/getStoreAllCouponList")
+    R<Page<LifeDiscountCouponVo>> getStoreAllCouponList(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam(value = "page", defaultValue = "1") int page,
+            @RequestParam(value = "size", defaultValue = "10") int size,
+            @RequestParam("storeId") String storeId,
+            @RequestParam(value = "couponName", required = false) String couponName,
+            @RequestParam("tab") String tab,
+            @RequestParam(value = "couponsFromType", defaultValue = "1") int couponsFromType,
+            @RequestParam(value = "couponStatus", defaultValue = "1", required = false) int couponStatus,
+            @RequestParam(value = "couponType", required = false) Integer couponType);
+
+    @PostMapping("/collect/addCollect")
+    R<Boolean> addCollect(@RequestBody LifeCollect lifeCollect);
+
+    @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    R<String> uploadFile(@RequestPart("file") MultipartFile file);
+}

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

@@ -1,273 +0,0 @@
-package shop.alien.store.feign;
-
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
-import shop.alien.entity.result.R;
-import shop.alien.entity.store.StoreTable;
-import shop.alien.entity.store.dto.CartDTO;
-import shop.alien.entity.store.dto.ChangeTableDTO;
-import shop.alien.entity.store.vo.*;
-
-import java.util.List;
-
-/**
- * 点餐服务 Feign 客户端
- * 用于 alien-store 模块调用 alien-dining 模块的接口
- *
- * @author ssk
- * @version 1.0
- * @date 2025/01/XX
- */
-@FeignClient(name = "alien-dining", url = "${feign.alienDining.url:}")
-public interface DiningServiceFeign {
-
-    // ==================== 点餐相关接口 ====================
-
-    /**
-     * 查询餐桌是否处于就餐中(免登录可调用)
-     *
-     * @param tableId 桌号ID
-     * @return R.data 为 TableDiningStatusVO(inDining、dinerCount)
-     */
-    @GetMapping("/store/dining/table-dining-status")
-    R<TableDiningStatusVO> getTableDiningStatus(@RequestParam("tableId") Integer tableId);
-
-    /**
-     * 获取点餐页面信息
-     *
-     * @param authorization 请求头 Authorization,供 dining 解析当前用户
-     * @param tableId       桌号ID
-     * @param dinerCount    就餐人数(首客必传;餐桌已就餐中时可省略)
-     * @return R.data 为 DiningPageInfoVO
-     */
-    @GetMapping("/store/dining/page-info")
-    R<DiningPageInfoVO> getDiningPageInfo(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam("tableId") Integer tableId,
-            @RequestParam(value = "dinerCount", required = false) Integer dinerCount);
-
-    /**
-     * 搜索菜品
-     *
-     * @param authorization 请求头 Authorization
-     * @param storeId       门店ID
-     * @param keyword       搜索关键词
-     * @param tableId       桌号ID
-     * @return R.data 为 List&lt;CuisineListVO&gt;
-     */
-    @GetMapping("/store/dining/search")
-    R<List<CuisineListVO>> searchCuisines(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam("keyword") String keyword,
-            @RequestParam("tableId") Integer tableId);
-
-    /**
-     * 根据分类获取菜品列表
-     *
-     * @param authorization 请求头 Authorization
-     * @param storeId       门店ID
-     * @param categoryId    分类ID(可为空,查询所有)
-     * @param tableId       桌号ID
-     * @param page          页码(默认1)
-     * @param size          每页数量(默认12)
-     * @return R.data 为 List&lt;CuisineListVO&gt;
-     */
-    @GetMapping("/store/dining/cuisines")
-    R<List<CuisineListVO>> getCuisinesByCategory(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam(value = "categoryId", required = false) Integer categoryId,
-            @RequestParam("tableId") Integer tableId,
-            @RequestParam(value = "page", defaultValue = "1") Integer page,
-            @RequestParam(value = "size", defaultValue = "12") Integer size);
-
-    /**
-     * 获取菜品详情
-     *
-     * @param authorization 请求头 Authorization
-     * @param cuisineId     菜品ID
-     * @param tableId       桌号ID
-     * @return R.data 为 CuisineDetailVO
-     */
-    @GetMapping("/store/dining/cuisine/{cuisineId}")
-    R<CuisineDetailVO> getCuisineDetail(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @PathVariable("cuisineId") Integer cuisineId,
-            @RequestParam("tableId") Integer tableId);
-
-    /**
-     * 获取可领取的优惠券列表
-     *
-     * @param authorization 请求头 Authorization
-     * @param storeId       门店ID
-     * @return R.data 为 List&lt;AvailableCouponVO&gt;
-     */
-    @GetMapping("/store/dining/coupons/available")
-    R<List<AvailableCouponVO>> getAvailableCoupons(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam("storeId") Integer storeId);
-
-    // ==================== 订单相关接口 ====================
-
-    /**
-     * 获取购物车
-     *
-     * @param authorization 请求头 Authorization
-     * @param tableId       桌号ID
-     * @return R.data 为 CartDTO
-     */
-    @GetMapping("/store/order/cart/{tableId}")
-    R<CartDTO> getCart(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @PathVariable("tableId") Integer tableId);
-
-    /**
-     * 换桌(迁移购物车、未完成订单及关联数据,支持点餐未下单场景,无需订单号)
-     *
-     * @param authorization 请求头 Authorization
-     * @param dto            原桌号ID、目标桌号ID、换桌原因
-     * @return R.data 为迁移后的购物车 CartDTO
-     */
-    @PostMapping("/store/order/change-table")
-    R<CartDTO> changeTable(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestBody ChangeTableDTO dto);
-
-    /**
-     * 获取订单详情
-     *
-     * @param authorization 请求头 Authorization
-     * @param orderId       订单ID
-     * @return R.data 为 OrderDetailWithChangeLogVO
-     */
-    @GetMapping("/store/order/detail/{orderId}")
-    R<OrderDetailWithChangeLogVO> getOrderDetail(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @PathVariable("orderId") Integer orderId);
-
-    /**
-     * 获取订单列表(分页)
-     *
-     * @param authorization 请求头 Authorization
-     * @param page          页码
-     * @param size          每页数量
-     * @param status        订单状态(可选)
-     * @return R.data 为 IPage&lt;StoreOrderPageVO&gt;
-     */
-    @GetMapping("/store/order/list")
-    R<IPage<StoreOrderPageVO>> getOrderList(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam(value = "page", defaultValue = "1") Integer page,
-            @RequestParam(value = "size", defaultValue = "10") Integer size,
-            @RequestParam(value = "status", required = false) Integer status);
-
-    /**
-     * 分页查询订单列表
-     * 包含订单中的菜品数量、菜品名称、菜品图片。支持按订单编号或菜品名称搜索(限15字)
-     *
-     * @param authorization 请求头 Authorization,供 dining 解析当前用户
-     * @param current       页码(默认1)
-     * @param size         每页数量(默认10)
-     * @param storeId       门店ID(可选)
-     * @param tableId       桌号ID(可选)
-     * @param orderStatus   订单状态(可选,0:待支付, 1:已支付, 2:已取消, 3:已完成)
-     * @param keyword       搜索关键词(订单编号或菜品名称,限15字)(可选)
-     * @return R.data 为 Page&lt;StoreOrderPageVO&gt;(使用具体的 Page 实现类,避免 IPage 反序列化问题)
-     */
-    @GetMapping("/store/order/page")
-    R<Page<StoreOrderPageVO>> getOrderPage(
-            @RequestHeader(value = "Authorization", required = false) String authorization,
-            @RequestParam(value = "current", defaultValue = "1") Long current,
-            @RequestParam(value = "size", defaultValue = "10") Long size,
-            @RequestParam(value = "storeId", required = false) Integer storeId,
-            @RequestParam(value = "tableId", required = false) Integer tableId,
-            @RequestParam(value = "orderStatus", required = false) Integer orderStatus,
-            @RequestParam(value = "keyword", required = false) String keyword);
-
-    /**
-     * 查询订单变更记录
-     *
-     * @param authorization 请求头 Authorization
-     * @param orderId       订单ID
-     * @return R.data 为 List&lt;OrderChangeLogBatchVO&gt;
-     */
-    @GetMapping("/store/order/change-log/{orderId}")
-    R<List<OrderChangeLogBatchVO>> getOrderChangeLogs(
-            @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);
-
-    // ==================== 用户相关接口 ====================
-
-    /**
-     * 获取用户信息
-     * 注意:返回类型使用 Object,因为 DiningUserVo 在 alien-dining 模块中
-     * 调用方需要自行转换为对应的 VO 对象
-     *
-     * @param authorization 请求头 Authorization
-     * @return R.data 为用户信息对象
-     */
-    @GetMapping("/dining/user/getUserInfo")
-    R<Object> getUserInfo(
-            @RequestHeader(value = "Authorization", required = false) String authorization);
-
-    // ==================== 门店信息接口 ====================
-
-    /**
-     * 根据门店ID查询桌号列表
-     *
-     * @param storeId 门店ID
-     * @return R.data 为 List&lt;StoreTable&gt;
-     */
-    @GetMapping("/store/info/tables")
-    R<List<StoreTable>> getTablesByStoreId(
-            @RequestParam("storeId") Integer storeId);
-
-    /**
-     * 根据门店ID查询菜品种类及各类别下菜品(可选按菜品名称模糊查询)
-     *
-     * @param storeId 门店ID
-     * @param keyword 菜品名称模糊查询关键词(可选)
-     * @return R.data 为 List&lt;CategoryWithCuisinesVO&gt;
-     */
-    @GetMapping("/store/info/categories-with-cuisines")
-    R<List<CategoryWithCuisinesVO>> getCategoriesWithCuisinesByStoreId(
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam(value = "keyword", required = false) String keyword);
-
-    /**
-     * 删除菜品种类(仅解除绑定+逻辑删分类,价目表菜品不变)
-     *
-     * @param categoryId 菜品种类ID
-     * @return R.data 为 Boolean
-     */
-    @DeleteMapping("/store/info/category/{categoryId}")
-    R<Boolean> deleteCategory(@PathVariable("categoryId") Integer categoryId);
-
-    // ==================== 文件上传接口 ====================
-
-    /**
-     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
-     *
-     * @param file 文件对象
-     * @return R.data 为文件路径(String)
-     */
-    @PostMapping(value = "/dining/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    R<String> uploadFile(@RequestPart("file") MultipartFile file);
-}

+ 121 - 0
alien-store/src/main/java/shop/alien/store/service/CartService.java

@@ -0,0 +1,121 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+
+/**
+ * 购物车服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface CartService {
+
+    /**
+     * 获取购物车
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO getCart(Integer tableId);
+
+    /**
+     * 添加商品到购物车
+     *
+     * @param dto 添加购物车商品DTO
+     * @return 购物车信息
+     */
+    CartDTO addItem(AddCartItemDTO dto);
+
+    /**
+     * 更新购物车商品数量
+     *
+     * @param tableId   桌号ID
+     * @param cuisineId 菜品ID
+     * @param quantity  数量
+     * @return 购物车信息
+     */
+    CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity);
+
+    /**
+     * 删除购物车商品
+     *
+     * @param tableId   桌号ID
+     * @param cuisineId 菜品ID
+     * @return 购物车信息
+     */
+    CartDTO removeItem(Integer tableId, Integer cuisineId);
+
+    /**
+     * 清空购物车
+     *
+     * @param tableId 桌号ID
+     */
+    void clearCart(Integer tableId);
+
+    /**
+     * 换桌时迁移购物车
+     *
+     * @param fromTableId 原桌号ID
+     * @param toTableId   目标桌号ID
+     * @return 迁移后的购物车信息
+     */
+    CartDTO migrateCart(Integer fromTableId, Integer toTableId);
+
+    /**
+     * 检查桌号是否已使用优惠券
+     *
+     * @param tableId 桌号ID
+     * @return true-已使用, false-未使用
+     */
+    boolean hasUsedCoupon(Integer tableId);
+
+    /**
+     * 标记桌号已使用优惠券
+     *
+     * @param tableId  桌号ID
+     * @param couponId 优惠券ID
+     */
+    void markCouponUsed(Integer tableId, Integer couponId);
+
+    /**
+     * 清除桌号优惠券使用标记(换桌时使用)
+     *
+     * @param tableId 桌号ID
+     */
+    void clearCouponUsed(Integer tableId);
+
+    /**
+     * 设置用餐人数(自动添加或更新餐具)
+     *
+     * @param tableId   桌号ID
+     * @param dinerCount 用餐人数
+     * @return 购物车信息
+     */
+    CartDTO setDinerCount(Integer tableId, Integer dinerCount);
+
+    /**
+     * 更新餐具数量
+     *
+     * @param tableId 桌号ID
+     * @param quantity 餐具数量
+     * @return 购物车信息
+     */
+    CartDTO updateTablewareQuantity(Integer tableId, Integer quantity);
+
+    /**
+     * 锁定购物车商品数量(下单时调用,将当前数量锁定,不允许减少或删除)
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO lockCartItems(Integer tableId);
+
+    /**
+     * 解锁购物车商品数量(取消订单时调用,清除已下单数量,允许重新下单)
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO unlockCartItems(Integer tableId);
+}

+ 22 - 0
alien-store/src/main/java/shop/alien/store/service/DiningCollectService.java

@@ -0,0 +1,22 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+
+/**
+ * 点餐模块-收藏服务(Feign 调 store 收藏接口)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+public interface DiningCollectService {
+
+    /**
+     * 添加收藏
+     *
+     * @param lifeCollect 收藏对象
+     * @return R.data 为 Boolean(是否成功)
+     */
+    R<Boolean> addCollect(LifeCollect lifeCollect);
+}

+ 101 - 0
alien-store/src/main/java/shop/alien/store/service/DiningCouponService.java

@@ -0,0 +1,101 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.LifeDiscountCouponVo;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 点餐模块-优惠券服务(Feign 调 store 优惠券接口,供小程序使用)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/1/29
+ */
+public interface DiningCouponService {
+
+    /**
+     * 获取当前用户优惠券列表(分 tab)
+     *
+     * @param authorization 请求头 Authorization,透传至 store 解析用户
+     * @param page          页码
+     * @param size          每页条数
+     * @param tabType       0:全部(未使用),1:即将过期,2:已使用,3:已过期
+     * @param type          券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)(可选)
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType, Integer type, Integer couponType, String storeId, String storeName);
+
+    /**
+     * 根据优惠券 id 获取优惠券详情
+     *
+     * @param authorization 请求头 Authorization
+     * @param counponId     优惠券 id(与 store 接口拼写一致)
+     * @return R.data 为 LifeDiscountCouponVo
+     */
+    R<LifeDiscountCouponVo> getCounponDetailById(String authorization, String counponId);
+
+    /**
+     * 获取该门店下用户可用/不可用优惠券列表(按消费金额区分,用于选券)
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization
+     * @param storeId        门店 id
+     * @param amount         当前消费金额(满减门槛)
+     * @param couponType     优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 Map:canUseLifeDiscountCouponVos、forbidUseLifeDiscountCouponVos
+     */
+    R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount, Integer couponType);
+
+    /**
+     * 查询用户拥有的优惠券(按符合支付条件优先,其次优惠力度大的优先)
+     *
+     * @param storeId 门店ID(可选,如果提供则只查询该门店的优惠券)
+     * @param amount  当前消费金额(用于判断是否符合支付条件)
+     * @return 优惠券列表
+     */
+    R<List<LifeDiscountCouponVo>> getUserOwnedCoupons(String storeId, BigDecimal amount);
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization,透传至 store 解析用户
+     * @param storeId       商户id
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    R<List<LifeDiscountCouponVo>> getStoreUserCouponList(String authorization, String storeId, Integer couponType);
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization   请求头 Authorization,透传至 store 解析用户
+     * @param page            页码(默认1)
+     * @param size            每页条数(默认10)
+     * @param storeId         商户id
+     * @param couponName      优惠券名称(可选)
+     * @param tab             分页类型(0:全部,1:进行中,2:已结束,3:草稿,4:未开始,5:已下架,6:已清库)
+     * @param couponsFromType 查询类型(1:我的优惠券,2:好友的优惠券)(默认1)
+     * @param couponStatus    优惠券状态(0:草稿,1:正式)(默认1)
+     * @param couponType      优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 IPage&lt;LifeDiscountCouponVo&gt;
+     */
+    R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(String authorization, int page, int size, String storeId,
+                                                          String couponName, String tab, int couponsFromType,
+                                                          int couponStatus, Integer couponType);
+
+    /**
+     * 查询用户目前所拥有的优惠券(可通过商铺ID进行查询)
+     *
+     * @param storeId     商铺ID(可选,如果提供则只查询该商铺的优惠券)
+     * @param couponType  优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return 优惠券列表
+     */
+    R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(String storeId, Integer couponType);
+}

+ 150 - 0
alien-store/src/main/java/shop/alien/store/service/DiningService.java

@@ -0,0 +1,150 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.TableDiningStatusVO;
+import shop.alien.entity.store.vo.*;
+
+import java.util.List;
+
+/**
+ * 点餐服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface DiningService {
+
+    /**
+     * 查询餐桌是否处于就餐中(就餐人数是否有数据),免登录可调用
+     *
+     * @param tableId 桌号ID
+     * @return 就餐状态(inDining、dinerCount)
+     */
+    TableDiningStatusVO getTableDiningStatus(Integer tableId);
+
+    /**
+     * 获取点餐页面信息
+     *
+     * @param tableId 桌号ID
+     * @param dinerCount 就餐人数
+     * @return 点餐页面信息
+     */
+    DiningPageInfoVO getDiningPageInfo(Integer tableId, Integer dinerCount);
+
+    /**
+     * 搜索菜品(模糊搜索,限10字)
+     *
+     * @param storeId 门店ID
+     * @param keyword 搜索关键词
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @return 菜品列表
+     */
+    List<CuisineListVO> searchCuisines(Integer storeId, String keyword, Integer tableId);
+
+    /**
+     * 根据分类获取菜品列表
+     *
+     * @param storeId 门店ID
+     * @param categoryId 分类ID(可为空,查询所有)
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @param page 页码(默认1)
+     * @param size 每页数量(默认12)
+     * @return 菜品列表
+     */
+    List<CuisineListVO> getCuisinesByCategory(Integer storeId, Integer categoryId, Integer tableId, Integer page, Integer size);
+
+    /**
+     * 获取菜品详情
+     *
+     * @param cuisineId 菜品ID
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @return 菜品详情
+     */
+    CuisineDetailVO getCuisineDetail(Integer cuisineId, Integer tableId);
+
+    /**
+     * 获取可领取的优惠券列表
+     *
+     * @param storeId 门店ID
+     * @param userId 用户ID
+     * @return 可领取优惠券列表
+     */
+    List<AvailableCouponVO> getAvailableCoupons(Integer storeId, Integer userId);
+
+    /**
+     * 领取优惠券
+     *
+     * @param couponId 优惠券ID
+     * @param userId 用户ID
+     * @return 是否成功
+     */
+    boolean receiveCoupon(Integer couponId, Integer userId);
+
+    /**
+     * 获取订单确认页面信息
+     *
+     * @param tableId 桌号ID
+     * @param dinerCount 就餐人数
+     * @param userId 用户ID
+     * @return 订单确认页面信息
+     */
+    OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId);
+
+    /**
+     * 锁定订单(防止多人同时下单)
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 解锁订单
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     */
+    void unlockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 检查订单是否被锁定
+     *
+     * @param tableId 桌号ID
+     * @return 锁定用户ID,如果未锁定返回null
+     */
+    Integer checkOrderLock(Integer tableId);
+
+    /**
+     * 获取订单结算确认页面信息
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     * @return 订单结算确认页面信息
+     */
+    shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId);
+
+    /**
+     * 锁定订单结算(防止多人同时结算)
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockSettlement(Integer orderId, Integer userId);
+
+    /**
+     * 解锁订单结算
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     */
+    void unlockSettlement(Integer orderId, Integer userId);
+
+    /**
+     * 检查订单结算是否被锁定
+     *
+     * @param orderId 订单ID
+     * @return 锁定用户ID,如果未锁定返回null
+     */
+    Integer checkSettlementLock(Integer orderId);
+}

+ 58 - 0
alien-store/src/main/java/shop/alien/store/service/DiningUserService.java

@@ -0,0 +1,58 @@
+package shop.alien.store.service;
+
+import shop.alien.store.dto.ChangePhoneDto;
+import shop.alien.store.dto.UserProfileUpdateDto;
+import shop.alien.store.vo.DiningUserVo;
+import shop.alien.store.vo.TokenVerifyVo;
+
+/**
+ * 点餐用户服务接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+public interface DiningUserService {
+
+    /**
+     * 微信小程序登录(标准流程:通过 code2session 获取 openid)
+     *
+     * @param code      微信登录凭证(通过 wx.login() 获取,必填)
+     * @param phoneCode 手机号凭证(通过 wx.getPhoneNumber() 获取,可选)
+     * @param macIp     客户端IP地址
+     * @return 用户信息(包含 token)
+     */
+    DiningUserVo wechatLogin(String code, String phoneCode, String macIp);
+
+    /**
+     * 更新用户个人信息
+     *
+     * @param dto 用户信息更新DTO
+     * @return 更新后的用户信息
+     */
+    DiningUserVo updateProfile(UserProfileUpdateDto dto);
+
+    /**
+     * 获取用户信息
+     *
+     * @param userId 用户ID
+     * @return 用户信息
+     */
+    DiningUserVo getUserInfo(Long userId);
+
+    /**
+     * 更换手机号:Feign 调 store 校验验证码后更新 user_phone
+     *
+     * @param dto 更换手机号请求
+     * @return 更新后的用户信息,失败返回 null
+     */
+    DiningUserVo changePhone(ChangePhoneDto dto);
+
+    /**
+     * 校验 Token 是否有效
+     *
+     * @param token Token字符串
+     * @return Token校验结果,包含用户信息和验证状态
+     */
+    TokenVerifyVo verifyToken(String token);
+}

+ 61 - 0
alien-store/src/main/java/shop/alien/store/service/OrderLockService.java

@@ -0,0 +1,61 @@
+package shop.alien.store.service;
+
+/**
+ * 订单锁定服务接口
+ * 用于管理订单和结算的锁定状态,防止并发操作
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface OrderLockService {
+
+    /**
+     * 锁定订单(防止多人同时下单)
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 解锁订单
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     */
+    void unlockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 检查订单是否被锁定
+     *
+     * @param tableId 桌号ID
+     * @return 锁定用户ID,如果未锁定返回null
+     */
+    Integer checkOrderLock(Integer tableId);
+
+    /**
+     * 锁定订单结算(防止多人同时结算)
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockSettlement(Integer orderId, Integer userId);
+
+    /**
+     * 解锁订单结算
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     */
+    void unlockSettlement(Integer orderId, Integer userId);
+
+    /**
+     * 检查订单结算是否被锁定
+     *
+     * @param orderId 订单ID
+     * @return 锁定用户ID,如果未锁定返回null
+     */
+    Integer checkSettlementLock(Integer orderId);
+}

+ 67 - 0
alien-store/src/main/java/shop/alien/store/service/ScanOrderStoreInfoService.java

@@ -0,0 +1,67 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
+import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+
+import java.util.List;
+
+/**
+ * 门店信息查询服务接口
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+public interface ScanOrderStoreInfoService {
+
+    /**
+     * 根据门店ID查询桌号列表
+     *
+     * @param storeId 门店ID
+     * @return 桌号列表
+     */
+    List<StoreTable> getTablesByStoreId(Integer storeId);
+
+    /**
+     * 根据门店ID查询菜品种类列表
+     *
+     * @param storeId 门店ID
+     * @return 菜品种类列表
+     */
+    List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId);
+
+    /**
+     * 根据菜品种类ID查询菜品信息列表
+     *
+     * @param categoryId 菜品种类ID
+     * @return 菜品信息列表
+     */
+    List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId);
+
+    /**
+     * 根据门店ID查询菜品种类及每个分类下的菜品列表(一次返回种类+菜品)
+     *
+     * @param storeId 门店ID
+     * @param keyword 菜品名称模糊查询关键词(可选,为空则不按名称筛选)
+     * @return 菜品种类及下属菜品列表
+     */
+    List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword);
+
+    /**
+     * 删除菜品种类:仅逻辑删除分类并解除菜品与该分类的绑定关系,价目表(菜品)本身不改动
+     *
+     * @param categoryId 菜品种类ID
+     * @return 是否成功
+     */
+    boolean deleteCategory(Integer categoryId);
+
+    /**
+     * 根据商铺ID查询店铺信息和首页展示美食价目表信息
+     *
+     * @param storeId 商铺ID
+     * @return 店铺信息和首页展示美食价目表信息
+     */
+    StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId);
+}

+ 35 - 0
alien-store/src/main/java/shop/alien/store/service/SseService.java

@@ -0,0 +1,35 @@
+package shop.alien.store.service;
+
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * SSE推送服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface SseService {
+
+    /**
+     * 创建SSE连接
+     *
+     * @param tableId 桌号ID
+     * @return SSE连接对象
+     */
+    SseEmitter createConnection(Integer tableId);
+
+    /**
+     * 推送购物车更新消息
+     *
+     * @param tableId 桌号ID
+     * @param message 消息内容
+     */
+    void pushCartUpdate(Integer tableId, Object message);
+
+    /**
+     * 关闭SSE连接
+     *
+     * @param tableId 桌号ID
+     */
+    void closeConnection(Integer tableId);
+}

+ 189 - 0
alien-store/src/main/java/shop/alien/store/service/StoreOrderService.java

@@ -0,0 +1,189 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.OrderInfoVO;
+import shop.alien.entity.store.vo.StoreOrderPageVO;
+
+import java.util.List;
+
+/**
+ * 订单服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreOrderService extends IService<StoreOrder> {
+
+    /**
+     * 创建订单
+     *
+     * @param dto 创建订单DTO
+     * @return 订单信息
+     */
+    StoreOrder createOrder(CreateOrderDTO dto);
+
+    /**
+     * 支付订单
+     *
+     * @param orderId 订单ID
+     * @param payType 支付方式
+     * @return 订单信息
+     */
+    StoreOrder payOrder(Integer orderId, Integer payType);
+
+    /**
+     * 取消订单
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean cancelOrder(Integer orderId);
+
+    /**
+     * 根据订单号查询订单
+     *
+     * @param orderNo 订单号
+     * @return 订单信息
+     */
+    StoreOrder getOrderByOrderNo(String orderNo);
+
+    /**
+     * 根据ID查询订单
+     *
+     * @param orderId 订单ID
+     * @return 订单信息
+     */
+    StoreOrder getOrderById(Integer orderId);
+
+    /**
+     * 分页查询订单列表
+     *
+     * @param page     分页参数
+     * @param storeId  门店ID
+     * @param tableId  桌号ID
+     * @param orderStatus 订单状态
+     * @param keyword  搜索关键词(订单编号或菜品名称,限15字)
+     * @return 订单分页列表
+     */
+    IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
+
+    /**
+     * 分页查询订单列表(包含菜品信息)
+     *
+     * @param page     分页参数
+     * @param storeId  门店ID
+     * @param tableId  桌号ID
+     * @param orderStatus 订单状态
+     * @param keyword  搜索关键词(订单编号或菜品名称,限15字)
+     * @return 订单分页列表(包含菜品信息)
+     */
+    IPage<StoreOrderPageVO> getOrderPageWithCuisines(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword);
+
+    /**
+     * 完成订单(支付完成后调用)
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean completeOrder(Integer orderId);
+
+    /**
+     * 商家手动完成订单(不校验支付状态)
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean completeOrderByMerchant(Integer orderId);
+
+    /**
+     * 更新订单优惠券
+     *
+     * @param orderId 订单ID
+     * @param couponId 优惠券ID(可为空,表示不使用优惠券)
+     * @return 订单信息
+     */
+    StoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
+
+    /**
+     * 管理员重置餐桌(删除购物车数据、未支付/已取消的订单数据,并重置餐桌表)
+     * 注意:已支付/已完成的订单会被保留,避免数据丢失
+     *
+     * @param tableId 餐桌ID
+     * @return 是否成功
+     */
+    boolean resetTable(Integer tableId);
+
+    /**
+     * 查询订单信息(包含订单基本信息、菜品清单、价格明细)
+     *
+     * @param orderId 订单ID
+     * @return 订单信息
+     */
+    OrderInfoVO getOrderInfo(Integer orderId);
+
+    /**
+     * 查询订单的所有变更记录(按批次分组)
+     *
+     * @param orderId 订单ID
+     * @return 变更记录批次列表
+     */
+    List<OrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId);
+
+    /**
+     * 查询订单详情(包含订单基本信息和按批次分组的变更记录)
+     *
+     * @param orderId 订单ID
+     * @return 订单详情(包含变更记录)
+     */
+    shop.alien.entity.store.vo.OrderDetailWithChangeLogVO getOrderDetailWithChangeLog(Integer orderId);
+
+    /**
+     * 支付完成后重置餐桌(保留订单数据,只重置餐桌绑定关系)
+     *
+     * @param tableId 餐桌ID
+     */
+    void resetTableAfterPayment(Integer tableId);
+
+    /**
+     * 查询我的订单(通过标识查询未支付订单或历史订单)
+     *
+     * @param page 分页参数
+     * @param type 订单类型(0或"unpaid":未支付订单, 1或"history":历史订单)
+     * @return 订单分页列表
+     */
+    IPage<StoreOrder> getMyOrders(Page<StoreOrder> page, String type);
+
+    /**
+     * 查询我的订单(包含菜品信息)
+     *
+     * @param page 分页参数
+     * @param type 订单类型(0或"unpaid":未支付订单, 1或"history":历史订单)
+     * @return 订单分页列表(包含菜品信息)
+     */
+    IPage<StoreOrderPageVO> getMyOrdersWithCuisines(Page<StoreOrder> page, String type);
+
+    /**
+     * 换桌时迁移所有关联数据(订单、订单变更记录、优惠券使用记录等)
+     *
+     * @param fromTableId 原桌号ID
+     * @param toTableId   目标桌号ID
+     * @param userId      操作用户ID
+     */
+    void migrateTableData(Integer fromTableId, Integer toTableId, Integer userId);
+
+    /**
+     * 换桌(包含所有业务逻辑:迁移购物车、订单、优惠券等,记录日志,推送消息)
+     *
+     * @param fromTableId  原桌号ID
+     * @param toTableId    目标桌号ID
+     * @param changeReason 换桌原因
+     * @param userId       操作用户ID
+     * @return 迁移后的购物车信息
+     */
+    shop.alien.entity.store.dto.CartDTO changeTable(Integer fromTableId, Integer toTableId, String changeReason, Integer userId);
+}

+ 1072 - 0
alien-store/src/main/java/shop/alien/store/service/impl/CartServiceImpl.java

@@ -0,0 +1,1072 @@
+package shop.alien.store.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.store.config.BaseRedisService;
+import shop.alien.store.service.CartService;
+import shop.alien.entity.store.StoreCart;
+import shop.alien.entity.store.StoreCouponUsage;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreInfo;
+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.StoreCuisineMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.store.util.TokenUtil;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * 购物车服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CartServiceImpl implements CartService {
+
+    private static final String CART_KEY_PREFIX = "cart:table:";
+    private static final String COUPON_USED_KEY_PREFIX = "coupon:used:table:";
+    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, "cart-db-write-" + System.currentTimeMillis());
+        t.setDaemon(true);
+        return t;
+    });
+
+    private final BaseRedisService baseRedisService;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreCartMapper storeCartMapper;
+    private final StoreCouponUsageMapper storeCouponUsageMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @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);
+        }
+
+        return 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);
+        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);
+        // 验证桌号
+        StoreTable table = storeTableMapper.selectById(dto.getTableId());
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        // 验证菜品
+        StoreCuisine cuisine = storeCuisineMapper.selectById(dto.getCuisineId());
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        if (cuisine.getShelfStatus() != 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("商品已存在且有已下单数量,叠加数量, cuisineId={}, oldQuantity={}, newQuantity={}, oldOrderedQuantity={}, newOrderedQuantity={}", 
+                        dto.getCuisineId(), existingItem.getQuantity() - dto.getQuantity(), newQuantity, lockedQuantity, newLockedQuantity);
+            } else {
+                // 商品已存在但没有已下单数量,不允许重复添加
+                throw new RuntimeException("已添加过本商品");
+            }
+        } else {
+            // 添加新商品
+            CartItemDTO newItem = new CartItemDTO();
+            newItem.setCuisineId(cuisine.getId());
+            newItem.setCuisineName(cuisine.getName());
+            newItem.setCuisineType(cuisine.getCuisineType());
+            newItem.setCuisineImage(cuisine.getImages());
+            newItem.setUnitPrice(cuisine.getTotalPrice());
+            newItem.setQuantity(dto.getQuantity());
+            newItem.setSubtotalAmount(cuisine.getTotalPrice()
+                    .multiply(BigDecimal.valueOf(dto.getQuantity())));
+            newItem.setAddUserId(userId);
+            newItem.setAddUserPhone(userPhone);
+            newItem.setRemark(dto.getRemark());
+            items.add(newItem);
+        }
+
+        // 重新计算总金额和总数量
+        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和数据库(双写策略)
+        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)));
+
+            // 重新计算总金额和总数量
+            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);
+
+            saveCart(cart);
+        } else {
+            // 商品不存在,自动添加
+            log.info("商品不在购物车中,自动添加, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
+            
+            // 验证菜品
+            StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+            if (cuisine == null) {
+                throw new RuntimeException("菜品不存在");
+            }
+            if (cuisine.getShelfStatus() != 1) {
+                throw new RuntimeException("菜品已下架");
+            }
+
+            // 获取当前用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            String userPhone = TokenUtil.getCurrentUserPhone();
+
+            // 创建新的购物车商品项
+            CartItemDTO newItem = new CartItemDTO();
+            newItem.setCuisineId(cuisine.getId());
+            newItem.setCuisineName(cuisine.getName());
+            newItem.setCuisineType(cuisine.getCuisineType());
+            newItem.setCuisineImage(cuisine.getImages());
+            newItem.setUnitPrice(cuisine.getTotalPrice());
+            newItem.setQuantity(quantity);
+            newItem.setSubtotalAmount(cuisine.getTotalPrice()
+                    .multiply(BigDecimal.valueOf(quantity)));
+            newItem.setAddUserId(userId);
+            newItem.setAddUserPhone(userPhone);
+            items.add(newItem);
+
+            // 重新计算总金额和总数量
+            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和数据库(双写策略)
+            saveCart(cart);
+            log.info("商品已自动添加到购物车, tableId={}, cuisineId={}, 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));
+
+        // 重新计算总金额和总数量
+        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);
+
+        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);
+            // 重新计算总金额和总数量(只计算保留的商品,数量已恢复为已下单数量)
+            BigDecimal totalAmount = orderedItems.stream()
+                    .map(CartItemDTO::getSubtotalAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            Integer totalQuantity = orderedItems.stream()
+                    .mapToInt(CartItemDTO::getQuantity)
+                    .sum();
+            cart.setTotalAmount(totalAmount);
+            cart.setTotalQuantity(totalQuantity);
+            
+            // 更新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);
+                // 排除餐具(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(totalQuantity);
+                table.setCartTotalAmount(totalAmount);
+                storeTableMapper.updateById(table);
+            }
+            
+            log.info("清空购物车完成(保留已下单商品和餐具,数量恢复为已下单数量), tableId={}, 删除商品数={}, 保留商品数={}", 
+                    tableId, unorderedItems.size(), orderedItems.size());
+        } else {
+            log.info("购物车无需更新, tableId={}", tableId);
+        }
+    }
+
+    @Override
+    public CartDTO migrateCart(Integer fromTableId, Integer toTableId) {
+        log.info("迁移购物车, fromTableId={}, toTableId={}", fromTableId, toTableId);
+        // 获取原购物车
+        CartDTO fromCart = getCart(fromTableId);
+
+        // 验证目标桌号
+        StoreTable toTable = storeTableMapper.selectById(toTableId);
+        if (toTable == null) {
+            throw new RuntimeException("目标桌号不存在");
+        }
+
+        // 获取目标购物车
+        CartDTO toCart = getCart(toTableId);
+
+        // 合并购物车(如果目标桌号已有商品,则合并)
+        List<CartItemDTO> mergedItems = new ArrayList<>(toCart.getItems());
+        for (CartItemDTO fromItem : fromCart.getItems()) {
+            CartItemDTO existingItem = mergedItems.stream()
+                    .filter(item -> item.getCuisineId().equals(fromItem.getCuisineId()))
+                    .findFirst()
+                    .orElse(null);
+
+            if (existingItem != null) {
+                // 合并数量
+                int newQuantity = (existingItem.getQuantity() != null ? existingItem.getQuantity() : 0)
+                        + (fromItem.getQuantity() != null ? fromItem.getQuantity() : 0);
+                existingItem.setQuantity(newQuantity);
+                existingItem.setSubtotalAmount(existingItem.getUnitPrice()
+                        .multiply(BigDecimal.valueOf(newQuantity)));
+                // 合并已下单数量(换桌后目标桌需保留两边的已下单数量)
+                int toLocked = existingItem.getLockedQuantity() != null ? existingItem.getLockedQuantity() : 0;
+                int fromLocked = fromItem.getLockedQuantity() != null ? fromItem.getLockedQuantity() : 0;
+                existingItem.setLockedQuantity(toLocked + fromLocked > 0 ? toLocked + fromLocked : null);
+            } else {
+                mergedItems.add(fromItem);
+            }
+        }
+
+        toCart.setItems(mergedItems);
+        toCart.setTableId(toTableId);
+        toCart.setTableNumber(toTable.getTableNumber());
+        toCart.setStoreId(toTable.getStoreId());
+
+        // 重新计算总金额和总数量
+        BigDecimal totalAmount = mergedItems.stream()
+                .map(CartItemDTO::getSubtotalAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Integer totalQuantity = mergedItems.stream()
+                .mapToInt(CartItemDTO::getQuantity)
+                .sum();
+        toCart.setTotalAmount(totalAmount);
+        toCart.setTotalQuantity(totalQuantity);
+
+        // 保存目标购物车
+        saveCart(toCart);
+
+        // 清空原购物车
+        clearCart(fromTableId);
+
+        // 迁移优惠券使用标记
+        if (hasUsedCoupon(fromTableId)) {
+            String couponUsedKey = COUPON_USED_KEY_PREFIX + fromTableId;
+            String couponId = baseRedisService.getString(couponUsedKey);
+            if (StringUtils.hasText(couponId)) {
+                markCouponUsed(toTableId, Integer.parseInt(couponId));
+                clearCouponUsed(fromTableId);
+            }
+        }
+
+        return toCart;
+    }
+
+    @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);
+            }
+        }
+
+        // 重新计算总金额和总数量
+        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和数据库(双写策略)
+        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);
+                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);
+                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)));
+
+        // 重新计算总金额和总数量
+        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和数据库(双写策略)
+        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);
+            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.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,继续执行
+        }
+    }
+}

+ 39 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DiningCollectServiceImpl.java

@@ -0,0 +1,39 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.store.feign.AlienStoreFeign;
+import shop.alien.store.service.DiningCollectService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeCollect;
+
+/**
+ * 点餐模块-收藏服务实现(透传 Feign 调 store)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningCollectServiceImpl implements DiningCollectService {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @Override
+    public R<Boolean> addCollect(LifeCollect lifeCollect) {
+        log.info("DiningCollectService.addCollect lifeCollect={}", lifeCollect);
+        try {
+            R<Boolean> result = alienStoreFeign.addCollect(lifeCollect);
+            if (result == null) {
+                return R.fail("添加收藏失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCollectService.addCollect ERROR Msg={}", e.getMessage());
+            return R.fail("添加收藏失败");
+        }
+    }
+}

+ 471 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DiningCouponServiceImpl.java

@@ -0,0 +1,471 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.store.feign.AlienStoreFeign;
+import shop.alien.store.service.DiningCouponService;
+import shop.alien.store.util.TokenUtil;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeDiscountCoupon;
+import shop.alien.entity.store.LifeDiscountCouponUser;
+import shop.alien.entity.store.vo.LifeDiscountCouponVo;
+import shop.alien.mapper.LifeDiscountCouponMapper;
+import shop.alien.mapper.LifeDiscountCouponUserMapper;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 点餐模块-优惠券服务实现(透传 Feign 调 store,满足小程序我的优惠券/详情/选券)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/1/29
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningCouponServiceImpl implements DiningCouponService {
+
+    private final AlienStoreFeign alienStoreFeign;
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+
+    @Override
+    public R<List<LifeDiscountCouponVo>> getUserCouponList(String authorization, int page, int size, String tabType, Integer type, Integer couponType, String storeId, String storeName) {
+        log.info("DiningCouponService.getUserCouponList page={}, size={}, tabType={}, type={}, couponType={}, storeId={}, storeName={}", page, size, tabType, type, couponType, storeId, storeName);
+        try {
+            // 参数校验
+            if (StringUtils.isEmpty(tabType)) {
+                log.warn("tabType参数为空");
+                return R.fail("分页类型不能为空");
+            }
+            if (page < 1) {
+                page = 1;
+            }
+            if (size < 1 || size > 100) {
+                size = 10;
+            }
+            
+            R<List<LifeDiscountCouponVo>> result = alienStoreFeign.getUserCouponList(authorization, tabType, page, size, type, couponType, storeId, storeName);
+            if (result == null) {
+                log.error("Feign调用返回null");
+                return R.fail("获取优惠券列表失败:服务返回为空");
+            }
+            // 如果 Feign 返回的是失败结果,直接返回
+            if (!result.isSuccess()) {
+                log.warn("Feign调用返回失败: code={}, msg={}", result.getCode(), result.getMsg());
+                return result;
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCouponService.getUserCouponList ERROR: {}", e.getMessage(), e);
+            return R.fail("获取优惠券列表失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<LifeDiscountCouponVo> getCounponDetailById(String authorization, String counponId) {
+        log.info("DiningCouponService.getCounponDetailById counponId={}", counponId);
+        try {
+            R<LifeDiscountCouponVo> result = alienStoreFeign.getCounponDetailById(authorization, counponId);
+            if (result == null) {
+                return R.fail("获取优惠券详情失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCouponService.getCounponDetailById ERROR Msg={}", e.getMessage());
+            return R.fail("获取优惠券详情失败");
+        }
+    }
+
+    @Override
+    public R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount, Integer couponType) {
+        log.info("DiningCouponService.getStoreUserUsableCouponList storeId={}, amount={}, couponType={}", storeId, amount, couponType);
+        try {
+            R<Map<String, Object>> result = alienStoreFeign.getStoreUserUsableCouponList(authorization, storeId, amount, couponType);
+            if (result == null) {
+                return R.fail("获取门店可用优惠券列表失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCouponService.getStoreUserUsableCouponList ERROR Msg={}", e.getMessage());
+            return R.fail("获取门店可用优惠券列表失败");
+        }
+    }
+
+    @Override
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCoupons(String storeId, BigDecimal amount) {
+        log.info("查询用户拥有的优惠券, storeId={}, amount={}", storeId, amount);
+
+        try {
+            // 获取当前用户ID
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+
+            // 查询用户拥有的优惠券(未使用且未过期的)
+            LambdaQueryWrapper<LifeDiscountCouponUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+            userWrapper.eq(LifeDiscountCouponUser::getStatus, 0); // 0:待使用
+            userWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+            userWrapper.ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()); // 未过期
+            List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
+
+            if (userCoupons == null || userCoupons.isEmpty()) {
+                log.info("用户没有可用的优惠券, userId={}", userId);
+                return R.data(new ArrayList<>());
+            }
+
+            // 获取优惠券ID列表
+            List<Integer> couponIds = userCoupons.stream()
+                    .map(LifeDiscountCouponUser::getCouponId)
+                    .collect(Collectors.toList());
+
+            // 查询优惠券详情
+            LambdaQueryWrapper<LifeDiscountCoupon> couponWrapper = new LambdaQueryWrapper<>();
+            couponWrapper.in(LifeDiscountCoupon::getId, couponIds);
+            couponWrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+            if (StringUtils.hasText(storeId)) {
+                couponWrapper.eq(LifeDiscountCoupon::getStoreId, storeId);
+            }
+            List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(couponWrapper);
+
+            if (coupons == null || coupons.isEmpty()) {
+                log.info("未找到优惠券详情, userId={}, couponIds={}", userId, couponIds);
+                return R.data(new ArrayList<>());
+            }
+
+            // 转换为VO并设置用户券ID,同时过滤掉已过期和未在使用时间内的优惠券
+            LocalDate now = LocalDate.now();
+            List<LifeDiscountCouponVo> couponVos = new ArrayList<>();
+            for (LifeDiscountCoupon coupon : coupons) {
+                // 找到对应的用户券
+                LifeDiscountCouponUser userCoupon = userCoupons.stream()
+                        .filter(uc -> uc.getCouponId().equals(coupon.getId()))
+                        .findFirst()
+                        .orElse(null);
+
+                if (userCoupon == null) {
+                    continue;
+                }
+
+                // 过滤1:检查用户券的过期时间(expirationTime)
+                if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
+                    log.debug("过滤已过期的用户券, couponId={}, expirationTime={}, now={}", 
+                            coupon.getId(), userCoupon.getExpirationTime(), now);
+                    continue;
+                }
+
+                // 过滤2:检查优惠券的使用时间范围(startDate 和 endDate)
+                // 如果优惠券有设置使用时间范围,则检查当前日期是否在范围内
+                if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
+                    boolean inTimeRange = true;
+                    if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
+                        // 当前日期早于开始日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
+                        // 当前日期晚于结束日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (!inTimeRange) {
+                        log.debug("过滤未在使用时间内的优惠券, couponId={}, startDate={}, endDate={}, now={}", 
+                                coupon.getId(), coupon.getStartDate(), coupon.getEndDate(), now);
+                        continue;
+                    }
+                }
+
+                // 过滤3:检查优惠券的有效期(validDate)
+                if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
+                    log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}", 
+                            coupon.getId(), coupon.getValidDate(), now);
+                    continue;
+                }
+
+                LifeDiscountCouponVo vo = convertToVo(coupon, userCoupon);
+                couponVos.add(vo);
+            }
+
+            // 排序:符合支付条件的优先,其次优惠力度大的优先
+            if (amount != null) {
+                couponVos.sort((vo1, vo2) -> {
+                    // 判断是否符合支付条件(满足最低消费要求)
+                    boolean vo1CanUse = vo1.getMinimumSpendingAmount() == null
+                            || vo1.getMinimumSpendingAmount().compareTo(amount) <= 0;
+                    boolean vo2CanUse = vo2.getMinimumSpendingAmount() == null
+                            || vo2.getMinimumSpendingAmount().compareTo(amount) <= 0;
+
+                    // 第一优先级:符合支付条件的优先
+                    if (vo1CanUse && !vo2CanUse) {
+                        return -1; // vo1 排在前面
+                    }
+                    if (!vo1CanUse && vo2CanUse) {
+                        return 1; // vo2 排在前面
+                    }
+
+                    // 第二优先级:优惠力度大的优先(根据优惠券类型计算实际优惠金额)
+                    BigDecimal discountAmount1 = calculateDiscountAmountForVO(vo1, amount);
+                    BigDecimal discountAmount2 = calculateDiscountAmountForVO(vo2, amount);
+                    return discountAmount2.compareTo(discountAmount1); // 降序排列
+                });
+            } else {
+                // 如果没有提供金额,无法计算折扣券的实际优惠金额,只按面值排序(满减券)
+                // 注意:这种情况下折扣券无法准确排序,建议前端传入订单金额
+                couponVos.sort((vo1, vo2) -> {
+                    BigDecimal nominalValue1 = vo1.getNominalValue() != null ? vo1.getNominalValue() : BigDecimal.ZERO;
+                    BigDecimal nominalValue2 = vo2.getNominalValue() != null ? vo2.getNominalValue() : BigDecimal.ZERO;
+                    return nominalValue2.compareTo(nominalValue1); // 降序排列
+                });
+            }
+
+            log.info("查询用户拥有的优惠券成功, userId={}, count={}", userId, couponVos.size());
+            return R.data(couponVos);
+
+        } catch (Exception e) {
+            log.error("查询用户拥有的优惠券失败: {}", e.getMessage(), e);
+            return R.fail("查询用户拥有的优惠券失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<List<LifeDiscountCouponVo>> getStoreUserCouponList(String authorization, String storeId, Integer couponType) {
+        log.info("DiningCouponService.getStoreUserCouponList storeId={}, couponType={}", storeId, couponType);
+        try {
+            R<List<LifeDiscountCouponVo>> result = alienStoreFeign.getStoreUserCouponList(authorization, storeId, couponType);
+            if (result == null) {
+                return R.fail("获取该用户该店铺优惠券列表失败");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("DiningCouponService.getStoreUserCouponList ERROR Msg={}", e.getMessage());
+            return R.fail("获取该用户该店铺优惠券列表失败");
+        }
+    }
+
+    @Override
+    public R<IPage<LifeDiscountCouponVo>> getStoreAllCouponList(String authorization, int page, int size, String storeId,
+                                                                String couponName, String tab, int couponsFromType,
+                                                                int couponStatus, Integer couponType) {
+        log.info("DiningCouponService.getStoreAllCouponList storeId={}, page={}, size={}, couponName={}, tab={}, couponsFromType={}, couponStatus={}, couponType={}",
+                storeId, page, size, couponName, tab, couponsFromType, couponStatus, couponType);
+        try {
+            // Feign 返回 Page 类型(具体实现类),但 Service 接口使用 IPage(接口类型)
+            // Page 实现了 IPage,所以可以直接返回
+            R<com.baomidou.mybatisplus.extension.plugins.pagination.Page<LifeDiscountCouponVo>> result = alienStoreFeign.getStoreAllCouponList(
+                    authorization, page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+            if (result == null) {
+                return R.fail("获取该店铺所有优惠券列表失败");
+            }
+            // Page 实现了 IPage 接口,可以直接转换
+            return R.data(result.getData());
+        } catch (Exception e) {
+            log.error("DiningCouponService.getStoreAllCouponList ERROR Msg={}", e.getMessage(), e);
+            return R.fail("获取该店铺所有优惠券列表失败");
+        }
+    }
+
+    @Override
+    public R<List<LifeDiscountCouponVo>> getUserOwnedCouponsByStore(String storeId, Integer couponType) {
+        log.info("查询用户目前所拥有的优惠券, storeId={}, couponType={}", storeId, couponType);
+
+        try {
+            // 获取当前用户ID
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+
+            // 查询用户拥有的优惠券(未使用且未过期的)
+            LambdaQueryWrapper<LifeDiscountCouponUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+            userWrapper.eq(LifeDiscountCouponUser::getStatus, 0); // 0:待使用
+            userWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+            userWrapper.ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()); // 未过期
+            List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
+
+            if (userCoupons == null || userCoupons.isEmpty()) {
+                log.info("用户没有可用的优惠券, userId={}", userId);
+                return R.data(new ArrayList<>());
+            }
+
+            // 获取优惠券ID列表
+            List<Integer> couponIds = userCoupons.stream()
+                    .map(LifeDiscountCouponUser::getCouponId)
+                    .collect(Collectors.toList());
+
+            // 查询优惠券详情
+            LambdaQueryWrapper<LifeDiscountCoupon> couponWrapper = new LambdaQueryWrapper<>();
+            couponWrapper.in(LifeDiscountCoupon::getId, couponIds);
+            couponWrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+            // 如果提供了商铺ID,则只查询该商铺的优惠券
+            if (StringUtils.hasText(storeId)) {
+                couponWrapper.eq(LifeDiscountCoupon::getStoreId, storeId);
+            }
+            // 如果提供了优惠券类型,则只查询该类型的优惠券
+            if (couponType != null) {
+                couponWrapper.eq(LifeDiscountCoupon::getCouponType, couponType);
+            }
+            List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(couponWrapper);
+
+            if (coupons == null || coupons.isEmpty()) {
+                log.info("未找到优惠券详情, userId={}, storeId={}, couponIds={}", userId, storeId, couponIds);
+                return R.data(new ArrayList<>());
+            }
+
+            // 转换为VO并设置用户券ID,同时过滤掉已过期和未在使用时间内的优惠券
+            LocalDate now = LocalDate.now();
+            List<LifeDiscountCouponVo> couponVos = new ArrayList<>();
+            for (LifeDiscountCoupon coupon : coupons) {
+                // 找到对应的用户券
+                LifeDiscountCouponUser userCoupon = userCoupons.stream()
+                        .filter(uc -> uc.getCouponId().equals(coupon.getId()))
+                        .findFirst()
+                        .orElse(null);
+
+                if (userCoupon == null) {
+                    continue;
+                }
+
+                // 过滤1:检查用户券的过期时间(expirationTime)
+                if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
+                    log.debug("过滤已过期的用户券, couponId={}, expirationTime={}, now={}", 
+                            coupon.getId(), userCoupon.getExpirationTime(), now);
+                    continue;
+                }
+
+                // 过滤2:检查优惠券的使用时间范围(startDate 和 endDate)
+                // 如果优惠券有设置使用时间范围,则检查当前日期是否在范围内
+                if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
+                    boolean inTimeRange = true;
+                    if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
+                        // 当前日期早于开始日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
+                        // 当前日期晚于结束日期,未在使用时间内
+                        inTimeRange = false;
+                    }
+                    if (!inTimeRange) {
+                        log.debug("过滤未在使用时间内的优惠券, couponId={}, startDate={}, endDate={}, now={}", 
+                                coupon.getId(), coupon.getStartDate(), coupon.getEndDate(), now);
+                        continue;
+                    }
+                }
+
+                // 过滤3:检查优惠券的有效期(validDate)
+                if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
+                    log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}", 
+                            coupon.getId(), coupon.getValidDate(), now);
+                    continue;
+                }
+
+                LifeDiscountCouponVo vo = convertToVo(coupon, userCoupon);
+                couponVos.add(vo);
+            }
+
+            // 按创建时间倒序排列(最新的在前)
+            couponVos.sort((vo1, vo2) -> {
+                if (vo1.getCreatedTime() == null && vo2.getCreatedTime() == null) {
+                    return 0;
+                }
+                if (vo1.getCreatedTime() == null) {
+                    return 1;
+                }
+                if (vo2.getCreatedTime() == null) {
+                    return -1;
+                }
+                return vo2.getCreatedTime().compareTo(vo1.getCreatedTime()); // 降序排列
+            });
+
+            log.info("查询用户拥有的优惠券成功, userId={}, storeId={}, count={}", userId, storeId, couponVos.size());
+            return R.data(couponVos);
+
+        } catch (Exception e) {
+            log.error("查询用户拥有的优惠券失败: {}", e.getMessage(), e);
+            return R.fail("查询用户拥有的优惠券失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 将优惠券实体转换为VO
+     */
+    private LifeDiscountCouponVo convertToVo(LifeDiscountCoupon coupon, LifeDiscountCouponUser userCoupon) {
+        LifeDiscountCouponVo vo = new LifeDiscountCouponVo();
+        vo.setId(coupon.getId());
+        vo.setUserCouponId(userCoupon.getId());
+        vo.setStoreId(coupon.getStoreId());
+        vo.setUserId(String.valueOf(userCoupon.getUserId()));
+        vo.setCouponId(coupon.getId());
+        vo.setName(coupon.getName());
+        vo.setNominalValue(coupon.getNominalValue());
+        vo.setExpirationDate(coupon.getExpirationDate());
+        vo.setStartDate(coupon.getStartDate());
+        vo.setEndDate(coupon.getEndDate());
+        vo.setSingleQty(coupon.getSingleQty());
+        vo.setSupplementaryInstruction(coupon.getSupplementaryInstruction());
+        vo.setGetStatus(coupon.getGetStatus());
+        vo.setRestrictedQuantity(coupon.getRestrictedQuantity());
+        vo.setMinimumSpendingAmount(coupon.getMinimumSpendingAmount());
+        vo.setType(coupon.getType());
+        vo.setAttentionCanReceived(coupon.getAttentionCanReceived());
+        vo.setExpirationTime(userCoupon.getExpirationTime());
+        vo.setCreatedTime(coupon.getCreatedTime());
+        vo.setCouponType(coupon.getCouponType());
+        vo.setDiscountRate(coupon.getDiscountRate());
+        return vo;
+    }
+
+    /**
+     * 计算优惠券的实际优惠金额(用于排序)
+     * 根据优惠券类型(满减券或折扣券)计算实际优惠金额
+     *
+     * @param vo          优惠券VO
+     * @param orderAmount 订单金额(用于计算折扣券的优惠金额)
+     * @return 实际优惠金额
+     */
+    private BigDecimal calculateDiscountAmountForVO(LifeDiscountCouponVo vo, BigDecimal orderAmount) {
+        if (vo == null) {
+            return BigDecimal.ZERO;
+        }
+
+        Integer couponType = vo.getCouponType();
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponType != null && couponType == 2) {
+            // 折扣券:根据折扣率计算优惠金额
+            // discountRate: 0-100,例如80表示8折,优惠金额 = 订单金额 * (100 - discountRate) / 100
+            BigDecimal discountRate = vo.getDiscountRate();
+            if (discountRate != null && discountRate.compareTo(BigDecimal.ZERO) > 0 
+                    && discountRate.compareTo(new BigDecimal(100)) <= 0 && orderAmount != null 
+                    && orderAmount.compareTo(BigDecimal.ZERO) > 0) {
+                // 计算折扣后的金额
+                BigDecimal discountedAmount = orderAmount.multiply(discountRate)
+                        .divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
+                // 优惠金额 = 原价 - 折扣后价格
+                discountAmount = orderAmount.subtract(discountedAmount);
+            }
+        } else {
+            // 满减券(默认或couponType=1):使用nominalValue
+            discountAmount = vo.getNominalValue();
+            if (discountAmount == null) {
+                discountAmount = BigDecimal.ZERO;
+            }
+            // 优惠金额不能超过订单总金额
+            if (orderAmount != null && discountAmount.compareTo(orderAmount) > 0) {
+                discountAmount = orderAmount;
+            }
+        }
+
+        return discountAmount;
+    }
+}

+ 690 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DiningServiceImpl.java

@@ -0,0 +1,690 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+import shop.alien.entity.store.vo.*;
+import shop.alien.mapper.*;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.DiningService;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 点餐服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningServiceImpl implements DiningService {
+
+    private final StoreTableMapper storeTableMapper;
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
+    private final StoreCuisineComboMapper storeCuisineComboMapper;
+    private final StoreOrderDetailMapper storeOrderDetailMapper;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    private final CartService cartService;
+    private final BaseRedisService baseRedisService;
+    private final shop.alien.store.service.StoreOrderService storeOrderService;
+    private final StoreOrderMapper storeOrderMapper;
+    private final shop.alien.store.service.OrderLockService orderLockService;
+
+    @Override
+    public TableDiningStatusVO getTableDiningStatus(Integer tableId) {
+        if (tableId == null) {
+            return new TableDiningStatusVO(null, false, null);
+        }
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            return new TableDiningStatusVO(tableId, false, null);
+        }
+        // 就餐中(1)、加餐(3) 均视为就餐状态;且需已有就餐人数
+        Integer status = table.getStatus();
+        boolean inDining = (Integer.valueOf(1).equals(status) || Integer.valueOf(3).equals(status))
+                && table.getDinerCount() != null && table.getDinerCount() > 0;
+        Integer dinerCount = inDining ? table.getDinerCount() : null;
+        return new TableDiningStatusVO(tableId, inDining, dinerCount);
+    }
+
+    @Override
+    public DiningPageInfoVO getDiningPageInfo(Integer tableId, Integer dinerCount) {
+        log.info("获取点餐页面信息, tableId={}, dinerCount={}", tableId, dinerCount);
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        StoreInfo storeInfo = storeInfoMapper.selectById(table.getStoreId());
+        if (storeInfo == null) {
+            throw new RuntimeException("门店不存在");
+        }
+
+        // 第一个用户:传入用餐人数时,将餐桌置为就餐中并保存就餐人数
+        if (dinerCount != null && dinerCount > 0) {
+            table.setStatus(1); // 就餐中
+            table.setDinerCount(dinerCount);
+            storeTableMapper.updateById(table);
+            log.info("首客选桌并填写用餐人数, tableId={}, dinerCount={}, 餐桌状态已置为就餐中", tableId, dinerCount);
+        } else {
+            // 第二个及后续用户:若餐桌已就餐中且表内已有就餐人数,直接使用
+            if (Integer.valueOf(1).equals(table.getStatus()) && table.getDinerCount() != null && table.getDinerCount() > 0) {
+                dinerCount = table.getDinerCount();
+                log.info("餐桌已就餐中,使用已保存的就餐人数, tableId={}, dinerCount={}", tableId, dinerCount);
+            } else {
+                // 餐桌空闲且未传就餐人数时要求必填
+                throw new RuntimeException("请选择用餐人数");
+            }
+        }
+
+        DiningPageInfoVO vo = new DiningPageInfoVO();
+        vo.setStoreName(storeInfo.getStoreName());
+        vo.setTableNumber(table.getTableNumber());
+        vo.setDinerCount(dinerCount);
+        vo.setStoreId(table.getStoreId());
+        vo.setTableId(tableId);
+
+        return vo;
+    }
+
+    @Override
+    public List<CuisineListVO> searchCuisines(Integer storeId, String keyword, Integer tableId) {
+        log.info("搜索菜品, storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
+
+        // 限制搜索关键词长度
+        if (StringUtils.hasText(keyword) && keyword.length() > 10) {
+            keyword = keyword.substring(0, 10);
+        }
+
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        if (StringUtils.hasText(keyword)) {
+            wrapper.like(StoreCuisine::getName, keyword);
+        }
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+
+        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        return convertToCuisineListVO(cuisines, tableId);
+    }
+
+    @Override
+    public List<CuisineListVO> getCuisinesByCategory(Integer storeId, Integer categoryId, Integer tableId, Integer page, Integer size) {
+        log.info("根据分类获取菜品列表, storeId={}, categoryId={}, tableId={}, page={}, size={}", storeId, categoryId, tableId, page, size);
+
+        if (page == null || page < 1) {
+            page = 1;
+        }
+        if (size == null || size < 1) {
+            size = 12;
+        }
+
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        if (categoryId != null) {
+            // 这里假设菜品表有category_id字段,如果没有需要关联查询
+            // wrapper.eq(StoreCuisine::getCategoryId, categoryId);
+        }
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+
+        // 分页查询
+        int offset = (page - 1) * size;
+        wrapper.last("LIMIT " + offset + ", " + size);
+
+        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        return convertToCuisineListVO(cuisines, tableId);
+    }
+
+    @Override
+    public CuisineDetailVO getCuisineDetail(Integer cuisineId, Integer tableId) {
+        log.info("获取菜品详情, cuisineId={}, tableId={}", cuisineId, tableId);
+
+        StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+
+        CuisineDetailVO vo = new CuisineDetailVO();
+        // 手动映射所有字段,确保所有属性都被正确复制
+        vo.setId(cuisine.getId());
+        vo.setStoreId(cuisine.getStoreId());
+        vo.setName(cuisine.getName());
+        vo.setTotalPrice(cuisine.getTotalPrice());
+        vo.setCuisineType(cuisine.getCuisineType());
+        vo.setCategoryIds(cuisine.getCategoryIds());
+        vo.setTags(cuisine.getTags());
+        vo.setDishReview(cuisine.getDishReview());
+        vo.setDescription(cuisine.getDescription());
+        vo.setDetailContent(cuisine.getDetailContent());
+        vo.setImageContent(cuisine.getImageContent());
+        vo.setExtraNote(cuisine.getExtraNote());
+        vo.setNeedReserve(cuisine.getNeedReserve());
+        vo.setReserveRule(cuisine.getReserveRule());
+        vo.setPeopleLimit(cuisine.getPeopleLimit());
+        vo.setUsageRule(cuisine.getUsageRule());
+
+        // 处理图片列表
+        if (StringUtils.hasText(cuisine.getImages())) {
+            List<String> images = Arrays.asList(cuisine.getImages().split(","));
+            vo.setImages(images);
+        } else {
+            vo.setImages(new ArrayList<>());
+        }
+
+        // 计算月售数量
+        vo.setMonthlySales(getMonthlySales(cuisineId));
+
+        // 获取购物车数量
+        CartDTO cart = cartService.getCart(tableId);
+        if (cart.getItems() != null) {
+            Optional<CartItemDTO> cartItem = cart.getItems().stream()
+                    .filter(item -> item.getCuisineId().equals(cuisineId))
+                    .findFirst();
+            vo.setCartQuantity(cartItem.map(CartItemDTO::getQuantity).orElse(0));
+        } else {
+            vo.setCartQuantity(0);
+        }
+
+        // 如果是套餐,获取套餐包含的菜品
+        if (cuisine.getCuisineType() != null && cuisine.getCuisineType() == 2) {
+            LambdaQueryWrapper<StoreCuisineCombo> comboWrapper = new LambdaQueryWrapper<>();
+            comboWrapper.eq(StoreCuisineCombo::getCid, cuisineId);
+            comboWrapper.eq(StoreCuisineCombo::getDeleteFlag, 0);
+            List<StoreCuisineCombo> combos = storeCuisineComboMapper.selectList(comboWrapper);
+
+            List<CuisineComboItemVO> comboItems = combos.stream().map(combo -> {
+                CuisineComboItemVO item = new CuisineComboItemVO();
+                item.setCuisineId(combo.getSid());
+                item.setQuantity(combo.getSnum());
+                item.setCategory(combo.getCategory());
+                // 查询菜品名称
+                StoreCuisine comboCuisine = storeCuisineMapper.selectById(combo.getSid());
+                if (comboCuisine != null) {
+                    item.setCuisineName(comboCuisine.getName());
+                }
+                return item;
+            }).collect(Collectors.toList());
+            vo.setComboItems(comboItems);
+        }
+
+        return vo;
+    }
+
+    @Override
+    public List<AvailableCouponVO> getAvailableCoupons(Integer storeId, Integer userId) {
+        log.info("获取可领取优惠券列表, storeId={}, userId={}", storeId, userId);
+
+        // 查询门店的优惠券
+        LambdaQueryWrapper<LifeDiscountCoupon> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LifeDiscountCoupon::getStoreId, String.valueOf(storeId));
+        wrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+        wrapper.eq(LifeDiscountCoupon::getGetStatus, 1); // 开启领取
+        wrapper.gt(LifeDiscountCoupon::getSingleQty, 0); // 有库存
+        LocalDate now = LocalDate.now();
+        wrapper.le(LifeDiscountCoupon::getStartDate, now);
+        wrapper.ge(LifeDiscountCoupon::getEndDate, now);
+        wrapper.orderByDesc(LifeDiscountCoupon::getCreatedTime);
+
+        List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(wrapper);
+
+        // 查询用户已领取的优惠券
+        Set<Integer> receivedCouponIds = new HashSet<>();
+        if (userId != null) {
+            LambdaQueryWrapper<LifeDiscountCouponUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+            List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
+            receivedCouponIds = userCoupons.stream()
+                    .map(LifeDiscountCouponUser::getCouponId)
+                    .collect(Collectors.toSet());
+        }
+
+        final Set<Integer> finalReceivedCouponIds = receivedCouponIds;
+        return coupons.stream().map(coupon -> {
+            AvailableCouponVO vo = new AvailableCouponVO();
+            vo.setId(coupon.getId());
+            vo.setName(coupon.getName());
+            vo.setNominalValue(coupon.getNominalValue());
+            vo.setMinimumSpendingAmount(coupon.getMinimumSpendingAmount());
+            vo.setEndDate(coupon.getEndDate());
+            vo.setIsReceived(finalReceivedCouponIds.contains(coupon.getId()));
+            vo.setIsAvailable(coupon.getSingleQty() > 0 && coupon.getEndDate().isAfter(now) || coupon.getEndDate().isEqual(now));
+            // 设置优惠券类型和折扣率(如果需要,可以在VO中添加这些字段)
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean receiveCoupon(Integer couponId, Integer userId) {
+        log.info("领取优惠券, couponId={}, userId={}", couponId, userId);
+
+        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+        if (coupon == null) {
+            throw new RuntimeException("优惠券不存在");
+        }
+
+        // 检查是否已领取
+        LambdaQueryWrapper<LifeDiscountCouponUser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+        wrapper.eq(LifeDiscountCouponUser::getCouponId, couponId);
+        LifeDiscountCouponUser existing = lifeDiscountCouponUserMapper.selectOne(wrapper);
+        if (existing != null) {
+            throw new RuntimeException("您已领取过该优惠券");
+        }
+
+        // 检查库存
+        if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {
+            throw new RuntimeException("优惠券已领完");
+        }
+
+        // 创建用户优惠券记录
+        LifeDiscountCouponUser userCoupon = new LifeDiscountCouponUser();
+        userCoupon.setUserId(userId);
+        userCoupon.setCouponId(couponId);
+        userCoupon.setReceiveTime(new Date());
+        userCoupon.setStatus(0); // 待使用
+        // 注意:如果需要设置 issueSource,需要确保 LifeDiscountCouponUser 实体类已包含该字段
+        if (coupon.getSpecifiedDay() != null && !coupon.getSpecifiedDay().isEmpty()) {
+            try {
+                int days = Integer.parseInt(coupon.getSpecifiedDay());
+                LocalDate expirationDate = LocalDate.now().plusDays(days);
+                userCoupon.setExpirationTime(expirationDate);
+            } catch (NumberFormatException e) {
+                userCoupon.setExpirationTime(coupon.getEndDate());
+            }
+        } else {
+            userCoupon.setExpirationTime(coupon.getEndDate());
+        }
+        lifeDiscountCouponUserMapper.insert(userCoupon);
+
+        // 更新库存
+        coupon.setSingleQty(coupon.getSingleQty() - 1);
+        lifeDiscountCouponMapper.updateById(coupon);
+
+        return true;
+    }
+
+    @Override
+    public OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId) {
+        log.info("获取订单确认页面信息, tableId={}, dinerCount={}, userId={}", tableId, dinerCount, userId);
+
+        // 获取点餐页面信息(就餐中且未传人数时,内部会使用桌台已保存的就餐人数)
+        DiningPageInfoVO pageInfo = getDiningPageInfo(tableId, dinerCount);
+        int effectiveDinerCount = pageInfo.getDinerCount() != null && pageInfo.getDinerCount() > 0
+                ? pageInfo.getDinerCount() : (dinerCount != null ? dinerCount : 0);
+
+        // 获取购物车
+        CartDTO cart = cartService.getCart(tableId);
+
+        // 为购物车项补全菜品标签(购物车从 DB 加载时可能无 tags)
+        List<CartItemDTO> items = cart.getItems();
+        if (items != null && !items.isEmpty()) {
+            Set<Integer> cuisineIds = items.stream()
+                    .map(CartItemDTO::getCuisineId)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toSet());
+            if (!cuisineIds.isEmpty()) {
+                List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+                Map<Integer, String> tagsMap = new HashMap<>();
+                if (cuisines != null) {
+                    for (StoreCuisine c : cuisines) {
+                        if (c.getTags() != null) {
+                            tagsMap.put(c.getId(), c.getTags());
+                        }
+                    }
+                }
+                for (CartItemDTO item : items) {
+                    if (item.getCuisineId() != null && item.getTags() == null) {
+                        item.setTags(tagsMap.get(item.getCuisineId()));
+                    }
+                }
+            }
+        }
+
+        // 检查订单锁定
+        Integer lockUserId = orderLockService.checkOrderLock(tableId);
+        boolean isLocked = lockUserId != null && !lockUserId.equals(userId);
+
+        OrderConfirmVO vo = new OrderConfirmVO();
+        vo.setStoreName(pageInfo.getStoreName());
+        vo.setTableNumber(pageInfo.getTableNumber());
+        vo.setDinerCount(pageInfo.getDinerCount() != null ? pageInfo.getDinerCount() : dinerCount);
+        // 联系电话和备注由前端传入,这里不设置默认值
+        vo.setItems(items != null ? items : cart.getItems());
+        vo.setTotalAmount(cart.getTotalAmount());
+        vo.setIsLocked(isLocked);
+        vo.setLockUserId(lockUserId);
+
+        // 计算餐具费(默认1元/人)
+        BigDecimal tablewareUnitPrice = new BigDecimal("1.00");
+        BigDecimal tablewareFee = BigDecimal.ZERO;
+        if (effectiveDinerCount > 0) {
+            tablewareFee = tablewareUnitPrice.multiply(BigDecimal.valueOf(effectiveDinerCount));
+        }
+        vo.setTablewareFee(tablewareFee);
+        vo.setTablewareUnitPrice(tablewareUnitPrice);
+
+        // 自动匹配最优惠的优惠券
+        List<AvailableCouponVO> availableCoupons = new ArrayList<>();
+        if (userId != null && cart.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
+            availableCoupons = getAvailableCoupons(pageInfo.getStoreId(), userId);
+            // 过滤出可用的优惠券(已领取且满足最低消费)
+            BigDecimal totalWithTableware = cart.getTotalAmount().add(tablewareFee);
+            // 需要查询优惠券详情来计算实际优惠金额(用于排序)
+            List<Integer> couponIds = availableCoupons.stream()
+                    .filter(c -> c.getIsReceived() && c.getIsAvailable())
+                    .filter(c -> totalWithTableware.compareTo(c.getMinimumSpendingAmount() != null ? c.getMinimumSpendingAmount() : BigDecimal.ZERO) >= 0)
+                    .map(AvailableCouponVO::getId)
+                    .collect(Collectors.toList());
+            
+            // 查询优惠券详情用于计算实际优惠金额
+            Map<Integer, LifeDiscountCoupon> couponMap = new HashMap<>();
+            if (!couponIds.isEmpty()) {
+                List<LifeDiscountCoupon> couponDetails = lifeDiscountCouponMapper.selectBatchIds(couponIds);
+                couponMap = couponDetails.stream().collect(Collectors.toMap(LifeDiscountCoupon::getId, c -> c));
+            }
+            
+            final BigDecimal finalTotal = totalWithTableware;
+            final Map<Integer, LifeDiscountCoupon> finalCouponMap = couponMap;
+            List<AvailableCouponVO> usableCoupons = availableCoupons.stream()
+                    .filter(c -> c.getIsReceived() && c.getIsAvailable())
+                    .filter(c -> totalWithTableware.compareTo(c.getMinimumSpendingAmount() != null ? c.getMinimumSpendingAmount() : BigDecimal.ZERO) >= 0)
+                    .sorted((a, b) -> {
+                        // 根据实际优惠金额排序(考虑折扣券)
+                        BigDecimal discountA = calculateDiscountAmountForVO(a, finalCouponMap, finalTotal);
+                        BigDecimal discountB = calculateDiscountAmountForVO(b, finalCouponMap, finalTotal);
+                        return discountB.compareTo(discountA); // 降序排列
+                    })
+                    .collect(Collectors.toList());
+
+            if (!usableCoupons.isEmpty()) {
+                AvailableCouponVO bestCoupon = usableCoupons.get(0);
+                vo.setCouponId(bestCoupon.getId());
+                vo.setCouponName(bestCoupon.getName());
+                // 根据优惠券类型计算优惠金额
+                // 需要查询优惠券详情来计算折扣券的优惠金额
+                LifeDiscountCoupon couponDetail = lifeDiscountCouponMapper.selectById(bestCoupon.getId());
+                BigDecimal discountAmount = calculateDiscountAmount(couponDetail, cart.getTotalAmount().add(tablewareFee));
+                BigDecimal totalAmount = cart.getTotalAmount().add(tablewareFee);
+                vo.setDiscountAmount(discountAmount);
+                vo.setPayAmount(totalAmount.subtract(discountAmount));
+            } else {
+                vo.setPayAmount(cart.getTotalAmount().add(tablewareFee));
+            }
+        } else {
+            vo.setPayAmount(cart.getTotalAmount().add(tablewareFee));
+        }
+        vo.setAvailableCoupons(availableCoupons);
+
+        return vo;
+    }
+
+    @Override
+    public boolean lockOrder(Integer tableId, Integer userId) {
+        return orderLockService.lockOrder(tableId, userId);
+    }
+
+    @Override
+    public void unlockOrder(Integer tableId, Integer userId) {
+        orderLockService.unlockOrder(tableId, userId);
+    }
+
+    @Override
+    public Integer checkOrderLock(Integer tableId) {
+        return orderLockService.checkOrderLock(tableId);
+    }
+
+    /**
+     * 转换为菜品列表VO
+     */
+    private List<CuisineListVO> convertToCuisineListVO(List<StoreCuisine> cuisines, Integer tableId) {
+        // 获取购物车
+        CartDTO cart = cartService.getCart(tableId);
+        Map<Integer, Integer> cartQuantityMap = new HashMap<>();
+        if (cart.getItems() != null) {
+            cartQuantityMap = cart.getItems().stream()
+                    .collect(Collectors.toMap(CartItemDTO::getCuisineId, CartItemDTO::getQuantity));
+        }
+
+        // 批量查询月售数量
+        Map<Integer, Integer> monthlySalesMap = new HashMap<>();
+        for (StoreCuisine cuisine : cuisines) {
+            monthlySalesMap.put(cuisine.getId(), getMonthlySales(cuisine.getId()));
+        }
+
+        final Map<Integer, Integer> finalCartQuantityMap = cartQuantityMap;
+        final Map<Integer, Integer> finalMonthlySalesMap = monthlySalesMap;
+
+        return cuisines.stream().map(cuisine -> {
+            CuisineListVO vo = new CuisineListVO();
+            BeanUtils.copyProperties(cuisine, vo);
+            vo.setPrice(cuisine.getTotalPrice());
+
+            // 处理首张图片
+            if (StringUtils.hasText(cuisine.getImages())) {
+                String[] images = cuisine.getImages().split(",");
+                vo.setFirstImage(images.length > 0 ? images[0] : null);
+            }
+
+            // 设置购物车数量
+            vo.setCartQuantity(finalCartQuantityMap.getOrDefault(cuisine.getId(), 0));
+
+            // 设置月售数量
+            vo.setMonthlySales(finalMonthlySalesMap.getOrDefault(cuisine.getId(), 0));
+
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取菜品月售数量(当月1日至当前日期)
+     */
+    private Integer getMonthlySales(Integer cuisineId) {
+        try {
+            // 计算当月1日
+            Calendar calendar = Calendar.getInstance();
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date monthStart = calendar.getTime();
+
+            // 查询订单明细中该菜品的销售数量
+            LambdaQueryWrapper<StoreOrderDetail> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreOrderDetail::getCuisineId, cuisineId);
+            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+            wrapper.ge(StoreOrderDetail::getCreatedTime, monthStart);
+            // 只统计已支付的订单
+            wrapper.inSql(StoreOrderDetail::getOrderId,
+                    "SELECT id FROM store_order WHERE pay_status = 1 AND delete_flag = 0");
+
+            List<StoreOrderDetail> details = storeOrderDetailMapper.selectList(wrapper);
+            return details.stream().mapToInt(StoreOrderDetail::getQuantity).sum();
+        } catch (Exception e) {
+            log.error("获取月售数量失败, cuisineId={}", cuisineId, e);
+            return 0;
+        }
+    }
+
+    @Override
+    public shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId) {
+        log.info("获取订单结算确认页面信息, orderId={}, userId={}", orderId, userId);
+
+        // 查询订单
+        shop.alien.entity.store.StoreOrder order = storeOrderService.getOrderById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        // 查询订单明细
+        LambdaQueryWrapper<shop.alien.entity.store.StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByDesc(shop.alien.entity.store.StoreOrderDetail::getCreatedTime);
+        List<shop.alien.entity.store.StoreOrderDetail> details = storeOrderDetailMapper.selectList(detailWrapper);
+
+        // 批量查询菜品标签
+        Set<Integer> cuisineIds = details.stream()
+                .map(shop.alien.entity.store.StoreOrderDetail::getCuisineId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Integer, String> cuisineIdToTags = new HashMap<>();
+        if (!cuisineIds.isEmpty()) {
+            List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+            if (cuisines != null) {
+                for (StoreCuisine c : cuisines) {
+                    if (c.getTags() != null) {
+                        cuisineIdToTags.put(c.getId(), c.getTags());
+                    }
+                }
+            }
+        }
+
+        // 转换为CartItemDTO(含菜品标签)
+        Map<Integer, String> finalTagsMap = cuisineIdToTags;
+        List<shop.alien.entity.store.dto.CartItemDTO> items = details.stream().map(detail -> {
+            shop.alien.entity.store.dto.CartItemDTO item = new shop.alien.entity.store.dto.CartItemDTO();
+            item.setCuisineId(detail.getCuisineId());
+            item.setCuisineName(detail.getCuisineName());
+            item.setCuisineType(detail.getCuisineType());
+            item.setCuisineImage(detail.getCuisineImage());
+            item.setUnitPrice(detail.getUnitPrice());
+            item.setQuantity(detail.getQuantity());
+            item.setSubtotalAmount(detail.getSubtotalAmount());
+            item.setAddUserId(detail.getAddUserId());
+            item.setAddUserPhone(detail.getAddUserPhone());
+            item.setRemark(detail.getRemark());
+            item.setTags(detail.getCuisineId() != null ? finalTagsMap.get(detail.getCuisineId()) : null);
+            return item;
+        }).collect(Collectors.toList());
+
+        // 查询门店信息
+        shop.alien.entity.store.StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+
+        // 检查结算锁定
+        Integer lockUserId = orderLockService.checkSettlementLock(orderId);
+        boolean isLocked = lockUserId != null && !lockUserId.equals(userId);
+
+        shop.alien.entity.store.vo.OrderSettlementVO vo = new shop.alien.entity.store.vo.OrderSettlementVO();
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setStoreName(storeInfo != null ? storeInfo.getStoreName() : null);
+        vo.setTableNumber(order.getTableNumber());
+        vo.setDinerCount(order.getDinerCount());
+        vo.setContactPhone(order.getContactPhone());
+        vo.setRemark(order.getRemark());
+        vo.setItems(items);
+        vo.setTotalAmount(order.getTotalAmount());
+        vo.setTablewareFee(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO);
+        vo.setCouponId(order.getCouponId());
+        vo.setDiscountAmount(order.getDiscountAmount() != null ? order.getDiscountAmount() : BigDecimal.ZERO);
+        vo.setPayAmount(order.getPayAmount());
+        vo.setIsLocked(isLocked);
+        vo.setLockUserId(lockUserId);
+
+        // 查询优惠券名称
+        if (order.getCouponId() != null) {
+            shop.alien.entity.store.LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            if (coupon != null) {
+                vo.setCouponName(coupon.getName());
+            }
+        }
+
+        return vo;
+    }
+
+    @Override
+    public boolean lockSettlement(Integer orderId, Integer userId) {
+        return orderLockService.lockSettlement(orderId, userId);
+    }
+
+    @Override
+    public void unlockSettlement(Integer orderId, Integer userId) {
+        orderLockService.unlockSettlement(orderId, userId);
+    }
+
+    @Override
+    public Integer checkSettlementLock(Integer orderId) {
+        return orderLockService.checkSettlementLock(orderId);
+    }
+
+    /**
+     * 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
+     *
+     * @param coupon           优惠券对象
+     * @param totalWithTableware 订单总金额(含餐具费)
+     * @return 优惠金额
+     */
+    private BigDecimal calculateDiscountAmount(LifeDiscountCoupon coupon, BigDecimal totalWithTableware) {
+        if (coupon == null || totalWithTableware == null || totalWithTableware.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+
+        Integer couponType = coupon.getCouponType();
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponType != null && couponType == 2) {
+            // 折扣券:根据折扣率计算优惠金额
+            // discountRate: 0-100,例如80表示8折,优惠金额 = 订单金额 * (100 - discountRate) / 100
+            BigDecimal discountRate = coupon.getDiscountRate();
+            if (discountRate != null && discountRate.compareTo(BigDecimal.ZERO) > 0 && discountRate.compareTo(new BigDecimal(100)) <= 0) {
+                // 计算折扣后的金额
+                BigDecimal discountedAmount = totalWithTableware.multiply(discountRate).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
+                // 优惠金额 = 原价 - 折扣后价格
+                discountAmount = totalWithTableware.subtract(discountedAmount);
+            }
+        } else {
+            // 满减券(默认或couponType=1):使用nominalValue
+            discountAmount = coupon.getNominalValue();
+            if (discountAmount == null) {
+                discountAmount = BigDecimal.ZERO;
+            }
+            // 优惠金额不能超过订单总金额
+            if (discountAmount.compareTo(totalWithTableware) > 0) {
+                discountAmount = totalWithTableware;
+            }
+        }
+
+        return discountAmount;
+    }
+
+    /**
+     * 为VO计算优惠金额(用于排序)
+     *
+     * @param vo          优惠券VO
+     * @param couponMap   优惠券详情Map
+     * @param totalAmount 订单总金额
+     * @return 优惠金额
+     */
+    private BigDecimal calculateDiscountAmountForVO(AvailableCouponVO vo, Map<Integer, LifeDiscountCoupon> couponMap, BigDecimal totalAmount) {
+        if (vo == null || totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+
+        LifeDiscountCoupon coupon = couponMap.get(vo.getId());
+        if (coupon == null) {
+            // 如果没有详情,使用nominalValue作为默认值
+            return vo.getNominalValue() != null ? vo.getNominalValue() : BigDecimal.ZERO;
+        }
+
+        return calculateDiscountAmount(coupon, totalAmount);
+    }
+}

+ 697 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DiningUserServiceImpl.java

@@ -0,0 +1,697 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.dto.ChangePhoneDto;
+import shop.alien.store.dto.UserProfileUpdateDto;
+import shop.alien.store.feign.AlienStoreFeign;
+import shop.alien.store.service.DiningUserService;
+import shop.alien.store.util.TokenUtil;
+import shop.alien.store.util.WeChatMiniProgramUtil;
+import shop.alien.store.util.WeChatMiniProgramUtil.WeChatSessionInfo;
+import shop.alien.entity.result.R;
+import shop.alien.store.vo.DiningUserVo;
+import shop.alien.store.vo.TokenVerifyVo;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.util.common.JwtUtil;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureException;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 点餐用户服务实现类
+ * 标准流程:通过 wx.login() 获取 code,调用 jscode2session 获取 openid
+ *
+ * @author ssk
+ * @version 2.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningUserServiceImpl implements DiningUserService {
+
+    private final LifeUserMapper lifeUserMapper;
+    private final BaseRedisService baseRedisService;
+    private final WeChatMiniProgramUtil weChatMiniProgramUtil;
+    private final AlienStoreFeign alienStoreFeign;
+
+    @Value("${jwt.expiration-time}")
+    private String effectiveTime;
+
+    /**
+     * Redis key 常量(小程序专用,避免与 APP 端冲突)
+     */
+    private static final String REDIS_KEY_OPENID_PREFIX = "wechat:openid:";
+    private static final String REDIS_KEY_TOKEN_PREFIX = "miniprogram_user_token:";
+    private static final String REDIS_KEY_USER_PHONE_PREFIX = "miniprogram_user_";
+    private static final long OPENID_MAPPING_EXPIRE_SECONDS = 30 * 24 * 60 * 60L; // 30天
+    private static final long TOKEN_EXPIRE_SECONDS = 7 * 24 * 60 * 60L; // 7天
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo wechatLogin(String code, String phoneCode, String macIp) {
+        // 1. 通过 code2session 获取 openid 和 session_key
+        String openid = getOpenidFromCode(code);
+        if (StringUtils.isBlank(openid)) {
+            return null;
+        }
+
+        // 2. 如果提供了 phoneCode,先解析手机号(用于返回给前端和更新用户)
+        String parsedPhone = null;
+        if (StringUtils.isNotBlank(phoneCode)) {
+            parsedPhone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
+            if (StringUtils.isNotBlank(parsedPhone)) {
+                log.info("成功解析手机号: {}", maskString(parsedPhone, 7));
+            } else {
+                log.warn("解析手机号失败,phoneCode可能已过期或无效");
+            }
+        }
+        if (StringUtils.isBlank(parsedPhone)) {
+            log.info("微信登录未传手机号或解析失败: openid={}, phoneCode={}", maskString(openid, 8), StringUtils.isNotBlank(phoneCode) ? "已传" : "未传");
+        }
+
+        // 3. 查找或创建用户(传入解析后的手机号,避免重复解析)
+        LifeUser user = findOrCreateUser(openid, parsedPhone);
+        if (user == null) {
+            return null;
+        }
+
+        // 4. 检查用户状态(提前检查,避免不必要的操作)
+        if (!isUserValid(user)) {
+            return null;
+        }
+
+        // 5. 生成并存储 token(传入本次解析的手机号,确保有手机号时一定写入 miniprogram_user_{phone})
+        String token = generateAndStoreToken(openid, user, parsedPhone);
+
+        // 6. 构建返回对象,优先使用解析后的手机号返回给前端
+        return buildDiningUserVo(user, token, openid, parsedPhone);
+    }
+
+    /**
+     * 通过 code 获取 openid
+     */
+    private String getOpenidFromCode(String code) {
+        WeChatSessionInfo sessionInfo = weChatMiniProgramUtil.code2Session(code);
+        if (sessionInfo == null || sessionInfo.getErrcode() != null) {
+            Integer errcode = sessionInfo != null ? sessionInfo.getErrcode() : null;
+            String errmsg = sessionInfo != null ? sessionInfo.getErrmsg() : "调用微信接口失败";
+            log.error("登录失败:code2session 失败, errcode={}, errmsg={}", errcode, errmsg);
+            return null;
+        }
+
+        String openid = sessionInfo.getOpenid();
+        if (StringUtils.isBlank(openid)) {
+            log.error("登录失败:无法获取 openid");
+            return null;
+        }
+        log.info("成功获取 openid: {}", maskString(openid, 8));
+        return openid;
+    }
+
+    /**
+     * 查找或创建用户
+     * 规则:只要 user_phone 存在,就代表存在账号,不创建新账号
+     * 
+     * @param openid 微信OpenID
+     * @param phone 解析后的手机号(如果为null,说明没有提供phoneCode或解析失败)
+     */
+    private LifeUser findOrCreateUser(String openid, String phone) {
+        LifeUser user = null;
+        
+        // 1. 如果提供了手机号,优先通过手机号查找(只要手机号存在就代表账号存在)
+        if (StringUtils.isNotBlank(phone)) {
+            user = findUserByPhone(phone);
+            if (user != null) {
+                // 账号已存在,建立 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;
+            }
+        }
+        
+        // 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(phone) && 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;
+        }
+
+        // 4. 如果都找不到,且未提供手机号(或手机号不存在),才创建新账号
+        // 注意:如果提供了手机号但找不到,说明这个手机号没有注册过,可以创建新账号
+        user = createNewUser(openid, phone);
+        if (user != null) {
+            saveOpenidMapping(openid, user.getId());
+        }
+
+        return user;
+    }
+
+    /**
+     * 通过 openid 查找用户
+     */
+    private LifeUser findUserByOpenid(String openid) {
+        String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
+        String userIdStr = baseRedisService.getString(openidKey);
+        
+        if (StringUtils.isBlank(userIdStr)) {
+            return null;
+        }
+
+        try {
+            Integer userId = Integer.parseInt(userIdStr);
+            LifeUser user = lifeUserMapper.selectById(userId);
+            if (user != null) {
+                log.info("通过 openid 找到用户: openid={}, userId={}", maskString(openid, 8), userId);
+            }
+            return user;
+        } catch (NumberFormatException e) {
+            log.warn("Redis 中的 userId 格式错误: {}", userIdStr);
+            return null;
+        }
+    }
+
+    /**
+     * 通过手机号查找用户
+     */
+    private LifeUser findUserByPhone(String phone) {
+        LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LifeUser::getUserPhone, phone);
+        LifeUser user = lifeUserMapper.selectOne(queryWrapper);
+        if (user != null) {
+            log.info("通过手机号找到用户: phone={}, userId={}", maskString(phone, 7), user.getId());
+        }
+        return user;
+    }
+
+    /**
+     * 创建新用户
+     */
+    private LifeUser createNewUser(String openid, String phone) {
+        LifeUser user = new LifeUser();
+        if (StringUtils.isNotBlank(phone)) {
+            user.setUserPhone(phone);
+            user.setUserName(phone);
+            user.setRealName(phone);
+        } else {
+            // 没有手机号时,使用 openid 后8位作为临时用户名
+            String tempName = "微信用户" + openid.substring(Math.max(0, openid.length() - 8));
+            user.setUserName(tempName);
+            user.setRealName(tempName);
+        }
+        user.setCreatedTime(new Date());
+        
+        int ret = lifeUserMapper.insert(user);
+        if (ret != 1) {
+            log.error("创建用户失败");
+            return null;
+        }
+        
+        // MyBatis-Plus 的 insert 会自动填充 ID,不需要重新查询
+        log.info("创建新用户: openid={}, userId={}, phone={}", maskString(openid, 8), user.getId(), maskString(phone, 7));
+        return user;
+    }
+
+    /**
+     * 更新用户手机号
+     */
+    private void updateUserPhone(LifeUser user, String phone) {
+        user.setUserPhone(phone);
+        if (StringUtils.isBlank(user.getUserName()) || user.getUserName().startsWith("微信用户")) {
+            user.setUserName(phone);
+        }
+        if (StringUtils.isBlank(user.getRealName()) || user.getRealName().startsWith("微信用户")) {
+            user.setRealName(phone);
+        }
+        user.setUpdatedTime(new Date());
+        lifeUserMapper.updateById(user);
+        log.info("更新用户手机号: userId={}, phone={}", user.getId(), maskString(phone, 7));
+    }
+
+    /**
+     * 保存 openid 和 userId 的映射关系
+     */
+    private void saveOpenidMapping(String openid, Integer userId) {
+        String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
+        baseRedisService.setString(openidKey, userId.toString(), OPENID_MAPPING_EXPIRE_SECONDS);
+    }
+
+    /**
+     * 检查用户状态是否有效
+     */
+    private boolean isUserValid(LifeUser user) {
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("用户已被封禁: userId={}", user.getId());
+            return false;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("用户已注销: userId={}", user.getId());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 生成并存储 token
+     * @param openid 微信 openid
+     * @param user 用户实体(可能尚未持久化手机号)
+     * @param currentLoginPhone 本次登录解析到的手机号(可选),有则优先用其写入 miniprogram_user_{phone},避免只存 openid 不存 phone 的情况
+     */
+    private String generateAndStoreToken(String openid, LifeUser user, String currentLoginPhone) {
+        // 构建 token 信息
+        Map<String, String> tokenMap = buildTokenMap(openid, user);
+        String userName = StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户";
+        String token = generateToken(openid, userName, tokenMap);
+
+        // 存入 Redis(使用 openid 作为 key,始终写入)
+        baseRedisService.setString(REDIS_KEY_TOKEN_PREFIX + openid, token, TOKEN_EXPIRE_SECONDS);
+
+        // 兼容旧版本:有手机号则同时按手机号存一份(优先用本次登录解析到的手机号,否则用用户实体中的手机号)
+        String phoneToStore = StringUtils.isNotBlank(currentLoginPhone) ? currentLoginPhone : user.getUserPhone();
+        if (StringUtils.isNotBlank(phoneToStore)) {
+            baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + phoneToStore, token, TOKEN_EXPIRE_SECONDS);
+        }
+
+        return token;
+    }
+
+    /**
+     * 构建 token 信息 Map
+     */
+    private Map<String, String> buildTokenMap(String openid, LifeUser user) {
+        Map<String, String> tokenMap = new HashMap<>();
+        tokenMap.put("openid", openid);
+        tokenMap.put("phone", StringUtils.isNotBlank(user.getUserPhone()) ? user.getUserPhone() : "");
+        tokenMap.put("userName", StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户");
+        tokenMap.put("userId", user.getId().toString());
+        tokenMap.put("userType", "miniprogram_user");
+        return tokenMap;
+    }
+
+    /**
+     * 构建返回对象
+     */
+    private DiningUserVo buildDiningUserVo(LifeUser user, String token, String openid) {
+        DiningUserVo diningUserVo = new DiningUserVo();
+        diningUserVo.setId(user.getId().longValue());
+        // 优先使用 user 对象中的手机号(如果通过 phoneCode 解析并更新了,这里会是最新的)
+        diningUserVo.setPhone(user.getUserPhone());
+        diningUserVo.setNickName(user.getUserName());
+        diningUserVo.setAvatarUrl(user.getUserImage());
+        diningUserVo.setStatus(0);
+        diningUserVo.setCreatedTime(user.getCreatedTime());
+        diningUserVo.setToken(token);
+        diningUserVo.setOpenId(openid);
+        return diningUserVo;
+    }
+    
+    /**
+     * 构建返回对象(带解析后的手机号)
+     */
+    private DiningUserVo buildDiningUserVo(LifeUser user, String token, String openid, String parsedPhone) {
+        DiningUserVo diningUserVo = buildDiningUserVo(user, token, openid);
+        // 如果解析到了手机号,优先使用解析后的手机号返回给前端
+        if (StringUtils.isNotBlank(parsedPhone)) {
+            diningUserVo.setPhone(parsedPhone);
+        }
+        return diningUserVo;
+    }
+
+    /**
+     * 掩码字符串(用于日志脱敏)
+     */
+    private String maskString(String str, int visibleLength) {
+        if (StringUtils.isBlank(str) || str.length() <= visibleLength) {
+            return "****";
+        }
+        return str.substring(0, Math.min(visibleLength, str.length())) + "****";
+    }
+
+    private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
+        int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
+        String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
+        long effectiveTimeIntLong = 0L;
+        switch (effectiveTimeUnit) {
+            case "s":
+                effectiveTimeIntLong = effectiveTimeInt * 1000L;
+                break;
+            case "m":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 1000L;
+                break;
+            case "h":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 60L * 1000L;
+                break;
+            case "d":
+                effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
+                break;
+            default:
+                effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
+        }
+        return JwtUtil.createJWT("user_" + userId, userName, JSONObject.toJSONString(tokenMap), effectiveTimeIntLong);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo updateProfile(UserProfileUpdateDto dto) {
+        // 1. 查询用户是否存在
+        LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
+        if (user == null) {
+            log.warn("更新个人信息失败:用户不存在, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 2. 获取当前用户ID(用于设置 updatedUserId)
+        Integer currentUserId = TokenUtil.getCurrentUserId();
+        if (currentUserId == null) {
+            currentUserId = dto.getUserId().intValue(); // 如果没有 token,使用被更新的用户ID
+        }
+
+        // 3. 使用 LambdaUpdateWrapper 只更新需要更新的字段,避免更新不应该更新的字段
+        LambdaUpdateWrapper<LifeUser> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(LifeUser::getId, dto.getUserId().intValue());
+
+        // 只更新非空字段
+        if (StringUtils.isNotBlank(dto.getNickName())) {
+            updateWrapper.set(LifeUser::getUserName, dto.getNickName());
+        }
+        if (StringUtils.isNotBlank(dto.getAvatarUrl())) {
+            updateWrapper.set(LifeUser::getUserImage, dto.getAvatarUrl());
+        }
+        if (StringUtils.isNotBlank(dto.getGender())) {
+            updateWrapper.set(LifeUser::getUserSex, dto.getGender());
+        }
+        if (dto.getBirthday() != null) {
+            updateWrapper.set(LifeUser::getUserBirthday, dto.getBirthday());
+        }
+        if (StringUtils.isNotBlank(dto.getRealName())) {
+            updateWrapper.set(LifeUser::getRealName, dto.getRealName());
+        }
+        if (StringUtils.isNotBlank(dto.getProvince())) {
+            updateWrapper.set(LifeUser::getProvince, dto.getProvince());
+        }
+        if (StringUtils.isNotBlank(dto.getCity())) {
+            updateWrapper.set(LifeUser::getCity, dto.getCity());
+        }
+        if (StringUtils.isNotBlank(dto.getDistrict())) {
+            updateWrapper.set(LifeUser::getDistrict, dto.getDistrict());
+        }
+        if (StringUtils.isNotBlank(dto.getAddress())) {
+            updateWrapper.set(LifeUser::getAddress, dto.getAddress());
+        }
+        if (StringUtils.isNotBlank(dto.getJianjie())) {
+            updateWrapper.set(LifeUser::getJianjie, dto.getJianjie());
+        }
+
+        // 设置更新时间和更新人ID
+        updateWrapper.set(LifeUser::getUpdatedTime, new Date());
+        updateWrapper.set(LifeUser::getUpdatedUserId, currentUserId);
+
+        // 4. 执行更新
+        int result = lifeUserMapper.update(null, updateWrapper);
+        if (result != 1) {
+            log.error("更新用户信息失败, userId={}", dto.getUserId());
+            return null;
+        }
+        log.info("用户信息更新成功, userId={}, updatedUserId={}", dto.getUserId(), currentUserId);
+
+        // 5. 重新查询用户信息(因为使用 updateWrapper 后,user 对象不会自动更新)
+        LifeUser updatedUser = lifeUserMapper.selectById(dto.getUserId().intValue());
+        if (updatedUser == null) {
+            log.error("更新后查询用户信息失败, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 6. 返回更新后的用户信息
+        return buildDiningUserVo(updatedUser);
+    }
+
+    @Override
+    public DiningUserVo getUserInfo(Long userId) {
+        // 1. 查询用户
+        LifeUser user = lifeUserMapper.selectById(userId.intValue());
+        if (user == null) {
+            log.warn("获取用户信息失败:用户不存在, userId={}", userId);
+            return null;
+        }
+
+        // 2. 检查用户状态
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("用户已被封禁: userId={}", userId);
+            return null;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("用户已注销: userId={}", userId);
+            return null;
+        }
+
+        // 3. 返回用户信息
+        return buildDiningUserVo(user);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo changePhone(ChangePhoneDto dto) {
+        String newPhone = dto.getNewPhone().trim();
+        String codeStr = dto.getCode().trim();
+
+        // 1. 校验验证码(Feign 调 store ali/checkSmsCode,appType=0 用户端,businessType=3 修改手机号)
+        int codeInt;
+        try {
+            codeInt = Integer.parseInt(codeStr);
+        } catch (NumberFormatException e) {
+            log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
+            return null;
+        }
+        R<?> checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
+        if (!R.isSuccess(checkRes)) {
+            log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
+            return null;
+        }
+
+        // 2. 查询用户
+        LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
+        if (user == null) {
+            log.warn("更换手机号失败:用户不存在, userId={}", dto.getUserId());
+            return null;
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("更换手机号失败:用户已被封禁, userId={}", dto.getUserId());
+            return null;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("更换手机号失败:用户已注销, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 3. 新手机号与当前相同则无需更新
+        if (newPhone.equals(user.getUserPhone())) {
+            log.info("新手机号与当前相同,无需更换, userId={}", dto.getUserId());
+            return buildDiningUserVo(user);
+        }
+
+        // 4. 新手机号是否已被其他用户使用
+        LambdaQueryWrapper<LifeUser> q = new LambdaQueryWrapper<>();
+        q.eq(LifeUser::getUserPhone, newPhone);
+        LifeUser existing = lifeUserMapper.selectOne(q);
+        if (existing != null && !existing.getId().equals(user.getId())) {
+            log.warn("更换手机号失败:新手机号已被其他用户使用, newPhone={}", newPhone);
+            return null;
+        }
+
+        // 5. 更新 user_phone
+        String oldPhone = user.getUserPhone();
+        user.setUserPhone(newPhone);
+        user.setUpdatedTime(new Date());
+        int n = lifeUserMapper.updateById(user);
+        if (n != 1) {
+            log.error("更换手机号失败:更新数据库异常, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 6. 只清除小程序旧手机号对应的 token,APP 的 token 保持不变
+        // 因为小程序更换手机号只影响小程序平台,APP 可以继续使用旧手机号登录
+        baseRedisService.delete(REDIS_KEY_USER_PHONE_PREFIX + oldPhone);
+        log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}(仅清除小程序 token,APP token 保持不变)", dto.getUserId(), oldPhone, newPhone);
+
+        return buildDiningUserVo(user);
+    }
+
+    @Override
+    public TokenVerifyVo verifyToken(String token) {
+        TokenVerifyVo verifyVo = new TokenVerifyVo();
+        verifyVo.setValid(false);
+
+        // 1. 检查 token 是否为空
+        if (StringUtils.isBlank(token)) {
+            verifyVo.setReason("Token 不能为空");
+            return verifyVo;
+        }
+
+        // 去除 "Bearer " 前缀(不区分大小写)
+        token = token.trim();
+        if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
+            token = token.substring(7).trim();
+        }
+        
+        if (StringUtils.isBlank(token)) {
+            verifyVo.setReason("Token 去除前缀后为空");
+            return verifyVo;
+        }
+
+        try {
+            // 2. 解析 token,验证格式和签名
+            Claims claims = JwtUtil.parseJWT(token);
+            
+            // 3. 检查 token 是否过期
+            Date expiration = claims.getExpiration();
+            if (expiration != null && expiration.before(new Date())) {
+                verifyVo.setReason("Token 已过期");
+                verifyVo.setExpirationTime(expiration);
+                return verifyVo;
+            }
+
+            // 4. 从 token 中提取用户信息
+            String sub = claims.get("sub").toString();
+            JSONObject tokenInfo = JSONObject.parseObject(sub);
+            String openid = tokenInfo.getString("openid");
+            String phone = tokenInfo.getString("phone");
+            String userIdStr = tokenInfo.getString("userId");
+            String userName = tokenInfo.getString("userName");
+
+            // 5. 验证 Redis 中是否存在该 token(使用小程序专用的 key 前缀)
+            boolean tokenExists = false;
+            if (StringUtils.isNotBlank(openid)) {
+                // 优先使用 openid 查找
+                String redisToken = baseRedisService.getString(REDIS_KEY_TOKEN_PREFIX + openid);
+                if (token.equals(redisToken)) {
+                    tokenExists = true;
+                }
+            }
+            
+            // 兼容旧版本:通过手机号查找
+            if (!tokenExists && StringUtils.isNotBlank(phone)) {
+                String redisToken = baseRedisService.getString(REDIS_KEY_USER_PHONE_PREFIX + phone);
+                if (token.equals(redisToken)) {
+                    tokenExists = true;
+                }
+            }
+
+            if (!tokenExists) {
+                verifyVo.setReason("Token 不存在或已失效(可能已退出登录)");
+                return verifyVo;
+            }
+
+            // 6. 验证用户状态(如果提供了 userId)
+            if (StringUtils.isNotBlank(userIdStr)) {
+                try {
+                    Integer userId = Integer.parseInt(userIdStr);
+                    LifeUser user = lifeUserMapper.selectById(userId);
+                    if (user == null) {
+                        verifyVo.setReason("用户不存在");
+                        return verifyVo;
+                    }
+                    if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+                        verifyVo.setReason("用户已被封禁");
+                        return verifyVo;
+                    }
+                    if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+                        verifyVo.setReason("用户已注销");
+                        return verifyVo;
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("Token 中的 userId 格式错误: {}", userIdStr);
+                }
+            }
+
+            // 7. Token 验证通过,返回用户信息
+            verifyVo.setValid(true);
+            if (StringUtils.isNotBlank(userIdStr)) {
+                try {
+                    verifyVo.setUserId(Long.parseLong(userIdStr));
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析 userId: {}", userIdStr);
+                }
+            }
+            verifyVo.setPhone(phone);
+            verifyVo.setNickName(userName);
+            verifyVo.setOpenid(openid);
+            verifyVo.setExpirationTime(expiration);
+
+            log.info("Token 验证成功: userId={}, openid={}, phone={}", userIdStr, openid, phone);
+            return verifyVo;
+
+        } catch (ExpiredJwtException e) {
+            log.warn("Token 已过期: {}", e.getMessage());
+            verifyVo.setReason("Token 已过期: " + e.getMessage());
+            return verifyVo;
+        } catch (MalformedJwtException e) {
+            log.warn("Token 格式错误: {}", e.getMessage());
+            verifyVo.setReason("Token 格式错误: " + e.getMessage());
+            return verifyVo;
+        } catch (SignatureException e) {
+            log.warn("Token 签名验证失败: {}", e.getMessage());
+            verifyVo.setReason("Token 签名验证失败: " + e.getMessage());
+            return verifyVo;
+        } catch (Exception e) {
+            log.error("Token 验证异常: {}", e.getMessage(), e);
+            verifyVo.setReason("Token 验证异常: " + e.getMessage());
+            return verifyVo;
+        }
+    }
+
+    /**
+     * 构建 DiningUserVo
+     */
+    private DiningUserVo buildDiningUserVo(LifeUser user) {
+        DiningUserVo diningUserVo = new DiningUserVo();
+        diningUserVo.setId(user.getId().longValue());
+        diningUserVo.setPhone(user.getUserPhone());
+        diningUserVo.setNickName(user.getUserName());
+        diningUserVo.setAvatarUrl(user.getUserImage());
+        diningUserVo.setStatus(0);
+        diningUserVo.setCreatedTime(user.getCreatedTime());
+        // 补充更多字段
+        diningUserVo.setGender(user.getUserSex());
+        diningUserVo.setBirthday(user.getUserBirthday());
+        diningUserVo.setRealName(user.getRealName());
+        diningUserVo.setProvince(user.getProvince());
+        diningUserVo.setCity(user.getCity());
+        diningUserVo.setDistrict(user.getDistrict());
+        diningUserVo.setAddress(user.getAddress());
+        diningUserVo.setJianjie(user.getJianjie());
+        return diningUserVo;
+    }
+}

+ 118 - 0
alien-store/src/main/java/shop/alien/store/service/impl/OrderLockServiceImpl.java

@@ -0,0 +1,118 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.OrderLockService;
+
+/**
+ * 订单锁定服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OrderLockServiceImpl implements OrderLockService {
+
+    private static final String ORDER_LOCK_KEY_PREFIX = "order:lock:table:";
+    private static final String SETTLEMENT_LOCK_KEY_PREFIX = "settlement:lock:order:";
+    private static final int ORDER_LOCK_EXPIRE_SECONDS = 300; // 5分钟过期
+
+    private final BaseRedisService baseRedisService;
+
+    @Override
+    public boolean lockOrder(Integer tableId, Integer userId) {
+        log.info("锁定订单, tableId={}, userId={}", tableId, userId);
+
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock)) {
+            // 已锁定,检查是否是当前用户
+            if (existingLock.equals(String.valueOf(userId))) {
+                // 刷新过期时间
+                baseRedisService.setString(lockKey, existingLock, (long) ORDER_LOCK_EXPIRE_SECONDS);
+                return true;
+            } else {
+                // 被其他用户锁定
+                return false;
+            }
+        }
+
+        // 设置锁定
+        baseRedisService.setString(lockKey, String.valueOf(userId), (long) ORDER_LOCK_EXPIRE_SECONDS);
+        return true;
+    }
+
+    @Override
+    public void unlockOrder(Integer tableId, Integer userId) {
+        log.info("解锁订单, tableId={}, userId={}", tableId, userId);
+
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock) && existingLock.equals(String.valueOf(userId))) {
+            baseRedisService.delete(lockKey);
+        }
+    }
+
+    @Override
+    public Integer checkOrderLock(Integer tableId) {
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String lockUserId = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(lockUserId)) {
+            try {
+                return Integer.parseInt(lockUserId);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean lockSettlement(Integer orderId, Integer userId) {
+        log.info("锁定订单结算, orderId={}, userId={}", orderId, userId);
+
+        String lockKey = SETTLEMENT_LOCK_KEY_PREFIX + orderId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock)) {
+            if (existingLock.equals(String.valueOf(userId))) {
+                baseRedisService.setString(lockKey, existingLock, (long) ORDER_LOCK_EXPIRE_SECONDS);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        baseRedisService.setString(lockKey, String.valueOf(userId), (long) ORDER_LOCK_EXPIRE_SECONDS);
+        return true;
+    }
+
+    @Override
+    public void unlockSettlement(Integer orderId, Integer userId) {
+        log.info("解锁订单结算, orderId={}, userId={}", orderId, userId);
+
+        String lockKey = SETTLEMENT_LOCK_KEY_PREFIX + orderId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock) && existingLock.equals(String.valueOf(userId))) {
+            baseRedisService.delete(lockKey);
+        }
+    }
+
+    @Override
+    public Integer checkSettlementLock(Integer orderId) {
+        String lockKey = SETTLEMENT_LOCK_KEY_PREFIX + orderId;
+        String lockUserId = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(lockUserId)) {
+            try {
+                return Integer.parseInt(lockUserId);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+}

+ 239 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ScanOrderStoreInfoServiceImpl.java

@@ -0,0 +1,239 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
+import shop.alien.entity.store.vo.CategoryWithCuisinesVO;
+import shop.alien.mapper.StoreCuisineCategoryMapper;
+import shop.alien.mapper.StoreCuisineMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.store.service.ScanOrderStoreInfoService;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 门店信息查询服务实现类
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ScanOrderStoreInfoServiceImpl implements ScanOrderStoreInfoService {
+
+    private final StoreTableMapper storeTableMapper;
+    private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Override
+    public List<StoreTable> getTablesByStoreId(Integer storeId) {
+        log.info("根据门店ID查询桌号列表, storeId={}", storeId);
+        
+        LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreTable::getStoreId, storeId);
+        wrapper.eq(StoreTable::getDeleteFlag, 0);
+        wrapper.orderByAsc(StoreTable::getTableNumber);
+        
+        return storeTableMapper.selectList(wrapper);
+    }
+
+    @Override
+    public List<StoreCuisineCategory> getCategoriesByStoreId(Integer storeId) {
+        log.info("根据门店ID查询菜品种类列表, storeId={}", storeId);
+        
+        LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisineCategory::getStoreId, storeId);
+        wrapper.eq(StoreCuisineCategory::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisineCategory::getStatus, 1); // 只查询启用的分类
+        wrapper.orderByAsc(StoreCuisineCategory::getSort); // 按排序字段排序
+        
+        return storeCuisineCategoryMapper.selectList(wrapper);
+    }
+
+    @Override
+    public List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId) {
+        log.info("根据菜品种类ID查询菜品信息列表, categoryId={}", categoryId);
+        
+        // 先查询分类信息,获取 storeId
+        StoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
+        if (category == null) {
+            log.warn("菜品种类不存在, categoryId={}", categoryId);
+            return new java.util.ArrayList<>();
+        }
+        
+        // 查询该门店下所有上架的菜品(与 getCategoriesWithCuisinesByStoreId 中 cuisines 查询条件、排序一致)
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, category.getStoreId());
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的菜品
+        wrapper.orderByAsc(StoreCuisine::getId); // 与 categories-with-cuisines 中 cuisines 顺序一致
+        
+        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
+        
+        // 过滤出包含该分类ID的菜品
+        // categoryIds 是 JSON 数组格式,如:[1,2,3]
+        return allCuisines.stream()
+                .filter(cuisine -> {
+                    String categoryIdsStr = cuisine.getCategoryIds();
+                    if (StringUtils.isBlank(categoryIdsStr)) {
+                        return false;
+                    }
+                    try {
+                        // 解析 JSON 数组
+                        List<Integer> categoryIds = JSON.parseArray(categoryIdsStr, Integer.class);
+                        return categoryIds != null && categoryIds.contains(categoryId);
+                    } catch (Exception e) {
+                        log.warn("解析菜品分类IDs失败, cuisineId={}, categoryIds={}, error={}", 
+                                cuisine.getId(), categoryIdsStr, e.getMessage());
+                        // 如果解析失败,使用简单的字符串匹配作为降级方案
+                        return categoryIdsStr.contains(String.valueOf(categoryId));
+                    }
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<CategoryWithCuisinesVO> getCategoriesWithCuisinesByStoreId(Integer storeId, String keyword) {
+        log.info("根据门店ID查询菜品种类及下属菜品, storeId={}, keyword={}", storeId, keyword);
+        List<StoreCuisineCategory> categories = getCategoriesByStoreId(storeId);
+        if (categories == null || categories.isEmpty()) {
+            return new ArrayList<>();
+        }
+        // 与 getCuisinesByCategoryId 相同的查询条件与排序,保证 cuisines 字段与 /store/info/cuisines 一致
+        LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
+        cuisineWrapper.eq(StoreCuisine::getStoreId, storeId);
+        cuisineWrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        cuisineWrapper.eq(StoreCuisine::getShelfStatus, 1);
+        cuisineWrapper.orderByAsc(StoreCuisine::getId);
+        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(cuisineWrapper);
+        if (allCuisines == null) {
+            allCuisines = new ArrayList<>();
+        }
+        boolean filterByName = StringUtils.isNotBlank(keyword);
+        String keywordLower = filterByName ? keyword.trim().toLowerCase() : null;
+        List<CategoryWithCuisinesVO> result = new ArrayList<>();
+        for (StoreCuisineCategory category : categories) {
+            Integer categoryId = category.getId();
+            List<StoreCuisine> cuisines = allCuisines.stream()
+                    .filter(c -> belongsToCategory(c, categoryId))
+                    .filter(c -> !filterByName || (c.getName() != null && c.getName().toLowerCase().contains(keywordLower)))
+                    .collect(Collectors.toList());
+            result.add(new CategoryWithCuisinesVO(category, cuisines));
+        }
+        return result;
+    }
+
+    private boolean belongsToCategory(StoreCuisine cuisine, Integer categoryId) {
+        String categoryIdsStr = cuisine.getCategoryIds();
+        if (StringUtils.isBlank(categoryIdsStr)) {
+            return false;
+        }
+        try {
+            List<Integer> categoryIds = JSON.parseArray(categoryIdsStr, Integer.class);
+            return categoryIds != null && categoryIds.contains(categoryId);
+        } catch (Exception e) {
+            return categoryIdsStr.contains(String.valueOf(categoryId));
+        }
+    }
+
+    @Override
+    public boolean deleteCategory(Integer categoryId) {
+        log.info("删除菜品种类(仅解除绑定+逻辑删分类), categoryId={}", categoryId);
+        if (categoryId == null) {
+            log.warn("删除菜品种类失败:分类ID为空");
+            return false;
+        }
+        StoreCuisineCategory category = storeCuisineCategoryMapper.selectById(categoryId);
+        if (category == null) {
+            log.warn("删除菜品种类失败:分类不存在, categoryId={}", categoryId);
+            return false;
+        }
+        Integer storeId = category.getStoreId();
+        // 1. 解除绑定:将该分类ID从所有关联菜品的 category_ids 中移除,价目表菜品其它字段不变
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        List<StoreCuisine> allCuisines = storeCuisineMapper.selectList(wrapper);
+        if (allCuisines != null) {
+            for (StoreCuisine cuisine : allCuisines) {
+                if (!belongsToCategory(cuisine, categoryId)) {
+                    continue;
+                }
+                String newCategoryIds = removeCategoryIdFromJson(cuisine.getCategoryIds(), categoryId);
+                cuisine.setCategoryIds(newCategoryIds);
+                storeCuisineMapper.updateById(cuisine);
+            }
+        }
+        // 2. 逻辑删除菜品种类
+        storeCuisineCategoryMapper.deleteById(categoryId);
+        log.info("删除菜品种类完成, categoryId={}", categoryId);
+        return true;
+    }
+
+    /** 从 categoryIds 的 JSON 数组中移除指定 categoryId,返回新 JSON 字符串;若为空则返回 null */
+    private String removeCategoryIdFromJson(String categoryIdsStr, Integer categoryId) {
+        if (StringUtils.isBlank(categoryIdsStr)) {
+            return null;
+        }
+        try {
+            List<Integer> list = JSON.parseArray(categoryIdsStr, Integer.class);
+            if (list == null || list.isEmpty()) {
+                return null;
+            }
+            list.remove(categoryId);
+            if (list.isEmpty()) {
+                return null;
+            }
+            return JSON.toJSONString(list);
+        } catch (Exception e) {
+            log.warn("解析 categoryIds 失败, categoryIdsStr={}, 原样返回", categoryIdsStr, e);
+            return categoryIdsStr;
+        }
+    }
+
+    @Override
+    public StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId) {
+        log.info("根据商铺ID查询店铺信息和首页展示美食价目表, storeId={}", storeId);
+        
+        // 1. 查询店铺信息
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            throw new RuntimeException("店铺不存在");
+        }
+        
+        // 2. 查询首页展示的美食价目表(is_homepage_display = 1,上架状态 = 1,审核通过 = 1)
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getIsHomepageDisplay, 1); // 首页展示
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 上架状态
+        wrapper.eq(StoreCuisine::getStatus, 1); // 审核通过
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime); // 按创建时间倒序
+        
+        List<StoreCuisine> homepageCuisines = storeCuisineMapper.selectList(wrapper);
+        if (homepageCuisines == null) {
+            homepageCuisines = new ArrayList<>();
+        }
+        
+        // 3. 构建返回DTO
+        StoreInfoWithHomepageCuisinesDTO dto = new StoreInfoWithHomepageCuisinesDTO();
+        dto.setStoreInfo(storeInfo);
+        dto.setHomepageCuisines(homepageCuisines);
+        
+        log.info("查询完成, storeId={}, 首页展示美食数量={}", storeId, homepageCuisines.size());
+        return dto;
+    }
+}

+ 180 - 0
alien-store/src/main/java/shop/alien/store/service/impl/SseServiceImpl.java

@@ -0,0 +1,180 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * SSE推送服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+public class SseServiceImpl implements shop.alien.store.service.SseService {
+
+    // 存储每个桌号的SSE连接,一个桌号可以有多个连接(多个用户)
+    private final ConcurrentHashMap<Integer, ConcurrentHashMap<String, SseEmitter>> connections = new ConcurrentHashMap<>();
+
+    // 定时任务执行器,用于发送心跳
+    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
+
+    @Override
+    public SseEmitter createConnection(Integer tableId) {
+        log.info("创建SSE连接, tableId={}", tableId);
+
+        // 创建SSE连接,设置超时时间为30分钟
+        SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
+
+        // 生成连接ID
+        String connectionId = "conn_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId();
+
+        // 存储连接
+        connections.computeIfAbsent(tableId, k -> new ConcurrentHashMap<>()).put(connectionId, emitter);
+
+        // 设置完成和超时回调
+        emitter.onCompletion(() -> {
+            log.info("SSE连接完成, tableId={}, connectionId={}", tableId, connectionId);
+            removeConnection(tableId, connectionId);
+        });
+
+        emitter.onTimeout(() -> {
+            log.info("SSE连接超时, tableId={}, connectionId={}", tableId, connectionId);
+            removeConnection(tableId, connectionId);
+        });
+
+        emitter.onError((ex) -> {
+            if (isClientDisconnect(ex)) {
+                log.info("SSE客户端已断开, tableId={}, connectionId={}", tableId, connectionId);
+            } else {
+                log.error("SSE连接错误, tableId={}, connectionId={}, error={}", tableId, connectionId, ex.getMessage(), ex);
+            }
+            removeConnection(tableId, connectionId);
+        });
+
+        // 发送初始连接成功消息
+        try {
+            emitter.send(SseEmitter.event()
+                    .name("connected")
+                    .data("连接成功"));
+        } catch (IOException e) {
+            log.error("发送初始消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            removeConnection(tableId, connectionId);
+            return null;
+        }
+
+        // 启动心跳任务
+        startHeartbeat(tableId, connectionId, emitter);
+
+        return emitter;
+    }
+
+    @Override
+    public void pushCartUpdate(Integer tableId, Object message) {
+        log.info("推送购物车更新, tableId={}", tableId);
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections == null || tableConnections.isEmpty()) {
+            log.warn("该桌号没有SSE连接, tableId={}", tableId);
+            return;
+        }
+
+        String messageJson = JSON.toJSONString(message);
+        tableConnections.forEach((connectionId, emitter) -> {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("cart_update")
+                        .data(messageJson));
+                log.info("推送购物车更新成功, tableId={}, connectionId={}", tableId, connectionId);
+            } catch (IOException e) {
+                if (isClientDisconnect(e)) {
+                    log.debug("推送时客户端已断开, tableId={}, connectionId={}", tableId, connectionId);
+                } else {
+                    log.error("推送购物车更新失败, tableId={}, connectionId={}, error={}", tableId, connectionId, e.getMessage(), e);
+                }
+                removeConnection(tableId, connectionId);
+            }
+        });
+    }
+
+    @Override
+    public void closeConnection(Integer tableId) {
+        log.info("关闭SSE连接, tableId={}", tableId);
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections != null) {
+            tableConnections.forEach((connectionId, emitter) -> {
+                try {
+                    emitter.complete();
+                } catch (Exception e) {
+                    log.error("关闭SSE连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            });
+            connections.remove(tableId);
+        }
+    }
+
+    /**
+     * 判断是否为客户端主动断开(Broken pipe、Connection reset 等),此类情况属正常,无需打 ERROR。
+     */
+    private boolean isClientDisconnect(Throwable ex) {
+        if (ex == null) return false;
+        String msg = ex.getMessage();
+        if (msg != null) {
+            String lower = msg.toLowerCase();
+            if (lower.contains("broken pipe") || lower.contains("connection reset")
+                    || lower.contains("connection closed") || lower.contains("an established connection was aborted")) {
+                return true;
+            }
+        }
+        return isClientDisconnect(ex.getCause());
+    }
+
+    /**
+     * 移除连接
+     */
+    private void removeConnection(Integer tableId, String connectionId) {
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections != null) {
+            SseEmitter emitter = tableConnections.remove(connectionId);
+            if (emitter != null) {
+                try {
+                    emitter.complete();
+                } catch (Exception e) {
+                    log.error("完成SSE连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            }
+            if (tableConnections.isEmpty()) {
+                connections.remove(tableId);
+            }
+        }
+    }
+
+    /**
+     * 启动心跳任务
+     */
+    private void startHeartbeat(Integer tableId, String connectionId, SseEmitter emitter) {
+        scheduler.scheduleAtFixedRate(() -> {
+            try {
+                if (emitter != null) {
+                    emitter.send(SseEmitter.event()
+                            .name("heartbeat")
+                            .data("ping"));
+                }
+            } catch (IOException e) {
+                if (isClientDisconnect(e)) {
+                    log.debug("心跳时客户端已断开, tableId={}, connectionId={}", tableId, connectionId);
+                } else {
+                    log.error("发送心跳失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+                removeConnection(tableId, connectionId);
+            }
+        }, 30, 30, TimeUnit.SECONDS); // 每30秒发送一次心跳
+    }
+}

+ 2019 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreOrderServiceImpl.java

@@ -0,0 +1,2019 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.StoreOrderService;
+import shop.alien.store.util.TokenUtil;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderChangeLogBatchVO;
+import shop.alien.entity.store.vo.OrderChangeLogItemVO;
+import shop.alien.entity.store.vo.OrderInfoVO;
+import shop.alien.entity.store.vo.StoreOrderPageVO;
+import shop.alien.entity.store.vo.OrderCuisineItemVO;
+import shop.alien.entity.store.vo.OrderDetailWithChangeLogVO;
+import shop.alien.mapper.*;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 订单服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOrder> implements StoreOrderService {
+
+
+    private final StoreOrderDetailMapper orderDetailMapper;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreTableLogMapper storeTableLogMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+    private final CartService cartService;
+    private final StoreOrderLockMapper storeOrderLockMapper;
+    private final StoreCouponUsageMapper storeCouponUsageMapper;
+    private final StoreCartMapper storeCartMapper;
+    private final BaseRedisService baseRedisService;
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreOrderChangeLogMapper orderChangeLogMapper;
+    private final shop.alien.store.service.SseService sseService;
+    private final shop.alien.store.service.OrderLockService orderLockService;
+
+    @Override
+    public StoreOrder createOrder(CreateOrderDTO dto) {
+        log.info("创建订单, dto={}", dto);
+
+        // 获取当前用户信息
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
+        String userPhone = (String) userInfo[1];
+
+        // 检查订单锁定状态
+        Integer lockUserId = orderLockService.checkOrderLock(dto.getTableId());
+        if (lockUserId != null && !lockUserId.equals(userId)) {
+            throw new RuntimeException("订单已被其他用户锁定,无法下单");
+        }
+
+        // 验证桌号
+        StoreTable table = storeTableMapper.selectById(dto.getTableId());
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        // 查询门店信息(用于获取餐具费)
+        StoreInfo storeInfo = storeInfoMapper.selectById(table.getStoreId());
+        if (storeInfo == null) {
+            throw new RuntimeException("门店不存在");
+        }
+
+        // 获取购物车
+        CartDTO cart = cartService.getCart(dto.getTableId());
+        if (cart.getItems() == null || cart.getItems().isEmpty()) {
+            throw new RuntimeException("购物车为空");
+        }
+        
+        // 检查是否有新增商品或商品数量增加
+        boolean hasNewItems = false; // 是否有新增商品(未下单过的商品)
+        boolean hasQuantityIncrease = false; // 是否有商品数量增加
+        
+        for (shop.alien.entity.store.dto.CartItemDTO item : cart.getItems()) {
+            Integer lockedQuantity = item.getLockedQuantity();
+            Integer currentQuantity = item.getQuantity();
+            
+            if (lockedQuantity == null || lockedQuantity == 0) {
+                // 有新增商品(未下单过的商品)
+                hasNewItems = true;
+                log.debug("发现新增商品, cuisineId={}, quantity={}", item.getCuisineId(), currentQuantity);
+            } else if (currentQuantity != null && currentQuantity > lockedQuantity) {
+                // 有商品数量增加(当前数量大于已下单数量)
+                hasQuantityIncrease = true;
+                log.debug("发现商品数量增加, cuisineId={}, currentQuantity={}, orderedQuantity={}", 
+                        item.getCuisineId(), currentQuantity, lockedQuantity);
+            }
+        }
+        
+        // 如果没有新增商品且没有商品数量增加,不允许创建/更新订单(下单后只能增不能减,不支持减量同步)
+        if (!hasNewItems && !hasQuantityIncrease) {
+            throw new RuntimeException("购物车中没有新增商品或商品数量未增加,无法创建订单");
+        }
+
+        // 直接使用前端传入的值
+        BigDecimal totalAmount = dto.getTotalAmount() != null ? dto.getTotalAmount() : BigDecimal.ZERO;
+        BigDecimal tablewareFee = dto.getTablewareFee() != null ? dto.getTablewareFee() : BigDecimal.ZERO;
+        BigDecimal discountAmount = dto.getDiscountAmount() != null ? dto.getDiscountAmount() : BigDecimal.ZERO;
+        BigDecimal payAmount = dto.getPayAmount() != null ? dto.getPayAmount() : BigDecimal.ZERO;
+        
+        // 优惠券仅校验是否存在及是否属于该门店,不做金额校验;订单总金额、餐具费、优惠金额、实付金额完全采用前端传参
+        LifeDiscountCoupon coupon = null;
+        
+        if (dto.getCouponId() != null) {
+            // 检查桌号是否已使用优惠券,如果已使用则替换为新优惠券
+            if (cartService.hasUsedCoupon(dto.getTableId())) {
+                // 获取旧的优惠券使用记录
+                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());
+            }
+
+            // 验证优惠券(只查询一次)
+            coupon = lifeDiscountCouponMapper.selectById(dto.getCouponId());
+            if (coupon == null) {
+                throw new RuntimeException("优惠券不存在");
+            }
+
+            // 验证优惠券是否属于该门店(空安全:coupon.storeId 为 String,table.storeId 为 Integer)
+            String couponStoreId = coupon.getStoreId();
+            Integer tableStoreId = table.getStoreId();
+            if (couponStoreId == null || tableStoreId == null
+                    || !couponStoreId.equals(String.valueOf(tableStoreId))) {
+                throw new RuntimeException("优惠券不属于该门店");
+            }
+
+            // 标记桌号已使用新优惠券
+            cartService.markCouponUsed(dto.getTableId(), dto.getCouponId());
+        } else {
+            // 直接使用前端传入的值,如果为null则设为0
+            if (discountAmount == null) {
+                discountAmount = BigDecimal.ZERO;
+            }
+        }
+
+        Date now = new Date();
+        StoreOrder order = null;
+        String orderNo = null;
+        boolean isUpdate = false; // 是否是更新订单
+        
+        // 检查桌号是否已绑定订单
+        if (table.getCurrentOrderId() != null) {
+            // 查询已存在的订单
+            StoreOrder existingOrder = this.getById(table.getCurrentOrderId());
+            if (existingOrder != null && existingOrder.getOrderStatus() == 0 && existingOrder.getPayStatus() == 0) {
+                // 订单存在且是待支付状态,更新订单
+                isUpdate = true;
+                order = existingOrder;
+                orderNo = order.getOrderNo(); // 使用原订单号
+                log.info("桌号已绑定订单,更新订单信息, orderId={}, orderNo={}", order.getId(), orderNo);
+                
+                // 更新订单信息(完全采用前端传参,不做金额校验)
+                order.setDinerCount(dto.getDinerCount());
+                order.setContactPhone(dto.getContactPhone());
+                order.setTablewareFee(tablewareFee);
+                order.setTotalAmount(totalAmount);
+                order.setCouponId(dto.getCouponId());
+                order.setCurrentCouponId(dto.getCouponId());
+                order.setDiscountAmount(discountAmount);
+                order.setPayAmount(payAmount);
+                order.setRemark(dto.getRemark());
+                order.setUpdatedUserId(userId);
+                order.setUpdatedTime(now);
+                this.updateById(order);
+                
+                // 逻辑删除旧的订单明细
+                LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+                detailWrapper.eq(StoreOrderDetail::getOrderId, order.getId());
+                detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+                List<StoreOrderDetail> oldDetails = orderDetailMapper.selectList(detailWrapper);
+                if (oldDetails != null && !oldDetails.isEmpty()) {
+                    List<Integer> oldDetailIds = oldDetails.stream()
+                            .map(StoreOrderDetail::getId)
+                            .collect(Collectors.toList());
+                    orderDetailMapper.deleteBatchIds(oldDetailIds);
+                    log.info("删除旧订单明细, orderId={}, count={}", order.getId(), oldDetailIds.size());
+                }
+            } else {
+                log.info("桌号绑定的订单不存在或已支付/已取消,将创建新订单");
+            }
+        }
+        
+        // 如果没有订单需要更新,创建新订单
+        if (!isUpdate) {
+            // 生成订单号
+            orderNo = generateOrderNo();
+            
+            // 创建订单
+            order = new StoreOrder();
+            order.setOrderNo(orderNo);
+            order.setStoreId(table.getStoreId());
+            order.setTableId(table.getId());
+            order.setTableNumber(table.getTableNumber());
+            order.setDinerCount(dto.getDinerCount());
+            order.setContactPhone(dto.getContactPhone());
+            order.setTablewareFee(tablewareFee);
+            order.setPayUserId(userId);
+            order.setPayUserPhone(userPhone);
+            order.setOrderStatus(0); // 待支付
+            order.setTotalAmount(totalAmount);
+            order.setCouponId(dto.getCouponId());
+            order.setCurrentCouponId(dto.getCouponId());
+            order.setDiscountAmount(discountAmount);
+            order.setPayAmount(payAmount);
+            
+            // 如果immediatePay为0,只创建订单不支付;为1则创建订单并支付
+            // 暂时不实现立即支付,由前端调用支付接口
+            // payType在支付时设置
+            order.setPayStatus(0); // 未支付
+            order.setRemark(dto.getRemark());
+            order.setCreatedUserId(userId);
+            order.setUpdatedUserId(userId);
+            // 手动设置创建时间和更新时间(临时方案,避免自动填充未生效导致 created_time 为 null)
+            order.setCreatedTime(now);
+            order.setUpdatedTime(now);
+            this.save(order);
+            log.info("创建新订单, orderId={}, orderNo={}", order.getId(), orderNo);
+        }
+        
+        // 确保 order 和 orderNo 已初始化(用于后续代码)
+        if (order == null || orderNo == null) {
+            throw new RuntimeException("订单创建失败,系统错误");
+        }
+        final StoreOrder finalOrder = order;
+        final String finalOrderNo = orderNo;
+        final boolean finalIsUpdate = isUpdate; // 用于 lambda 表达式
+
+        // 更新优惠券使用记录状态为已下单
+        if (dto.getCouponId() != null) {
+            LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
+            usageWrapper.eq(StoreCouponUsage::getTableId, dto.getTableId());
+            usageWrapper.eq(StoreCouponUsage::getCouponId, dto.getCouponId());
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setOrderId(finalOrder.getId());
+                usage.setUsageStatus(1); // 已下单
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            }
+        }
+        
+        // 如果是更新订单,且优惠券发生变化,需要处理旧优惠券的使用记录
+        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();
+                    detail.setOrderId(finalOrder.getId());
+                    detail.setOrderNo(finalOrderNo);
+                    detail.setCuisineId(item.getCuisineId());
+                    detail.setCuisineName(item.getCuisineName());
+                    
+                    // 设置菜品类型:如果为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());
+                    detail.setSubtotalAmount(item.getSubtotalAmount());
+                    detail.setAddUserId(item.getAddUserId());
+                    detail.setAddUserPhone(item.getAddUserPhone());
+                    detail.setRemark(item.getRemark());
+                    detail.setCreatedUserId(userId);
+                    detail.setCreatedTime(now);
+                    detail.setUpdatedTime(now);
+                    
+                    // 如果是更新订单,判断是否为新增商品或数量增加的商品
+                    if (finalIsUpdate) {
+                        Integer lockedQuantity = item.getLockedQuantity();
+                        // 如果是新增商品(lockedQuantity 为 null 或 0)或数量增加的商品,标记为加餐
+                        if (lockedQuantity == null || lockedQuantity == 0 || 
+                            (item.getQuantity() != null && item.getQuantity() > lockedQuantity)) {
+                            detail.setIsAddDish(1); // 标记为加餐
+                            detail.setAddDishTime(now);
+                        }
+                    }
+                    
+                    return detail;
+                })
+                .collect(Collectors.toList());
+
+        for (StoreOrderDetail detail : orderDetails) {
+            orderDetailMapper.insert(detail);
+        }
+
+        // 记录订单变更日志
+        recordOrderChangeLog(finalOrder.getId(), finalOrderNo, cart.getItems(), finalIsUpdate ? 3 : 1, now, userId, userPhone);
+
+        // 更新桌号的当前订单ID和状态(显式用 LambdaUpdateWrapper 写入,避免 updateById 忽略更新)
+        // 约定:首次下单 → 餐桌状态=就餐中(1);后续下单(更新订单/加餐) → 餐桌状态=加餐(3)
+        Integer tableId = table.getId();
+        if (!finalIsUpdate) {
+            // 首次下单:设置订单ID,餐桌状态为就餐中(1)
+            LambdaUpdateWrapper<StoreTable> newTableWrapper = new LambdaUpdateWrapper<>();
+            newTableWrapper.eq(StoreTable::getId, tableId)
+                    .set(StoreTable::getCurrentOrderId, finalOrder.getId())
+                    .set(StoreTable::getStatus, 1)
+                    .set(StoreTable::getUpdatedTime, now);
+            if (userId != null) {
+                newTableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, newTableWrapper);
+        } else {
+            // 后续下单(更新订单/加餐):餐桌状态设为加餐(3)
+            LambdaUpdateWrapper<StoreTable> addDishTableWrapper = new LambdaUpdateWrapper<>();
+            addDishTableWrapper.eq(StoreTable::getId, tableId)
+                    .set(StoreTable::getStatus, 3)
+                    .set(StoreTable::getUpdatedTime, now);
+            if (userId != null) {
+                addDishTableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, addDishTableWrapper);
+            log.info("更新订单(加餐),设置餐桌状态为加餐, tableId={}, orderId={}", tableId, finalOrder.getId());
+        }
+
+        // 锁定购物车商品数量(禁止减少或删除已下单的商品)
+        cartService.lockCartItems(dto.getTableId());
+
+        // 下单后不清空购物车,允许加餐(加餐时添加到同一订单)
+        // 只有在支付完成后才清空购物车
+
+        // 创建订单成功后,自动解锁订单锁定
+        try {
+            orderLockService.unlockOrder(dto.getTableId(), userId);
+            log.info("订单创建成功后自动解锁订单锁定, tableId={}, userId={}", dto.getTableId(), userId);
+        } catch (Exception e) {
+            log.warn("自动解锁订单锁定失败, tableId={}, userId={}, error={}", dto.getTableId(), userId, e.getMessage());
+            // 解锁失败不影响订单创建,只记录警告日志
+        }
+
+        if (isUpdate) {
+            log.info("订单更新成功, orderId={}, orderNo={}", finalOrder.getId(), finalOrderNo);
+        } else {
+            log.info("订单创建成功, orderId={}, orderNo={}", finalOrder.getId(), finalOrderNo);
+        }
+        return finalOrder;
+    }
+
+    @Override
+    public StoreOrder payOrder(Integer orderId, Integer payType) {
+        log.info("支付订单, orderId={}, payType={}", orderId, payType);
+
+        // 获取当前用户信息
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
+
+        // 检查结算锁定状态
+        Integer lockUserId = orderLockService.checkSettlementLock(orderId);
+        if (lockUserId != null && !lockUserId.equals(userId)) {
+            throw new RuntimeException("订单结算已被其他用户锁定,无法支付");
+        }
+
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "支付");
+
+        // 支付时重新计算优惠券金额和实付金额(此时订单金额已确定,不会再变化)
+        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);
+        order.setPayTime(new Date());
+        order.setPayTradeNo("TRADE_" + System.currentTimeMillis()); // 模拟交易号
+        order.setUpdatedTime(new Date());
+        order.setUpdatedUserId(userId);
+
+        this.updateById(order);
+
+        // 更新优惠券使用记录状态为已支付
+        if (order.getCouponId() != null) {
+            LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
+            usageWrapper.eq(StoreCouponUsage::getOrderId, orderId);
+            usageWrapper.eq(StoreCouponUsage::getCouponId, order.getCouponId());
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setUsageStatus(2); // 已支付
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            }
+        }
+
+        // 支付完成后,清空购物车和重置餐桌状态(保留订单数据,不删除订单)
+        // resetTableAfterPayment 方法会清空购物车和重置餐桌状态,但不会删除订单数据
+        resetTableAfterPayment(order.getTableId());
+
+        // 支付订单成功后,自动解锁结算锁定
+        try {
+            orderLockService.unlockSettlement(orderId, userId);
+            log.info("订单支付成功后自动解锁结算锁定, orderId={}, userId={}", orderId, userId);
+        } catch (Exception e) {
+            log.warn("自动解锁结算锁定失败, orderId={}, userId={}, error={}", orderId, userId, e.getMessage());
+            // 解锁失败不影响订单支付,只记录警告日志
+        }
+
+        log.info("订单支付成功, orderId={}", orderId);
+        return order;
+    }
+
+    @Override
+    public boolean cancelOrder(Integer orderId) {
+        log.info("取消订单, orderId={}", orderId);
+
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "取消");
+
+        order.setOrderStatus(2); // 已取消
+        order.setUpdatedTime(new Date());
+
+        // 获取当前用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
+        }
+
+        this.updateById(order);
+
+        // 恢复购物车的已下单数量(允许重新下单)
+        cartService.unlockCartItems(order.getTableId());
+
+        // 清除优惠券使用标记
+        cartService.clearCouponUsed(order.getTableId());
+
+        // 更新桌号状态:显式清空 currentOrderId(updateById 会忽略 null,必须用 LambdaUpdateWrapper)
+        Integer tableId = order.getTableId();
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            LambdaUpdateWrapper<StoreTable> tableWrapper = new LambdaUpdateWrapper<>();
+            tableWrapper.eq(StoreTable::getId, tableId)
+                    .set(StoreTable::getCurrentOrderId, null)
+                    .set(StoreTable::getUpdatedTime, new Date());
+            CartDTO cart = cartService.getCart(tableId);
+            if (cart.getItems() == null || cart.getItems().isEmpty()) {
+                tableWrapper.set(StoreTable::getStatus, 0).set(StoreTable::getDinerCount, null);
+            } else {
+                tableWrapper.set(StoreTable::getStatus, 1); // 就餐中
+            }
+            if (userId != null) {
+                tableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, tableWrapper);
+        }
+
+        log.info("订单取消成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public StoreOrder getOrderByOrderNo(String orderNo) {
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getOrderNo, orderNo);
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public StoreOrder getOrderById(Integer orderId) {
+        return this.getById(orderId);
+    }
+
+    /** 前端只传 0=进行中、3=已完成。进行中对应库表 0待支付+1已支付,已完成对应 3。 */
+    private List<Integer> resolveOrderStatusFilter(Integer orderStatus) {
+        if (orderStatus == null) {
+            return null;
+        }
+        if (orderStatus == 0) {
+            return Arrays.asList(0, 1); // 进行中 = 待支付 + 已支付
+        }
+        if (orderStatus == 3) {
+            return Collections.singletonList(3); // 已完成
+        }
+        return null; // 其他值不参与筛选
+    }
+
+    @Override
+    public IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword) {
+        // 前端只传 0=进行中、3=已完成。进行中=待支付(0)+已支付(1),已完成=3
+        List<Integer> orderStatusList = resolveOrderStatusFilter(orderStatus);
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        if (storeId != null) {
+            wrapper.eq(StoreOrder::getStoreId, storeId);
+        }
+        if (tableId != null) {
+            wrapper.eq(StoreOrder::getTableId, tableId);
+        }
+        if (orderStatusList != null && !orderStatusList.isEmpty()) {
+            wrapper.in(StoreOrder::getOrderStatus, orderStatusList);
+        }
+        
+        // 搜索功能:按订单编号模糊搜索
+        if (StringUtils.hasText(keyword)) {
+            // 限制搜索关键词长度(15字)
+            String searchKeyword = keyword.length() > 15 ? keyword.substring(0, 15) : keyword;
+            wrapper.like(StoreOrder::getOrderNo, searchKeyword);
+        }
+        
+        wrapper.orderByDesc(StoreOrder::getCreatedTime);
+        return this.page(page, wrapper);
+    }
+
+    @Override
+    public IPage<StoreOrderPageVO> getOrderPageWithCuisines(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword) {
+        // 前端只传 0=进行中、3=已完成。进行中=待支付(0)+已支付(1),已完成=3
+        List<Integer> orderStatusList = resolveOrderStatusFilter(orderStatus);
+        log.info("分页查询订单列表(包含菜品信息), storeId={}, tableId={}, orderStatus={}, keyword={}", storeId, tableId, orderStatusList, keyword);
+        
+        // 限制搜索关键词长度(15字)
+        String searchKeyword = null;
+        if (StringUtils.hasText(keyword)) {
+            searchKeyword = keyword.length() > 15 ? keyword.substring(0, 15) : keyword;
+        }
+        
+        // 1. 如果有关键词,需要同时支持按订单编号和菜品名称搜索
+        final Set<Integer> matchingOrderIds;
+        if (StringUtils.hasText(searchKeyword)) {
+            Set<Integer> orderIdsSet = new HashSet<>();
+            
+            // 1.1 按订单编号搜索
+            LambdaQueryWrapper<StoreOrder> orderNoWrapper = new LambdaQueryWrapper<>();
+            orderNoWrapper.eq(StoreOrder::getDeleteFlag, 0)
+                    .like(StoreOrder::getOrderNo, searchKeyword);
+            if (storeId != null) {
+                orderNoWrapper.eq(StoreOrder::getStoreId, storeId);
+            }
+            if (tableId != null) {
+                orderNoWrapper.eq(StoreOrder::getTableId, tableId);
+            }
+            if (orderStatusList != null && !orderStatusList.isEmpty()) {
+                orderNoWrapper.in(StoreOrder::getOrderStatus, orderStatusList);
+            }
+            List<StoreOrder> orderNoMatches = this.list(orderNoWrapper);
+            orderNoMatches.forEach(order -> orderIdsSet.add(order.getId()));
+            
+            // 1.2 按菜品名称搜索
+            LambdaQueryWrapper<StoreOrderDetail> detailSearchWrapper = new LambdaQueryWrapper<>();
+            detailSearchWrapper.select(StoreOrderDetail::getOrderId)
+                    .like(StoreOrderDetail::getCuisineName, searchKeyword)
+                    .eq(StoreOrderDetail::getDeleteFlag, 0);
+            
+            // 如果有限制条件,需要先查询符合条件的订单ID
+            if (storeId != null || tableId != null || (orderStatusList != null && !orderStatusList.isEmpty())) {
+                LambdaQueryWrapper<StoreOrder> filterWrapper = new LambdaQueryWrapper<>();
+                filterWrapper.eq(StoreOrder::getDeleteFlag, 0)
+                        .select(StoreOrder::getId);
+                if (storeId != null) {
+                    filterWrapper.eq(StoreOrder::getStoreId, storeId);
+                }
+                if (tableId != null) {
+                    filterWrapper.eq(StoreOrder::getTableId, tableId);
+                }
+                if (orderStatusList != null && !orderStatusList.isEmpty()) {
+                    filterWrapper.in(StoreOrder::getOrderStatus, orderStatusList);
+                }
+                List<StoreOrder> filteredOrders = this.list(filterWrapper);
+                Set<Integer> filteredOrderIds = filteredOrders.stream()
+                        .map(StoreOrder::getId)
+                        .collect(Collectors.toSet());
+                if (!filteredOrderIds.isEmpty()) {
+                    detailSearchWrapper.in(StoreOrderDetail::getOrderId, filteredOrderIds);
+                } else {
+                    // 如果没有符合条件的订单,直接返回空结果
+                    Page<StoreOrderPageVO> emptyPage = new Page<>(page.getCurrent(), page.getSize());
+                    emptyPage.setTotal(0);
+                    emptyPage.setPages(0);
+                    return emptyPage;
+                }
+            }
+            
+            List<StoreOrderDetail> matchingDetails = orderDetailMapper.selectList(detailSearchWrapper);
+            matchingDetails.forEach(detail -> orderIdsSet.add(detail.getOrderId()));
+            
+            matchingOrderIds = orderIdsSet;
+            
+            // 如果没有任何匹配的订单,返回空结果
+            if (matchingOrderIds.isEmpty()) {
+                Page<StoreOrderPageVO> emptyPage = new Page<>(page.getCurrent(), page.getSize());
+                emptyPage.setTotal(0);
+                emptyPage.setPages(0);
+                return emptyPage;
+            }
+        } else {
+            matchingOrderIds = null;
+        }
+        
+        // 2. 查询订单列表
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        if (storeId != null) {
+            wrapper.eq(StoreOrder::getStoreId, storeId);
+        }
+        if (tableId != null) {
+            wrapper.eq(StoreOrder::getTableId, tableId);
+        }
+        if (orderStatusList != null && !orderStatusList.isEmpty()) {
+            wrapper.in(StoreOrder::getOrderStatus, orderStatusList);
+        }
+        // 如果有关键词搜索,只查询匹配的订单ID
+        if (matchingOrderIds != null && !matchingOrderIds.isEmpty()) {
+            wrapper.in(StoreOrder::getId, matchingOrderIds);
+        }
+        wrapper.orderByDesc(StoreOrder::getCreatedTime);
+        
+        IPage<StoreOrder> orderPage = this.page(page, wrapper);
+        
+        // 3. 获取所有订单ID
+        List<StoreOrder> orders = orderPage.getRecords();
+        if (orders == null || orders.isEmpty()) {
+            // 如果没有订单,返回空的分页结果
+            Page<StoreOrderPageVO> resultPage = new Page<>(page.getCurrent(), page.getSize());
+            resultPage.setTotal(orderPage.getTotal());
+            resultPage.setPages(orderPage.getPages());
+            return resultPage;
+        }
+        
+        List<Integer> orderIds = orders.stream()
+                .map(StoreOrder::getId)
+                .collect(Collectors.toList());
+        
+        // 3. 批量查询所有订单的菜品详情
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.in(StoreOrderDetail::getOrderId, orderIds);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByAsc(StoreOrderDetail::getOrderId);
+        detailWrapper.orderByAsc(StoreOrderDetail::getCreatedTime);
+        List<StoreOrderDetail> allDetails = orderDetailMapper.selectList(detailWrapper);
+        
+        // 4. 按订单ID分组菜品详情
+        Map<Integer, List<StoreOrderDetail>> detailsMap = allDetails.stream()
+                .collect(Collectors.groupingBy(StoreOrderDetail::getOrderId));
+        
+        // 4.1 批量查询菜品标签(用于分页列表展示)
+        Set<Integer> cuisineIds = allDetails.stream()
+                .map(StoreOrderDetail::getCuisineId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Integer, String> cuisineIdToTags = new HashMap<>();
+        if (!cuisineIds.isEmpty()) {
+            List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+            if (cuisines != null) {
+                for (StoreCuisine c : cuisines) {
+                    if (c.getTags() != null) {
+                        cuisineIdToTags.put(c.getId(), c.getTags());
+                    }
+                }
+            }
+        }
+        Map<Integer, String> finalCuisineIdToTags = cuisineIdToTags;
+        
+        // 5. 批量查询门店名称
+        Set<Integer> storeIds = orders.stream().map(StoreOrder::getStoreId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Integer, String> storeNameMap = new HashMap<>();
+        if (!storeIds.isEmpty()) {
+            List<StoreInfo> storeList = storeInfoMapper.selectBatchIds(storeIds);
+            if (storeList != null) {
+                for (StoreInfo s : storeList) {
+                    storeNameMap.put(s.getId(), s.getStoreName());
+                }
+            }
+        }
+        
+        // 6. 构建返回结果
+        List<StoreOrderPageVO> voList = orders.stream().map(order -> {
+            StoreOrderPageVO vo = new StoreOrderPageVO();
+            vo.setOrder(order);
+            vo.setStoreName(storeNameMap.getOrDefault(order.getStoreId(), ""));
+            
+            // 获取该订单的菜品列表(含菜品标签)
+            List<StoreOrderDetail> orderDetails = detailsMap.getOrDefault(order.getId(), new ArrayList<>());
+            List<OrderCuisineItemVO> cuisineItems = orderDetails.stream()
+                    .map(detail -> {
+                        OrderCuisineItemVO item = new OrderCuisineItemVO();
+                        item.setCuisineId(detail.getCuisineId());
+                        item.setCuisineName(detail.getCuisineName());
+                        item.setCuisineImage(detail.getCuisineImage());
+                        item.setQuantity(detail.getQuantity());
+                        item.setUnitPrice(detail.getUnitPrice());
+                        item.setTags(detail.getCuisineId() != null ? finalCuisineIdToTags.get(detail.getCuisineId()) : null);
+                        return item;
+                    })
+                    .collect(Collectors.toList());
+            vo.setCuisineItems(cuisineItems);
+            
+            return vo;
+        }).collect(Collectors.toList());
+        
+        // 7. 构建分页结果
+        Page<StoreOrderPageVO> resultPage = new Page<>(page.getCurrent(), page.getSize());
+        resultPage.setRecords(voList);
+        resultPage.setTotal(orderPage.getTotal());
+        resultPage.setPages(orderPage.getPages());
+        
+        return resultPage;
+    }
+
+    @Override
+    public boolean completeOrder(Integer orderId) {
+        log.info("完成订单, orderId={}", orderId);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        // 检查订单状态:只有已支付状态(1)的订单才能完成
+        if (order.getOrderStatus() != 1) {
+            throw new RuntimeException("订单状态不正确,只有已支付状态的订单才能完成");
+        }
+
+        // 检查支付状态:确保订单已支付
+        if (order.getPayStatus() != 1) {
+            throw new RuntimeException("订单未支付,无法完成");
+        }
+
+        order.setOrderStatus(3); // 已完成
+        order.setUpdatedTime(new Date());
+
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
+        }
+
+        this.updateById(order);
+
+        // 更新桌号状态:显式清空 currentOrderId(updateById 会忽略 null,必须用 LambdaUpdateWrapper)
+        Integer tableId = order.getTableId();
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            LambdaUpdateWrapper<StoreTable> tableWrapper = new LambdaUpdateWrapper<>();
+            tableWrapper.eq(StoreTable::getId, tableId)
+                    .set(StoreTable::getCurrentOrderId, null)
+                    .set(StoreTable::getStatus, 0)
+                    .set(StoreTable::getDinerCount, null)
+                    .set(StoreTable::getUpdatedTime, new Date());
+            if (userId != null) {
+                tableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, tableWrapper);
+        }
+
+        log.info("订单完成成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public boolean completeOrderByMerchant(Integer orderId) {
+        log.info("商家手动完成订单, orderId={}", orderId);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        // 商家手动完成订单,不校验支付状态,直接设置为已完成
+        order.setOrderStatus(3); // 已完成
+        order.setUpdatedTime(new Date());
+
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
+        }
+
+        this.updateById(order);
+
+        // 更新桌号状态:显式清空 currentOrderId(updateById 会忽略 null,必须用 LambdaUpdateWrapper)
+        Integer tableId = order.getTableId();
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            LambdaUpdateWrapper<StoreTable> tableWrapper = new LambdaUpdateWrapper<>();
+            tableWrapper.eq(StoreTable::getId, tableId)
+                    .set(StoreTable::getCurrentOrderId, null)
+                    .set(StoreTable::getStatus, 0)
+                    .set(StoreTable::getDinerCount, null)
+                    .set(StoreTable::getUpdatedTime, new Date());
+            if (userId != null) {
+                tableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, tableWrapper);
+        }
+
+        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) {
+            // 验证优惠券
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+            if (coupon == null) {
+                throw new RuntimeException("优惠券不存在");
+            }
+
+            // 验证优惠券是否属于该门店
+            if (!coupon.getStoreId().equals(String.valueOf(order.getStoreId()))) {
+                throw new RuntimeException("优惠券不属于该门店");
+            }
+
+            // 验证最低消费(菜品总价 + 餐具费)
+            BigDecimal totalWithTableware = order.getTotalAmount().add(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO);
+            if (coupon.getMinimumSpendingAmount() != null
+                    && totalWithTableware.compareTo(coupon.getMinimumSpendingAmount()) < 0) {
+                throw new RuntimeException("订单金额未达到优惠券最低消费要求");
+            }
+
+            // 计算优惠金额:根据优惠券类型(满减券或折扣券)计算
+            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());
+            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);
+            }
+        }
+
+        // 如果新设置了优惠券,需要标记桌号已使用优惠券,并更新或创建使用记录
+        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);
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setOrderId(orderId);
+                usage.setUsageStatus(1); // 已下单
+                usage.setDiscountAmount(discountAmount);
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            } else {
+                // 创建新的使用记录
+                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());
+                    newUsage.setOrderId(orderId);
+                    newUsage.setCouponId(couponId);
+                    newUsage.setDiscountAmount(discountAmount);
+                    newUsage.setUsageStatus(1); // 已下单
+                    newUsage.setCreatedTime(now);
+                    newUsage.setUpdatedTime(now); // 设置更新时间,避免数据库约束错误
+                    Integer userId = TokenUtil.getCurrentUserId();
+                    if (userId != null) {
+                        newUsage.setCreatedUserId(userId);
+                    }
+                    storeCouponUsageMapper.insert(newUsage);
+                }
+            }
+        }
+
+        // 重新计算实付金额(菜品总价 + 餐具费 - 优惠金额)
+        BigDecimal tablewareFee = order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO;
+        BigDecimal payAmount = order.getTotalAmount().add(tablewareFee).subtract(discountAmount);
+
+        order.setCouponId(couponId);
+        order.setCurrentCouponId(couponId);
+        order.setDiscountAmount(discountAmount);
+        order.setPayAmount(payAmount);
+        order.setUpdatedTime(new Date());
+
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
+        }
+
+        this.updateById(order);
+
+        log.info("更新订单优惠券成功, orderId={}", orderId);
+        return order;
+    }
+
+    @Override
+    public boolean resetTable(Integer tableId) {
+        log.info("管理员重置餐桌, tableId={}", tableId);
+        
+        // 验证餐桌是否存在
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("餐桌不存在");
+        }
+        
+        // 1. 删除购物车数据(逻辑删除,使用 MyBatis-Plus 的 removeBatchIds)
+        LambdaQueryWrapper<StoreCart> cartWrapper = new LambdaQueryWrapper<>();
+        cartWrapper.eq(StoreCart::getTableId, tableId);
+        cartWrapper.eq(StoreCart::getDeleteFlag, 0);
+        List<StoreCart> cartList = storeCartMapper.selectList(cartWrapper);
+        if (cartList != null && !cartList.isEmpty()) {
+            List<Integer> cartIds = cartList.stream()
+                    .map(StoreCart::getId)
+                    .collect(Collectors.toList());
+            // 使用 removeBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            storeCartMapper.deleteBatchIds(cartIds);
+            log.info("删除购物车数据, tableId={}, count={}", tableId, cartList.size());
+        }
+        
+        // 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()) {
+            orderIds = orderList.stream()
+                    .map(StoreOrder::getId)
+                    .collect(Collectors.toList());
+            
+            // 删除订单明细(逻辑删除,使用 MyBatis-Plus 的 deleteBatchIds)
+            for (Integer orderId : orderIds) {
+                LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+                detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+                detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+                List<StoreOrderDetail> detailList = orderDetailMapper.selectList(detailWrapper);
+                if (detailList != null && !detailList.isEmpty()) {
+                    List<Integer> detailIds = detailList.stream()
+                            .map(StoreOrderDetail::getId)
+                            .collect(Collectors.toList());
+                    // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                    orderDetailMapper.deleteBatchIds(detailIds);
+                    log.info("删除订单明细, orderId={}, count={}", orderId, detailList.size());
+                }
+            }
+            
+            // 删除订单变更记录(逻辑删除,使用 MyBatis-Plus 的 deleteBatchIds)
+            for (Integer orderId : orderIds) {
+                LambdaQueryWrapper<StoreOrderChangeLog> changeLogWrapper = new LambdaQueryWrapper<>();
+                changeLogWrapper.eq(StoreOrderChangeLog::getOrderId, orderId);
+                changeLogWrapper.eq(StoreOrderChangeLog::getDeleteFlag, 0);
+                List<StoreOrderChangeLog> changeLogList = orderChangeLogMapper.selectList(changeLogWrapper);
+                if (changeLogList != null && !changeLogList.isEmpty()) {
+                    List<Integer> changeLogIds = changeLogList.stream()
+                            .map(StoreOrderChangeLog::getId)
+                            .collect(Collectors.toList());
+                    // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                    orderChangeLogMapper.deleteBatchIds(changeLogIds);
+                    log.info("删除订单变更记录, orderId={}, count={}", orderId, changeLogList.size());
+                }
+            }
+            
+            // 删除订单(逻辑删除,使用 MyBatis-Plus 的 removeByIds)
+            // 使用 removeByIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            this.removeByIds(orderIds);
+            log.info("删除未支付/已取消订单数据, tableId={}, count={}", tableId, orderList.size());
+        }
+        
+        // 查询该桌号的所有订单(包括已支付/已完成的),用于后续处理锁定记录
+        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);
+        
+        // 6. 清除优惠券使用标记(Redis中的标记)
+        cartService.clearCouponUsed(tableId);
+        log.info("清除优惠券使用标记, tableId={}", tableId);
+        
+        // 7. 重置餐桌表(使用 LambdaUpdateWrapper 来显式设置 null 值)
+        LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreTable::getId, tableId)
+                .set(StoreTable::getCurrentOrderId, null)  // 显式设置 null
+                .set(StoreTable::getCurrentCouponId, null)  // 显式设置 null
+                .set(StoreTable::getCartItemCount, 0)
+                .set(StoreTable::getCartTotalAmount, BigDecimal.ZERO)
+                .set(StoreTable::getStatus, 0)  // 空闲
+                .set(StoreTable::getDinerCount, null)  // 清空就餐人数
+                .set(StoreTable::getUpdatedTime, new Date());
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            updateWrapper.set(StoreTable::getUpdatedUserId, userId);
+        }
+        storeTableMapper.update(null, updateWrapper);
+        log.info("重置餐桌表完成, tableId={}", tableId);
+        
+        return true;
+    }
+
+    /**
+     * 支付完成后重置餐桌(保留订单数据,只重置餐桌绑定关系)
+     *
+     * @param tableId 餐桌ID
+     */
+    @Override
+    public void resetTableAfterPayment(Integer tableId) {
+        log.info("支付完成后重置餐桌, tableId={}", tableId);
+
+        // 验证餐桌是否存在
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            log.warn("餐桌不存在,跳过重置, tableId={}", tableId);
+            return;
+        }
+
+        // 1. 完全清空购物车(包括已下单的商品,因为订单已支付完成)
+        // 逻辑删除所有购物车数据
+        LambdaQueryWrapper<StoreCart> cartWrapper = new LambdaQueryWrapper<>();
+        cartWrapper.eq(StoreCart::getTableId, tableId);
+        cartWrapper.eq(StoreCart::getDeleteFlag, 0);
+        List<StoreCart> cartList = storeCartMapper.selectList(cartWrapper);
+        if (cartList != null && !cartList.isEmpty()) {
+            List<Integer> cartIds = cartList.stream()
+                    .map(StoreCart::getId)
+                    .collect(Collectors.toList());
+            storeCartMapper.deleteBatchIds(cartIds);
+            log.info("支付完成后删除购物车数据, tableId={}, count={}", tableId, cartList.size());
+        }
+
+        // 2. 清空Redis中的购物车缓存
+        String cartKey = "cart:table:" + tableId;
+        baseRedisService.delete(cartKey);
+        log.info("清空Redis购物车缓存, tableId={}", tableId);
+
+        // 3. 清除优惠券使用标记
+        cartService.clearCouponUsed(tableId);
+        log.info("清除优惠券使用标记, tableId={}", tableId);
+
+        // 4. 重置餐桌表(使用 LambdaUpdateWrapper 来显式设置 null 值)
+        LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreTable::getId, tableId)
+                .set(StoreTable::getCurrentOrderId, null)  // 显式设置 null
+                .set(StoreTable::getCurrentCouponId, null)  // 显式设置 null
+                .set(StoreTable::getCartItemCount, 0)
+                .set(StoreTable::getCartTotalAmount, BigDecimal.ZERO)
+                .set(StoreTable::getStatus, 0)  // 空闲
+                .set(StoreTable::getDinerCount, null)  // 清空就餐人数
+                .set(StoreTable::getUpdatedTime, new Date());
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            updateWrapper.set(StoreTable::getUpdatedUserId, userId);
+        }
+        storeTableMapper.update(null, updateWrapper);
+        log.info("支付完成后重置餐桌表完成, tableId={}", tableId);
+    }
+
+    @Override
+    public IPage<StoreOrder> getMyOrders(Page<StoreOrder> page, String type) {
+        log.info("查询我的订单, type={}", type);
+
+        // 获取当前用户ID
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId == null) {
+            throw new RuntimeException("用户未登录");
+        }
+
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        wrapper.eq(StoreOrder::getCreatedUserId, userId); // 查询当前用户创建的订单
+
+        // 根据类型过滤订单
+        if ("0".equals(type) || "unpaid".equalsIgnoreCase(type)) {
+            // 未支付订单:orderStatus=0 且 payStatus=0
+            wrapper.eq(StoreOrder::getOrderStatus, 0);
+            wrapper.eq(StoreOrder::getPayStatus, 0);
+            log.info("查询未支付订单, userId={}", userId);
+        } else if ("1".equals(type) || "history".equalsIgnoreCase(type)) {
+            // 历史订单:已支付或已完成的订单(orderStatus=1 或 3,payStatus=1)
+            wrapper.and(w -> w.and(w1 -> w1.eq(StoreOrder::getOrderStatus, 1).eq(StoreOrder::getPayStatus, 1))
+                    .or(w2 -> w2.eq(StoreOrder::getOrderStatus, 3).eq(StoreOrder::getPayStatus, 1)));
+            log.info("查询历史订单, userId={}", userId);
+        } else {
+            // 如果类型不明确,返回所有订单
+            log.warn("订单类型不明确, type={}, 返回所有订单", type);
+        }
+
+        wrapper.orderByDesc(StoreOrder::getCreatedTime);
+        return this.page(page, wrapper);
+    }
+
+    @Override
+    public IPage<StoreOrderPageVO> getMyOrdersWithCuisines(Page<StoreOrder> page, String type) {
+        log.info("查询我的订单(包含菜品信息), type={}", type);
+
+        // 1. 查询订单列表
+        IPage<StoreOrder> orderPage = getMyOrders(page, type);
+
+        // 2. 获取所有订单ID
+        List<StoreOrder> orders = orderPage.getRecords();
+        if (orders == null || orders.isEmpty()) {
+            // 如果没有订单,返回空的分页结果
+            Page<StoreOrderPageVO> resultPage = new Page<>(page.getCurrent(), page.getSize());
+            resultPage.setTotal(orderPage.getTotal());
+            resultPage.setPages(orderPage.getPages());
+            return resultPage;
+        }
+
+        List<Integer> orderIds = orders.stream()
+                .map(StoreOrder::getId)
+                .collect(Collectors.toList());
+
+        // 3. 批量查询所有订单的菜品详情
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.in(StoreOrderDetail::getOrderId, orderIds);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByAsc(StoreOrderDetail::getOrderId);
+        detailWrapper.orderByAsc(StoreOrderDetail::getCreatedTime);
+        List<StoreOrderDetail> allDetails = orderDetailMapper.selectList(detailWrapper);
+
+        // 4. 按订单ID分组菜品详情
+        Map<Integer, List<StoreOrderDetail>> detailsMap = allDetails.stream()
+                .collect(Collectors.groupingBy(StoreOrderDetail::getOrderId));
+
+        // 4.1 批量查询菜品标签(用于我的订单分页展示)
+        Set<Integer> cuisineIdsMy = allDetails.stream()
+                .map(StoreOrderDetail::getCuisineId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Integer, String> cuisineIdToTagsMy = new HashMap<>();
+        if (!cuisineIdsMy.isEmpty()) {
+            List<StoreCuisine> cuisinesMy = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIdsMy));
+            if (cuisinesMy != null) {
+                for (StoreCuisine c : cuisinesMy) {
+                    if (c.getTags() != null) {
+                        cuisineIdToTagsMy.put(c.getId(), c.getTags());
+                    }
+                }
+            }
+        }
+        Map<Integer, String> finalCuisineIdToTagsMy = cuisineIdToTagsMy;
+
+        // 5. 批量查询门店名称
+        Set<Integer> storeIds = orders.stream().map(StoreOrder::getStoreId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Integer, String> storeNameMap = new HashMap<>();
+        if (!storeIds.isEmpty()) {
+            List<StoreInfo> storeList = storeInfoMapper.selectBatchIds(storeIds);
+            if (storeList != null) {
+                for (StoreInfo s : storeList) {
+                    storeNameMap.put(s.getId(), s.getStoreName());
+                }
+            }
+        }
+
+        // 6. 构建返回结果
+        List<StoreOrderPageVO> voList = orders.stream().map(order -> {
+            StoreOrderPageVO vo = new StoreOrderPageVO();
+            vo.setOrder(order);
+            vo.setStoreName(storeNameMap.getOrDefault(order.getStoreId(), ""));
+
+            // 获取该订单的菜品列表(含菜品标签)
+            List<StoreOrderDetail> orderDetails = detailsMap.getOrDefault(order.getId(), new ArrayList<>());
+            List<OrderCuisineItemVO> cuisineItems = orderDetails.stream()
+                    .map(detail -> {
+                        OrderCuisineItemVO item = new OrderCuisineItemVO();
+                        item.setCuisineId(detail.getCuisineId());
+                        item.setCuisineName(detail.getCuisineName());
+                        item.setCuisineImage(detail.getCuisineImage());
+                        item.setQuantity(detail.getQuantity());
+                        item.setUnitPrice(detail.getUnitPrice());
+                        item.setTags(detail.getCuisineId() != null ? finalCuisineIdToTagsMy.get(detail.getCuisineId()) : null);
+                        return item;
+                    })
+                    .collect(Collectors.toList());
+            vo.setCuisineItems(cuisineItems);
+
+            return vo;
+        }).collect(Collectors.toList());
+
+        // 7. 构建分页结果
+        Page<StoreOrderPageVO> resultPage = new Page<>(page.getCurrent(), page.getSize());
+        resultPage.setRecords(voList);
+        resultPage.setTotal(orderPage.getTotal());
+        resultPage.setPages(orderPage.getPages());
+
+        return resultPage;
+    }
+
+    @Override
+    public OrderInfoVO getOrderInfo(Integer orderId) {
+        log.info("查询订单信息, orderId={}", orderId);
+        
+        // 1. 查询订单基本信息
+        StoreOrder order = this.getById(orderId);
+        if (order == null || order.getDeleteFlag() == 1) {
+            throw new RuntimeException("订单不存在");
+        }
+        
+        // 2. 查询订单明细(菜品清单)
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByDesc(StoreOrderDetail::getCreatedTime);
+        List<StoreOrderDetail> details = orderDetailMapper.selectList(detailWrapper);
+        
+        // 批量查询菜品标签(订单明细无 tags,从价目表取)
+        java.util.Set<Integer> cuisineIds = details.stream()
+                .map(StoreOrderDetail::getCuisineId)
+                .filter(java.util.Objects::nonNull)
+                .collect(Collectors.toSet());
+        java.util.Map<Integer, String> cuisineIdToTags = new java.util.HashMap<>();
+        if (!cuisineIds.isEmpty()) {
+            List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(cuisineIds);
+            if (cuisines != null) {
+                for (StoreCuisine c : cuisines) {
+                    cuisineIdToTags.put(c.getId(), c.getTags());
+                }
+            }
+        }
+        final java.util.Map<Integer, String> tagsMap = cuisineIdToTags;
+        
+        // 转换为CartItemDTO(含 tags)
+        List<CartItemDTO> items = details.stream().map(detail -> {
+            CartItemDTO item = new CartItemDTO();
+            item.setCuisineId(detail.getCuisineId());
+            item.setCuisineName(detail.getCuisineName());
+            item.setCuisineType(detail.getCuisineType());
+            item.setCuisineImage(detail.getCuisineImage());
+            item.setUnitPrice(detail.getUnitPrice());
+            item.setQuantity(detail.getQuantity());
+            item.setSubtotalAmount(detail.getSubtotalAmount());
+            item.setAddUserId(detail.getAddUserId());
+            item.setAddUserPhone(detail.getAddUserPhone());
+            item.setRemark(detail.getRemark());
+            item.setTags(detail.getCuisineId() != null ? tagsMap.get(detail.getCuisineId()) : null);
+            return item;
+        }).collect(Collectors.toList());
+        
+        // 3. 查询门店信息
+        StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+        String storeName = "";
+        String storeTel = "";
+        String storeAddress = "";
+        String storeBlurb = "";
+        String storeType = "";
+        Integer businessStatus = null;
+        if (storeInfo != null) {
+            storeName = storeInfo.getStoreName();
+            storeTel = storeInfo.getStoreTel();
+            storeAddress = storeInfo.getStoreAddress();
+            storeBlurb = storeInfo.getStoreBlurb();
+            storeType = storeInfo.getStoreType();
+            businessStatus = storeInfo.getBusinessStatus();
+        }
+
+        // 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();
+            }
+        }
+
+        // 5. 组装OrderInfoVO
+        OrderInfoVO vo = new OrderInfoVO();
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setStoreId(order.getStoreId());
+        vo.setStoreName(storeName);
+        vo.setStoreTel(storeTel);
+        vo.setStoreAddress(storeAddress);
+        vo.setStoreBlurb(storeBlurb);
+        vo.setStoreType(storeType);
+        vo.setBusinessStatus(businessStatus);
+        vo.setTableNumber(order.getTableNumber());
+        vo.setDinerCount(order.getDinerCount());
+        vo.setContactPhone(order.getContactPhone());
+        vo.setRemark(order.getRemark());
+        vo.setItems(items);
+        vo.setTotalAmount(order.getTotalAmount());
+        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());
+        vo.setPayStatus(order.getPayStatus());
+        vo.setPayType(order.getPayType());
+        vo.setCreatedTime(order.getCreatedTime());
+        vo.setPayTime(order.getPayTime());
+        
+        log.info("查询订单信息完成, orderId={}, itemCount={}", orderId, items.size());
+        return vo;
+    }
+
+    @Override
+    public List<OrderChangeLogBatchVO> getOrderChangeLogs(Integer orderId) {
+        log.info("查询订单变更记录, orderId={}", orderId);
+        
+        // 1. 查询订单的所有变更记录
+        LambdaQueryWrapper<StoreOrderChangeLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrderChangeLog::getOrderId, orderId);
+        wrapper.eq(StoreOrderChangeLog::getDeleteFlag, 0);
+        wrapper.orderByAsc(StoreOrderChangeLog::getOperationTime);
+        wrapper.orderByAsc(StoreOrderChangeLog::getBatchNo);
+        List<StoreOrderChangeLog> logs = orderChangeLogMapper.selectList(wrapper);
+        
+        if (logs == null || logs.isEmpty()) {
+            log.info("订单没有变更记录, orderId={}", orderId);
+            return new ArrayList<>();
+        }
+        
+        // 2. 按批次号分组
+        Map<String, List<StoreOrderChangeLog>> batchMap = logs.stream()
+                .collect(Collectors.groupingBy(StoreOrderChangeLog::getBatchNo));
+        
+        // 2.1 批量查询菜品标签(用于订单详情展示)
+        Set<Integer> cuisineIds = logs.stream()
+                .map(StoreOrderChangeLog::getCuisineId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Integer, String> cuisineIdToTags = new HashMap<>();
+        if (!cuisineIds.isEmpty()) {
+            List<StoreCuisine> cuisines = storeCuisineMapper.selectBatchIds(new ArrayList<>(cuisineIds));
+            if (cuisines != null) {
+                for (StoreCuisine c : cuisines) {
+                    if (c.getTags() != null) {
+                        cuisineIdToTags.put(c.getId(), c.getTags());
+                    }
+                }
+            }
+        }
+        
+        // 3. 转换为批次VO列表
+        List<OrderChangeLogBatchVO> batchList = new ArrayList<>();
+        for (Map.Entry<String, List<StoreOrderChangeLog>> entry : batchMap.entrySet()) {
+            String batchNo = entry.getKey();
+            List<StoreOrderChangeLog> batchLogs = entry.getValue();
+            
+            if (batchLogs.isEmpty()) {
+                continue;
+            }
+            
+            // 取第一条记录作为批次信息(同一批次的操作类型、时间等信息相同)
+            StoreOrderChangeLog firstLog = batchLogs.get(0);
+            
+            OrderChangeLogBatchVO batchVO = new OrderChangeLogBatchVO();
+            batchVO.setBatchNo(batchNo);
+            batchVO.setOperationType(firstLog.getOperationType());
+            batchVO.setOperationTypeText(getOperationTypeText(firstLog.getOperationType()));
+            batchVO.setOperationTime(firstLog.getOperationTime());
+            batchVO.setOperatorUserId(firstLog.getOperatorUserId());
+            batchVO.setOperatorUserPhone(firstLog.getOperatorUserPhone());
+            batchVO.setRemark(firstLog.getRemark());
+            
+            // 计算批次统计信息
+            Integer totalQuantityChange = batchLogs.stream()
+                    .mapToInt(log -> log.getQuantityChange() != null ? log.getQuantityChange() : 0)
+                    .sum();
+            BigDecimal totalAmountChange = batchLogs.stream()
+                    .map(log -> log.getAmountChange() != null ? log.getAmountChange() : BigDecimal.ZERO)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            
+            batchVO.setTotalQuantityChange(totalQuantityChange);
+            batchVO.setTotalAmountChange(totalAmountChange);
+            batchVO.setItemCount(batchLogs.size());
+            
+            // 转换为商品项VO列表(含菜品标签)
+            Map<Integer, String> finalCuisineIdToTags = cuisineIdToTags;
+            List<OrderChangeLogItemVO> items = batchLogs.stream().map(log -> {
+                OrderChangeLogItemVO itemVO = new OrderChangeLogItemVO();
+                itemVO.setCuisineId(log.getCuisineId());
+                itemVO.setCuisineName(log.getCuisineName());
+                itemVO.setCuisineType(log.getCuisineType());
+                itemVO.setCuisineImage(log.getCuisineImage());
+                itemVO.setUnitPrice(log.getUnitPrice());
+                itemVO.setQuantityChange(log.getQuantityChange());
+                itemVO.setQuantityBefore(log.getQuantityBefore());
+                itemVO.setQuantityAfter(log.getQuantityAfter());
+                itemVO.setAmountChange(log.getAmountChange());
+                itemVO.setRemark(log.getRemark());
+                itemVO.setTags(log.getCuisineId() != null ? finalCuisineIdToTags.get(log.getCuisineId()) : null);
+                return itemVO;
+            }).collect(Collectors.toList());
+            
+            batchVO.setItems(items);
+            batchList.add(batchVO);
+        }
+        
+        log.info("查询订单变更记录完成, orderId={}, batchCount={}", orderId, batchList.size());
+        return batchList;
+    }
+
+    @Override
+    public OrderDetailWithChangeLogVO getOrderDetailWithChangeLog(Integer orderId) {
+        log.info("查询订单详情(包含变更记录), orderId={}", orderId);
+
+        // 1. 查询订单基本信息
+        StoreOrder order = this.getById(orderId);
+        if (order == null || order.getDeleteFlag() == 1) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        // 2. 查询门店信息
+        StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+        String storeName = storeInfo != null ? storeInfo.getStoreName() : "";
+
+        // 3. 查询优惠券信息(如果有)
+        String couponName = null;
+        if (order.getCouponId() != null) {
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            if (coupon != null) {
+                couponName = coupon.getName();
+            }
+        }
+
+        // 4. 查询订单变更记录(按批次分组)
+        List<OrderChangeLogBatchVO> changeLogBatches = getOrderChangeLogs(orderId);
+
+        // 5. 组装OrderDetailWithChangeLogVO
+        OrderDetailWithChangeLogVO vo = new OrderDetailWithChangeLogVO();
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setStoreName(storeName);
+        vo.setTableNumber(order.getTableNumber());
+        vo.setDinerCount(order.getDinerCount());
+        vo.setContactPhone(order.getContactPhone());
+        vo.setRemark(order.getRemark());
+        vo.setTotalAmount(order.getTotalAmount());
+        vo.setTablewareFee(order.getTablewareFee());
+        vo.setCouponId(order.getCouponId());
+        vo.setCouponName(couponName);
+        vo.setDiscountAmount(order.getDiscountAmount());
+        vo.setPayAmount(order.getPayAmount());
+        vo.setOrderStatus(order.getOrderStatus());
+        vo.setPayStatus(order.getPayStatus());
+        vo.setPayType(order.getPayType());
+        vo.setCreatedTime(order.getCreatedTime());
+        vo.setPayTime(order.getPayTime());
+        vo.setChangeLogBatches(changeLogBatches);
+
+        log.info("查询订单详情(包含变更记录)完成, orderId={}, batchCount={}", orderId, changeLogBatches.size());
+        return vo;
+    }
+    
+    /**
+     * 获取操作类型文本
+     */
+    private String getOperationTypeText(Integer operationType) {
+        if (operationType == null) {
+            return "未知";
+        }
+        switch (operationType) {
+            case 1:
+                return "首次下单";
+            case 3:
+                return "更新订单";
+            default:
+                return "未知";
+        }
+    }
+
+    /**
+     * 记录订单变更日志
+     *
+     * @param orderId 订单ID
+     * @param orderNo 订单号
+     * @param items 商品列表
+     * @param operationType 操作类型(1:首次下单, 3:更新订单)
+     * @param operationTime 操作时间
+     * @param userId 操作人ID
+     * @param userPhone 操作人手机号
+     */
+    private void recordOrderChangeLog(Integer orderId, String orderNo, List<shop.alien.entity.store.dto.CartItemDTO> items,
+                                      Integer operationType, Date operationTime, Integer userId, String userPhone) {
+        if (items == null || items.isEmpty()) {
+            log.warn("商品列表为空,不记录变更日志, orderId={}", orderId);
+            return;
+        }
+
+        // 生成批次号:ORDER{orderId}_{yyyyMMddHHmmss}
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        String timestamp = sdf.format(operationTime);
+        String batchNo = "ORDER" + orderId + "_" + timestamp;
+
+        List<StoreOrderChangeLog> changeLogs = new ArrayList<>();
+
+        for (shop.alien.entity.store.dto.CartItemDTO item : items) {
+            Integer lockedQuantity = item.getLockedQuantity(); // 已下单数量
+            Integer currentQuantity = item.getQuantity(); // 当前数量
+            Integer quantityChange = 0; // 数量变化
+            Integer quantityBefore = 0; // 变化前数量
+            Integer quantityAfter = 0; // 变化后数量
+
+            if (operationType == 1) {
+                // 首次下单:记录所有商品
+                quantityBefore = 0;
+                quantityAfter = currentQuantity != null ? currentQuantity : 0;
+                quantityChange = quantityAfter;
+            } else if (operationType == 3) {
+                // 更新订单:只记录新增的商品或数量增加的商品
+                if (lockedQuantity == null || lockedQuantity == 0) {
+                    // 新增商品
+                    quantityBefore = 0;
+                    quantityAfter = currentQuantity != null ? currentQuantity : 0;
+                    quantityChange = quantityAfter;
+                } else if (currentQuantity != null && currentQuantity > lockedQuantity) {
+                    // 数量增加
+                    quantityBefore = lockedQuantity;
+                    quantityAfter = currentQuantity;
+                    quantityChange = quantityAfter - quantityBefore;
+                } else {
+                    // 没有变化,跳过
+                    continue;
+                }
+            }
+
+            // 只记录有数量变化的商品
+            if (quantityChange == null || quantityChange == 0) {
+                continue;
+            }
+
+            // 计算金额变化
+            BigDecimal amountChange = item.getUnitPrice() != null
+                    ? item.getUnitPrice().multiply(BigDecimal.valueOf(quantityChange))
+                    : BigDecimal.ZERO;
+
+            StoreOrderChangeLog changeLog = new StoreOrderChangeLog();
+            changeLog.setOrderId(orderId);
+            changeLog.setOrderNo(orderNo);
+            changeLog.setBatchNo(batchNo);
+            changeLog.setOperationType(operationType);
+            changeLog.setCuisineId(item.getCuisineId());
+            changeLog.setCuisineName(item.getCuisineName());
+            changeLog.setCuisineType(item.getCuisineType());
+            changeLog.setCuisineImage(item.getCuisineImage());
+            changeLog.setUnitPrice(item.getUnitPrice());
+            changeLog.setQuantityChange(quantityChange);
+            changeLog.setQuantityBefore(quantityBefore);
+            changeLog.setQuantityAfter(quantityAfter);
+            changeLog.setAmountChange(amountChange);
+            changeLog.setOperationTime(operationTime);
+            changeLog.setOperatorUserId(userId);
+            changeLog.setOperatorUserPhone(userPhone);
+            changeLog.setRemark(item.getRemark());
+            changeLog.setCreatedUserId(userId);
+            changeLog.setCreatedTime(operationTime);
+            changeLog.setUpdatedTime(operationTime);
+
+            changeLogs.add(changeLog);
+        }
+
+        // 批量插入变更记录(使用 MyBatis-Plus 的批量插入)
+        if (!changeLogs.isEmpty()) {
+            // 使用循环插入(MyBatis-Plus 的 BaseMapper 没有批量插入方法,需要手动实现或使用 ServiceImpl 的 saveBatch)
+            // 注意:如果变更记录数量很大,可以考虑使用 MyBatis 的批量插入
+            for (StoreOrderChangeLog log : changeLogs) {
+                orderChangeLogMapper.insert(log);
+            }
+            log.info("记录订单变更日志完成, orderId={}, batchNo={}, operationType={}, itemCount={}",
+                    orderId, batchNo, operationType, changeLogs.size());
+        } else {
+            log.warn("没有需要记录的变更, orderId={}, operationType={}", orderId, operationType);
+        }
+    }
+
+    /**
+     * 生成订单号
+     */
+    private String generateOrderNo() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        String timestamp = sdf.format(new Date());
+        String random = String.valueOf((int) (Math.random() * 10000));
+        return "ORD" + timestamp + String.format("%04d", Integer.parseInt(random));
+    }
+
+
+    @Override
+    public void migrateTableData(Integer fromTableId, Integer toTableId, Integer userId) {
+        log.info("换桌迁移数据, fromTableId={}, toTableId={}, userId={}", fromTableId, toTableId, userId);
+
+        // 1. 查询原桌号信息
+        StoreTable fromTable = storeTableMapper.selectById(fromTableId);
+        if (fromTable == null) {
+            throw new RuntimeException("原桌号不存在");
+        }
+
+        // 2. 查询目标桌号信息
+        StoreTable toTable = storeTableMapper.selectById(toTableId);
+        if (toTable == null) {
+            throw new RuntimeException("目标桌号不存在");
+        }
+
+        // 3. 迁移未完成的订单(orderStatus=0 且 payStatus=0)
+        if (fromTable.getCurrentOrderId() != null) {
+            StoreOrder pendingOrder = this.getById(fromTable.getCurrentOrderId());
+            if (pendingOrder != null && pendingOrder.getOrderStatus() == 0 && pendingOrder.getPayStatus() == 0) {
+                // 更新订单的桌号信息
+                pendingOrder.setTableId(toTableId);
+                pendingOrder.setTableNumber(toTable.getTableNumber());
+                pendingOrder.setUpdatedUserId(userId);
+                pendingOrder.setUpdatedTime(new Date());
+                this.updateById(pendingOrder);
+                log.info("迁移未完成订单, orderId={}, fromTableId={}, toTableId={}", pendingOrder.getId(), fromTableId, toTableId);
+
+                // 更新订单明细的订单号(如果需要,但通常订单号不变,所以这里只更新 table_id 关联)
+                // 订单明细通过 order_id 关联,不需要单独更新
+            }
+        }
+
+        // 4. 迁移所有未完成的订单(包括可能存在的其他未完成订单)
+        LambdaQueryWrapper<StoreOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(StoreOrder::getTableId, fromTableId);
+        orderWrapper.eq(StoreOrder::getOrderStatus, 0); // 待支付
+        orderWrapper.eq(StoreOrder::getPayStatus, 0); // 未支付
+        orderWrapper.eq(StoreOrder::getDeleteFlag, 0);
+        List<StoreOrder> pendingOrders = this.list(orderWrapper);
+        
+        if (pendingOrders != null && !pendingOrders.isEmpty()) {
+            for (StoreOrder order : pendingOrders) {
+                order.setTableId(toTableId);
+                order.setTableNumber(toTable.getTableNumber());
+                order.setUpdatedUserId(userId);
+                order.setUpdatedTime(new Date());
+            }
+            this.updateBatchById(pendingOrders);
+            log.info("迁移未完成订单, count={}, fromTableId={}, toTableId={}", pendingOrders.size(), fromTableId, toTableId);
+        }
+
+        // 5. 迁移优惠券使用记录
+        LambdaQueryWrapper<StoreCouponUsage> couponUsageWrapper = new LambdaQueryWrapper<>();
+        couponUsageWrapper.eq(StoreCouponUsage::getTableId, fromTableId);
+        couponUsageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+        couponUsageWrapper.in(StoreCouponUsage::getUsageStatus, 0, 1, 2); // 已标记使用、已下单、已支付
+        List<StoreCouponUsage> couponUsages = storeCouponUsageMapper.selectList(couponUsageWrapper);
+        
+        if (couponUsages != null && !couponUsages.isEmpty()) {
+            Date now = new Date();
+            for (StoreCouponUsage usage : couponUsages) {
+                usage.setFromTableId(fromTableId);
+                usage.setTableId(toTableId);
+                usage.setMigrateTime(now);
+                usage.setUpdatedUserId(userId);
+                usage.setUpdatedTime(now);
+            }
+            // 批量更新
+            for (StoreCouponUsage usage : couponUsages) {
+                storeCouponUsageMapper.updateById(usage);
+            }
+            log.info("迁移优惠券使用记录, count={}, fromTableId={}, toTableId={}", couponUsages.size(), fromTableId, toTableId);
+        }
+
+        // 6. 更新桌号表的关联信息
+        // 原桌号:清空 currentOrderId 和 currentCouponId
+        LambdaUpdateWrapper<StoreTable> fromTableWrapper = new LambdaUpdateWrapper<>();
+        fromTableWrapper.eq(StoreTable::getId, fromTableId)
+                .set(StoreTable::getCurrentOrderId, null)
+                .set(StoreTable::getCurrentCouponId, null)
+                .set(StoreTable::getUpdatedTime, new Date());
+        if (userId != null) {
+            fromTableWrapper.set(StoreTable::getUpdatedUserId, userId);
+        }
+        storeTableMapper.update(null, fromTableWrapper);
+        log.info("清空原桌号关联信息, fromTableId={}", fromTableId);
+
+        // 目标桌号:设置 currentOrderId 和 currentCouponId(如果有)
+        if (fromTable.getCurrentOrderId() != null || fromTable.getCurrentCouponId() != null) {
+            LambdaUpdateWrapper<StoreTable> toTableWrapper = new LambdaUpdateWrapper<>();
+            toTableWrapper.eq(StoreTable::getId, toTableId);
+            
+            if (fromTable.getCurrentOrderId() != null) {
+                toTableWrapper.set(StoreTable::getCurrentOrderId, fromTable.getCurrentOrderId());
+            }
+            if (fromTable.getCurrentCouponId() != null) {
+                toTableWrapper.set(StoreTable::getCurrentCouponId, fromTable.getCurrentCouponId());
+            }
+            toTableWrapper.set(StoreTable::getUpdatedTime, new Date());
+            if (userId != null) {
+                toTableWrapper.set(StoreTable::getUpdatedUserId, userId);
+            }
+            storeTableMapper.update(null, toTableWrapper);
+            log.info("设置目标桌号关联信息, toTableId={}, currentOrderId={}, currentCouponId={}", 
+                    toTableId, fromTable.getCurrentOrderId(), fromTable.getCurrentCouponId());
+        }
+
+        log.info("换桌数据迁移完成, fromTableId={}, toTableId={}", fromTableId, toTableId);
+    }
+
+    @Override
+    public shop.alien.entity.store.dto.CartDTO changeTable(Integer fromTableId, Integer toTableId, String changeReason, Integer userId) {
+        log.info("换桌, fromTableId={}, toTableId={}, changeReason={}, userId={}", fromTableId, toTableId, changeReason, userId);
+
+        // 0. 校验:目标桌只能是空桌(空闲且无当前订单)
+        if (fromTableId.equals(toTableId)) {
+            throw new RuntimeException("原桌号与目标桌号不能相同");
+        }
+        StoreTable fromTable = storeTableMapper.selectById(fromTableId);
+        if (fromTable == null) {
+            throw new RuntimeException("原桌号不存在");
+        }
+        StoreTable toTable = storeTableMapper.selectById(toTableId);
+        if (toTable == null) {
+            throw new RuntimeException("目标桌号不存在");
+        }
+        if (!fromTable.getStoreId().equals(toTable.getStoreId())) {
+            throw new RuntimeException("原桌号与目标桌号须在同一门店");
+        }
+        // 空桌:状态为空闲(0)且无当前订单
+        boolean emptyStatus = (toTable.getStatus() == null || toTable.getStatus() == 0);
+        boolean noOrder = (toTable.getCurrentOrderId() == null);
+        if (!emptyStatus || !noOrder) {
+            throw new RuntimeException("只能换到空桌,请选择空闲且无订单的桌号");
+        }
+
+        // 1. 迁移购物车
+        shop.alien.entity.store.dto.CartDTO cart = cartService.migrateCart(fromTableId, toTableId);
+
+        // 2. 迁移所有关联数据(订单、订单变更记录、优惠券使用记录等)
+        migrateTableData(fromTableId, toTableId, userId);
+
+        // 3. 记录换桌日志(fromTable、toTable 已在步骤0中查询)
+        Date now = new Date();
+        StoreTableLog tableLog = new StoreTableLog();
+        tableLog.setStoreId(cart.getStoreId());
+        tableLog.setOrderId(fromTable.getCurrentOrderId()); // 有订单则记录,仅购物车换桌时为 null
+        tableLog.setFromTableId(fromTableId);
+        tableLog.setFromTableNumber(fromTable != null ? fromTable.getTableNumber() : null);
+        tableLog.setToTableId(toTableId);
+        tableLog.setToTableNumber(toTable != null ? toTable.getTableNumber() : null);
+        tableLog.setChangeReason(changeReason);
+        tableLog.setCreatedUserId(userId);
+        tableLog.setCreatedTime(now);
+        tableLog.setUpdatedTime(now);
+        tableLog.setUpdatedUserId(userId);
+        storeTableLogMapper.insert(tableLog);
+
+        // 4. 推送购物车更新消息到新桌号
+        sseService.pushCartUpdate(toTableId, cart);
+
+        log.info("换桌完成, fromTableId={}, toTableId={}", fromTableId, toTableId);
+        return cart;
+    }
+
+    /**
+     * 验证订单是否存在且状态正确(用于支付、取消、加餐、更新优惠券等操作)
+     *
+     * @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           优惠券对象
+     * @param totalWithTableware 订单总金额(含餐具费)
+     * @return 优惠金额
+     */
+    private BigDecimal calculateDiscountAmount(LifeDiscountCoupon coupon, BigDecimal totalWithTableware) {
+        if (coupon == null || totalWithTableware == null || totalWithTableware.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+
+        Integer couponType = coupon.getCouponType();
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponType != null && couponType == 2) {
+            // 折扣券:根据折扣率计算优惠金额
+            // discountRate: 0-100,例如80表示8折,优惠金额 = 订单金额 * (100 - discountRate) / 100
+            BigDecimal discountRate = coupon.getDiscountRate();
+            if (discountRate != null && discountRate.compareTo(BigDecimal.ZERO) > 0 && discountRate.compareTo(new BigDecimal(100)) <= 0) {
+                // 计算折扣后的金额
+                BigDecimal discountedAmount = totalWithTableware.multiply(discountRate).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
+                // 优惠金额 = 原价 - 折扣后价格
+                discountAmount = totalWithTableware.subtract(discountedAmount);
+            }
+        } else {
+            // 满减券(默认或couponType=1):使用nominalValue
+            discountAmount = coupon.getNominalValue();
+            if (discountAmount == null) {
+                discountAmount = BigDecimal.ZERO;
+            }
+            // 优惠金额不能超过订单总金额
+            if (discountAmount.compareTo(totalWithTableware) > 0) {
+                discountAmount = totalWithTableware;
+            }
+        }
+
+        return discountAmount;
+    }
+
+}

+ 227 - 0
alien-store/src/main/java/shop/alien/store/util/TokenUtil.java

@@ -0,0 +1,227 @@
+package shop.alien.store.util;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Token 工具类
+ * 用于统一处理 token 的获取和解析
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+public class TokenUtil {
+
+    /**
+     * 去除 token 中的 "Bearer " 前缀
+     *
+     * @param token 原始 token 字符串
+     * @return 去除前缀后的 token
+     */
+    private static String removeBearerPrefix(String token) {
+        if (StringUtils.isBlank(token)) {
+            return token;
+        }
+        // 去除首尾空格
+        token = token.trim();
+        
+        // 去除 "Bearer " 前缀(不区分大小写)
+        if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
+            token = token.substring(7).trim();
+        }
+        
+        return token;
+    }
+
+    /**
+     * 从请求头获取当前用户信息
+     * 兼容新的 token 格式(包含 openid)和旧格式
+     *
+     * @return 用户信息 JSONObject,包含 userId、openid、phone、userName 等,如果获取失败返回 null
+     */
+    public static JSONObject getCurrentUserInfo() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes == null) {
+                return null;
+            }
+
+            HttpServletRequest request = attributes.getRequest();
+            String token = request.getHeader("Authorization");
+            
+            if (StringUtils.isBlank(token)) {
+                return null;
+            }
+
+            // 去除 "Bearer " 前缀
+            token = removeBearerPrefix(token);
+            
+            if (StringUtils.isBlank(token)) {
+                log.warn("Token 去除前缀后为空");
+                return null;
+            }
+
+            // 解析 token,验证格式和签名(与 verifyToken 方法保持一致)
+            io.jsonwebtoken.Claims claims = JwtUtil.parseJWT(token);
+            
+            // 从 token 中提取用户信息(与 verifyToken 方法保持一致)
+            String sub = claims.get("sub").toString();
+            JSONObject userInfo = JSONObject.parseObject(sub);
+            
+            return userInfo;
+        } catch (io.jsonwebtoken.ExpiredJwtException e) {
+            log.warn("Token 已过期: {}", e.getMessage());
+            return null;
+        } catch (io.jsonwebtoken.MalformedJwtException e) {
+            log.error("Token 格式错误: {}", e.getMessage());
+            return null;
+        } catch (io.jsonwebtoken.SignatureException e) {
+            log.error("Token 签名验证失败: {}", e.getMessage());
+            return null;
+        } catch (Exception e) {
+            log.warn("获取当前用户信息失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 获取当前用户ID
+     *
+     * @return 用户ID,如果获取失败返回 null
+     */
+    public static Integer getCurrentUserId() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            log.debug("getCurrentUserInfo() 返回 null,无法获取 userId");
+            return null;
+        }
+        
+        try {
+            String userIdStr = userInfo.getString("userId");
+            if (StringUtils.isBlank(userIdStr)) {
+                log.warn("Token 中的 userId 字段为空或不存在,userInfo keys: {}", userInfo.keySet());
+                return null;
+            }
+            Integer userId = Integer.parseInt(userIdStr);
+            log.debug("成功获取 userId: {}", userId);
+            return userId;
+        } catch (NumberFormatException e) {
+            log.error("解析 userId 失败,userIdStr: {}, 错误: {}", userInfo.getString("userId"), e.getMessage());
+        } catch (Exception e) {
+            log.error("获取 userId 时发生异常: {}", e.getMessage(), e);
+        }
+        
+        return null;
+    }
+
+    /**
+     * 获取当前用户的 openid
+     *
+     * @return openid,如果获取失败返回 null
+     */
+    public static String getCurrentUserOpenid() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            return null;
+        }
+        return userInfo.getString("openid");
+    }
+
+    /**
+     * 获取当前用户的手机号
+     *
+     * @return 手机号,如果获取失败返回 null
+     */
+    public static String getCurrentUserPhone() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            return null;
+        }
+        return userInfo.getString("phone");
+    }
+
+    /**
+     * 检查是否有有效的 token
+     *
+     * @return 如果有有效的 token 返回 true,否则返回 false
+     */
+    public static boolean hasValidToken() {
+        JSONObject userInfo = getCurrentUserInfo();
+        return userInfo != null && userInfo.containsKey("userId");
+    }
+
+    /**
+     * 从 token 字符串直接解析用户信息(用于调试)
+     *
+     * @param token Token 字符串
+     * @return 用户信息 JSONObject,如果解析失败返回 null
+     */
+    public static JSONObject parseToken(String token) {
+        if (StringUtils.isBlank(token)) {
+            log.warn("Token 字符串为空");
+            return null;
+        }
+
+        try {
+            // 去除 "Bearer " 前缀
+            token = removeBearerPrefix(token);
+            
+            if (StringUtils.isBlank(token)) {
+                log.warn("Token 去除前缀后为空");
+                return null;
+            }
+            
+            log.info("开始解析 token: {}", token.length() > 30 ? token.substring(0, 30) + "..." : token);
+
+            // 解析 token
+            io.jsonwebtoken.Claims claims = JwtUtil.parseJWT(token);
+            if (claims == null) {
+                log.warn("Token 解析失败,claims 为 null");
+                return null;
+            }
+
+            log.info("Token claims keys: {}", claims.keySet());
+
+            // 从 token 中提取用户信息
+            Object subObj = claims.get("sub");
+            if (subObj == null) {
+                log.warn("Token claims 中没有 'sub' 字段");
+                return null;
+            }
+
+            String sub;
+            if (subObj instanceof String) {
+                sub = (String) subObj;
+            } else {
+                sub = subObj.toString();
+            }
+
+            log.info("Token sub 内容: {}", sub);
+
+            JSONObject userInfo = JSONObject.parseObject(sub);
+            if (userInfo == null) {
+                log.warn("解析 sub 为 JSONObject 返回 null");
+                return null;
+            }
+
+            log.info("成功解析用户信息: userId={}, openid={}, phone={}, 所有 keys: {}",
+                    userInfo.getString("userId"),
+                    userInfo.getString("openid"),
+                    userInfo.getString("phone"),
+                    userInfo.keySet());
+
+            return userInfo;
+        } catch (Exception e) {
+            log.error("解析 token 失败: {}, 异常类型: {}", e.getMessage(), e.getClass().getName(), e);
+            return null;
+        }
+    }
+}

+ 375 - 0
alien-store/src/main/java/shop/alien/store/util/WeChatMiniProgramUtil.java

@@ -0,0 +1,375 @@
+package shop.alien.store.util;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.store.config.BaseRedisService;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 微信小程序工具类
+ * 用于调用微信小程序相关接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WeChatMiniProgramUtil {
+
+    @Value("${wechat.miniprogram.appId}")
+    private String appId;
+
+    @Value("${wechat.miniprogram.appSecret}")
+    private String appSecret;
+
+    private final BaseRedisService baseRedisService;
+
+    /**
+     * access_token 缓存键前缀
+     */
+    private static final String ACCESS_TOKEN_CACHE_KEY = "wechat:access_token:";
+    
+    /**
+     * access_token 有效期(秒),微信官方为7200秒,这里设置为7000秒,提前刷新
+     */
+    private static final long ACCESS_TOKEN_EXPIRE_TIME = 7000L;
+
+    /**
+     * 微信小程序登录接口地址
+     */
+    private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
+
+    /**
+     * 微信小程序手机号获取接口地址(新版本)
+     */
+    private static final String CODE2VERIFYINFO_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
+
+    /**
+     * 通过 code 获取 openid 和 session_key
+     *
+     * @param code 小程序端通过 wx.login() 获取的 code
+     * @return WeChatSessionInfo 包含 openid、session_key、unionid 等信息
+     */
+    public WeChatSessionInfo code2Session(String code) {
+        String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+                CODE2SESSION_URL, appId, appSecret, code);
+
+        HttpClient httpClient = HttpClients.createDefault();
+        HttpGet httpGet = new HttpGet(url);
+
+        try {
+            HttpResponse response = httpClient.execute(httpGet);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            log.info("微信 code2session 响应: {}", responseBody);
+
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+
+            // 检查是否有错误
+            if (jsonObject.containsKey("errcode")) {
+                Integer errcode = jsonObject.getInteger("errcode");
+                String errmsg = jsonObject.getString("errmsg");
+                
+                // 将错误信息保存到返回对象中,方便上层处理
+                WeChatSessionInfo errorInfo = new WeChatSessionInfo();
+                errorInfo.setErrcode(errcode);
+                errorInfo.setErrmsg(errmsg);
+                
+                // 根据错误码记录详细的错误信息
+                String errorDetail = getErrorDetail(errcode, errmsg);
+                log.error("微信 code2session 失败: errcode={}, errmsg={}, 详情={}", errcode, errmsg, errorDetail);
+                return errorInfo;
+            }
+
+            // 解析返回数据
+            WeChatSessionInfo sessionInfo = new WeChatSessionInfo();
+            sessionInfo.setOpenid(jsonObject.getString("openid"));
+            sessionInfo.setSessionKey(jsonObject.getString("session_key"));
+            sessionInfo.setUnionid(jsonObject.getString("unionid"));
+
+            return sessionInfo;
+        } catch (IOException e) {
+            log.error("调用微信 code2session 接口异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取微信错误码的详细说明
+     */
+    private String getErrorDetail(Integer errcode, String errmsg) {
+        if (errcode == null) {
+            return "未知错误";
+        }
+        
+        switch (errcode) {
+            case 40029:
+                return "code无效(可能原因:1. code已使用过,每个code只能使用一次;2. code已过期,有效期5分钟;3. code格式错误;4. appId或appSecret配置错误)";
+            case 45011:
+                return "频率限制,每个用户每分钟最多调用5次";
+            case 40226:
+                return "高风险等级用户,需要用户进行验证";
+            case 40013:
+                return "appId无效,请检查Nacos配置中的wechat.miniprogram.appId";
+            case 40125:
+                return "appSecret无效,请检查Nacos配置中的wechat.miniprogram.appSecret";
+            case -1:
+                return "系统繁忙,请稍后重试";
+            default:
+                return String.format("微信接口错误,错误码:%d,错误信息:%s", errcode, errmsg);
+        }
+    }
+
+    /**
+     * 解密微信小程序加密数据(手机号等)
+     * 使用 AES-128-CBC 算法,使用 session_key 作为密钥
+     *
+     * @param encryptedData 加密数据(Base64编码)
+     * @param sessionKey    session_key(Base64编码)
+     * @param iv           初始向量(Base64编码)
+     * @return 解密后的JSON字符串,包含手机号等信息
+     */
+    public String decryptData(String encryptedData, String sessionKey, String iv) {
+        try {
+            if (encryptedData == null || encryptedData.isEmpty()) {
+                log.warn("加密数据为空,无法解密");
+                return null;
+            }
+            if (sessionKey == null || sessionKey.isEmpty()) {
+                log.warn("session_key为空,无法解密");
+                return null;
+            }
+            if (iv == null || iv.isEmpty()) {
+                log.warn("初始向量为空,无法解密");
+                return null;
+            }
+
+            // Base64解码
+            byte[] dataByte = Base64.decodeBase64(encryptedData);
+            byte[] keyByte = Base64.decodeBase64(sessionKey);
+            byte[] ivByte = Base64.decodeBase64(iv);
+
+            // AES-128-CBC 解密
+            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
+            IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
+            cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec);
+
+            byte[] decrypted = cipher.doFinal(dataByte);
+            String result = new String(decrypted, StandardCharsets.UTF_8);
+            
+            log.info("微信加密数据解密成功");
+            return result;
+        } catch (Exception e) {
+            log.error("微信加密数据解密失败: encryptedData={}, error={}", 
+                    encryptedData != null ? encryptedData.substring(0, Math.min(20, encryptedData.length())) : "null", 
+                    e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 从解密后的数据中提取手机号(旧版本,已废弃)
+     *
+     * @param decryptedData 解密后的JSON字符串
+     * @return 手机号
+     */
+    @Deprecated
+    public String extractPhoneNumber(String decryptedData) {
+        try {
+            if (decryptedData == null || decryptedData.isEmpty()) {
+                return null;
+            }
+            
+            JSONObject jsonObject = JSONObject.parseObject(decryptedData);
+            String phoneNumber = jsonObject.getString("phoneNumber");
+            
+            log.info("从解密数据中提取手机号: {}", phoneNumber != null ? phoneNumber.substring(0, Math.min(7, phoneNumber.length())) + "****" : "null");
+            return phoneNumber;
+        } catch (Exception e) {
+            log.error("提取手机号失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 通过手机号凭证 code 获取手机号(新版本API)
+     * 使用 code2Verifyinfo 接口
+     *
+     * @param phoneCode 手机号凭证 code(通过wx.getPhoneNumber()获取)
+     * @return 手机号,如果获取失败返回null
+     */
+    public String getPhoneNumberByCode(String phoneCode) {
+        if (phoneCode == null || phoneCode.isEmpty()) {
+            log.warn("手机号凭证code为空");
+            return null;
+        }
+
+        try {
+            // 获取 access_token
+            String accessToken = getAccessToken();
+            if (accessToken == null || accessToken.isEmpty()) {
+                log.error("获取access_token失败,无法调用手机号接口");
+                return null;
+            }
+
+            // 构建请求URL
+            String url = String.format("%s?access_token=%s", CODE2VERIFYINFO_URL, accessToken);
+            
+            // 构建请求体
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("code", phoneCode);
+
+            // 使用HttpClient发送POST请求
+            HttpClient httpClient = HttpClients.createDefault();
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setHeader("Content-Type", "application/json");
+            httpPost.setEntity(new StringEntity(requestBody.toJSONString(), "UTF-8"));
+
+            HttpResponse response = httpClient.execute(httpPost);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            log.info("微信 code2Verifyinfo 响应: {}", responseBody);
+
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+
+            // 检查是否有错误(errcode 为 0 表示成功,非 0 才视为失败)
+            if (jsonObject.containsKey("errcode")) {
+                Integer errcode = jsonObject.getInteger("errcode");
+                if (errcode != null && errcode != 0) {
+                    String errmsg = jsonObject.getString("errmsg");
+                    log.error("微信 code2Verifyinfo 失败: errcode={}, errmsg={}", errcode, errmsg);
+                    return null;
+                }
+            }
+
+            // 解析返回数据
+            JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
+            if (phoneInfo != null) {
+                String phoneNumber = phoneInfo.getString("phoneNumber");
+                log.info("成功获取手机号");
+                return phoneNumber;
+            }
+
+            return null;
+        } catch (Exception e) {
+            log.error("调用微信 code2Verifyinfo 接口异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取微信 access_token(带缓存机制)
+     * access_token有效期为7200秒,这里缓存7000秒,提前刷新避免过期
+     * 
+     * @return access_token,获取失败返回null
+     */
+    private String getAccessToken() {
+        // 1. 先从缓存获取
+        String cacheKey = ACCESS_TOKEN_CACHE_KEY + appId;
+        String cachedToken = baseRedisService.getString(cacheKey);
+        if (cachedToken != null && !cachedToken.isEmpty()) {
+            log.debug("从缓存获取access_token成功");
+            return cachedToken;
+        }
+
+        // 2. 缓存未命中,请求微信接口
+        try {
+            String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                    appId, appSecret);
+            
+            HttpClient httpClient = HttpClients.createDefault();
+            HttpGet httpGet = new HttpGet(url);
+            HttpResponse response = httpClient.execute(httpGet);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            
+            log.info("微信获取access_token响应: {}", responseBody);
+            
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+            if (jsonObject.containsKey("access_token")) {
+                String accessToken = jsonObject.getString("access_token");
+                
+                // 3. 存入缓存(有效期7000秒,提前刷新)
+                baseRedisService.setString(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_TIME);
+                log.info("成功获取access_token并存入缓存");
+                return accessToken;
+            } else {
+                Integer errcode = jsonObject.getInteger("errcode");
+                String errmsg = jsonObject.getString("errmsg");
+                log.error("获取access_token失败: errcode={}, errmsg={}", errcode, errmsg);
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("获取access_token异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 微信会话信息
+     */
+    public static class WeChatSessionInfo {
+        private String openid;
+        private String sessionKey;
+        private String unionid;
+        private Integer errcode;
+        private String errmsg;
+
+        public String getOpenid() {
+            return openid;
+        }
+
+        public void setOpenid(String openid) {
+            this.openid = openid;
+        }
+
+        public String getSessionKey() {
+            return sessionKey;
+        }
+
+        public void setSessionKey(String sessionKey) {
+            this.sessionKey = sessionKey;
+        }
+
+        public String getUnionid() {
+            return unionid;
+        }
+
+        public void setUnionid(String unionid) {
+            this.unionid = unionid;
+        }
+
+        public Integer getErrcode() {
+            return errcode;
+        }
+
+        public void setErrcode(Integer errcode) {
+            this.errcode = errcode;
+        }
+
+        public String getErrmsg() {
+            return errmsg;
+        }
+
+        public void setErrmsg(String errmsg) {
+            this.errmsg = errmsg;
+        }
+    }
+}
+

+ 65 - 0
alien-store/src/main/java/shop/alien/store/util/WeChatPayUtil.java

@@ -0,0 +1,65 @@
+package shop.alien.store.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 微信支付工具类
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+public class WeChatPayUtil {
+    // TODO: 实现微信支付相关工具方法
+
+    // AES-GCM模式的TAG长度(微信固定为128位/16字节)
+    private static final int GCM_TAG_LENGTH = 128;
+    // AES-256算法要求密钥长度为32字节(APIv3密钥必须是32位)
+    private static final int API_V3_KEY_LENGTH = 32;
+
+    /**
+     * 解密微信支付resource字段密文
+     * @param apiV3Key 商户平台设置的APIv3密钥(32位字符串)
+     * @param nonce resource.nonce(加密随机串)
+     * @param associatedData resource.associated_data(关联数据)
+     * @param ciphertext resource.ciphertext(Base64编码的密文)
+     * @return 解密后的明文(JSON格式业务信息)
+     * @throws Exception 解密异常(密钥错误、参数非法等)
+     */
+    public static String decrypt(String apiV3Key, String nonce, String associatedData, String ciphertext) throws Exception {
+        // 1. 校验APIv3密钥长度(必须32字节)
+        if (apiV3Key == null || apiV3Key.length() != API_V3_KEY_LENGTH) {
+            throw new IllegalArgumentException("APIv3密钥必须是32位字符串");
+        }
+
+        // 2. 转换密钥为SecretKeySpec(AES-256)
+        SecretKeySpec secretKeySpec = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
+
+        // 3. Base64解密密文
+        byte[] ciphertextBytes = Base64.decodeBase64(ciphertext);
+
+        // 4. 初始化GCM参数(nonce为16字节?微信实际是12字节,符合rfc5116)
+        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce.getBytes(StandardCharsets.UTF_8));
+
+        // 5. 初始化AES-GCM解密器
+        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
+
+        // 6. 设置关联数据(AEAD附加数据)
+        cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
+
+        // 7. 执行解密
+        byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
+
+        // 8. 转换为UTF-8明文(JSON格式)
+        return new String(plaintextBytes, StandardCharsets.UTF_8);
+    }
+}
+

+ 76 - 0
alien-store/src/main/java/shop/alien/store/vo/DiningUserVo.java

@@ -0,0 +1,76 @@
+package shop.alien.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 点餐用户VO
+ * 基于 LifeUser 表
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("点餐用户信息")
+public class DiningUserVo {
+
+    @ApiModelProperty("用户ID")
+    private Long id;
+
+    @ApiModelProperty("用户昵称")
+    private String nickName;
+
+    @ApiModelProperty("用户头像")
+    private String avatarUrl;
+
+    @ApiModelProperty("手机号")
+    private String phone;
+
+    @ApiModelProperty("性别")
+    private String gender;
+
+    @ApiModelProperty("生日")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date birthday;
+
+    @ApiModelProperty("真实姓名")
+    private String realName;
+
+    @ApiModelProperty("省份")
+    private String province;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("区县")
+    private String district;
+
+    @ApiModelProperty("详细地址")
+    private String address;
+
+    @ApiModelProperty("个人简介")
+    private String jianjie;
+
+    @ApiModelProperty("用户状态:0-正常,1-禁用")
+    private Integer status;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty("最后登录时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastLoginTime;
+
+    @ApiModelProperty("JWT Token")
+    private String token;
+
+    @ApiModelProperty("微信OpenID")
+    private String openId;
+}
+

+ 40 - 0
alien-store/src/main/java/shop/alien/store/vo/TokenVerifyVo.java

@@ -0,0 +1,40 @@
+package shop.alien.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Token 校验结果VO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("Token校验结果")
+public class TokenVerifyVo {
+
+    @ApiModelProperty(value = "Token是否有效")
+    private Boolean valid;
+
+    @ApiModelProperty(value = "用户ID")
+    private Long userId;
+
+    @ApiModelProperty(value = "用户手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "用户昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "微信OpenID")
+    private String openid;
+
+    @ApiModelProperty(value = "Token过期时间")
+    private Date expirationTime;
+
+    @ApiModelProperty(value = "验证失败原因")
+    private String reason;
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.