Sfoglia il codice sorgente

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

# Conflicts:
#	alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java
刘云鑫 1 mese fa
parent
commit
b09c9fcaf6
100 ha cambiato i file con 15759 aggiunte e 19 eliminazioni
  1. 1270 0
      SSE前后端使用说明书.md
  2. 28 0
      alien-dining/README.md
  3. 15 0
      alien-dining/doc/gateway-route-example.yml
  4. 84 0
      alien-dining/doc/nacos-config-example.yml
  5. 82 0
      alien-dining/doc/nacos-config-ready.yml
  6. 152 0
      alien-dining/doc/订单变更记录表使用说明.md
  7. 253 0
      alien-dining/docs/扫码点餐商家功能实现情况检查报告.md
  8. 354 0
      alien-dining/pom.xml
  9. 24 0
      alien-dining/src/main/java/shop/alien/dining/AlienDiningApplication.java
  10. 26 0
      alien-dining/src/main/java/shop/alien/dining/annotation/OperationLog.java
  11. 28 0
      alien-dining/src/main/java/shop/alien/dining/aspect/OperationLogAspect.java
  12. 261 0
      alien-dining/src/main/java/shop/alien/dining/config/BaseRedisService.java
  13. 387 0
      alien-dining/src/main/java/shop/alien/dining/config/CartWebSocketProcess.java
  14. 34 0
      alien-dining/src/main/java/shop/alien/dining/config/CustomAccessDeniedHandler.java
  15. 59 0
      alien-dining/src/main/java/shop/alien/dining/config/DruidConfig.java
  16. 14 0
      alien-dining/src/main/java/shop/alien/dining/config/MyBatisPlusPageConfig.java
  17. 69 0
      alien-dining/src/main/java/shop/alien/dining/config/SecurityConfig.java
  18. 59 0
      alien-dining/src/main/java/shop/alien/dining/config/SwaggerConfig.java
  19. 19 0
      alien-dining/src/main/java/shop/alien/dining/config/WebConfig.java
  20. 103 0
      alien-dining/src/main/java/shop/alien/dining/config/WebSocketConfig.java
  21. 48 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningCollectController.java
  22. 292 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java
  23. 177 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java
  24. 60 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningFileUploadController.java
  25. 187 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningUserController.java
  26. 120 0
      alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java
  27. 95 0
      alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java
  28. 562 0
      alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java
  29. 34 0
      alien-dining/src/main/java/shop/alien/dining/dto/ChangePhoneDto.java
  30. 56 0
      alien-dining/src/main/java/shop/alien/dining/dto/UserProfileUpdateDto.java
  31. 20 0
      alien-dining/src/main/java/shop/alien/dining/dto/VerifyTokenDto.java
  32. 29 0
      alien-dining/src/main/java/shop/alien/dining/dto/WeChatLoginDto.java
  33. 48 0
      alien-dining/src/main/java/shop/alien/dining/enums/OrderStatus.java
  34. 160 0
      alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java
  35. 17 0
      alien-dining/src/main/java/shop/alien/dining/feign/DiningServiceFeign.java
  36. 67 0
      alien-dining/src/main/java/shop/alien/dining/filter/PreAuthFilter.java
  37. 26 0
      alien-dining/src/main/java/shop/alien/dining/listener/RedisKeyExpirationListener.java
  38. 121 0
      alien-dining/src/main/java/shop/alien/dining/service/CartService.java
  39. 22 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningCollectService.java
  40. 101 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningCouponService.java
  41. 150 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningService.java
  42. 58 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningUserService.java
  43. 61 0
      alien-dining/src/main/java/shop/alien/dining/service/OrderLockService.java
  44. 35 0
      alien-dining/src/main/java/shop/alien/dining/service/SseService.java
  45. 49 0
      alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java
  46. 200 0
      alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java
  47. 1016 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java
  48. 39 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCollectServiceImpl.java
  49. 471 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java
  50. 644 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java
  51. 690 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java
  52. 118 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/OrderLockServiceImpl.java
  53. 152 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/SseServiceImpl.java
  54. 137 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java
  55. 2007 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  56. 75 0
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java
  57. 64 0
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategyFactory.java
  58. 773 0
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java
  59. 227 0
      alien-dining/src/main/java/shop/alien/dining/util/TokenUtil.java
  60. 856 0
      alien-dining/src/main/java/shop/alien/dining/util/WXPayUtility.java
  61. 375 0
      alien-dining/src/main/java/shop/alien/dining/util/WeChatMiniProgramUtil.java
  62. 65 0
      alien-dining/src/main/java/shop/alien/dining/util/WeChatPayUtil.java
  63. 76 0
      alien-dining/src/main/java/shop/alien/dining/vo/DiningUserVo.java
  64. 40 0
      alien-dining/src/main/java/shop/alien/dining/vo/TokenVerifyVo.java
  65. 26 0
      alien-dining/src/main/resources/bootstrap-bug.yml
  66. 26 0
      alien-dining/src/main/resources/bootstrap-dev.yml
  67. 26 0
      alien-dining/src/main/resources/bootstrap-prod.yml
  68. 26 0
      alien-dining/src/main/resources/bootstrap-test.yml
  69. 26 0
      alien-dining/src/main/resources/bootstrap-uat.yml
  70. 4 0
      alien-dining/src/main/resources/bootstrap.yml
  71. 178 0
      alien-dining/src/main/resources/logback-spring.xml
  72. 4 0
      alien-entity/pom.xml
  73. 3 0
      alien-entity/src/main/java/shop/alien/entity/result/R.java
  74. 3 3
      alien-entity/src/main/java/shop/alien/entity/store/LifeDiscountCoupon.java
  75. 58 15
      alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java
  76. 99 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java
  77. 88 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCouponUsage.java
  78. 141 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrder.java
  79. 120 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderChangeLog.java
  80. 108 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderDetail.java
  81. 76 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderLock.java
  82. 17 1
      alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java
  83. 35 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddCartItemDTO.java
  84. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddDishDTO.java
  85. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartDTO.java
  86. 51 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java
  87. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/ChangeTableDTO.java
  88. 49 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CreateOrderDTO.java
  89. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoWithHomepageCuisinesDTO.java
  90. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/UpdateOrderCouponDTO.java
  91. 40 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java
  92. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineComboItemVO.java
  93. 79 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineDetailVO.java
  94. 49 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineListVO.java
  95. 31 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/DiningPageInfoVO.java
  96. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderChangeLogBatchVO.java
  97. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderChangeLogItemVO.java
  98. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderConfirmVO.java
  99. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderCuisineItemVO.java
  100. 77 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderDetailWithChangeLogVO.java

+ 1270 - 0
SSE前后端使用说明书.md

@@ -0,0 +1,1270 @@
+# 购物车实时通信使用说明书(SSE + WebSocket)
+
+## 📌 重要说明
+
+**SSE(Server-Sent Events)是单向通信**,只能从服务器推送到客户端,**不能从客户端发送消息到服务器**。
+
+如果您需要前端向服务器发送购物车变化,有两种方案:
+
+1. **方案一:SSE + HTTP API(推荐)**
+   - 前端通过HTTP API更新购物车
+   - 后端通过SSE推送更新给所有连接的客户端
+   - 简单、稳定、兼容性好
+
+2. **方案二:WebSocket(双向通信)**
+   - 前端可以直接通过WebSocket发送购物车变化
+   - 后端通过WebSocket推送更新给所有连接的客户端
+   - 支持双向实时通信
+
+**本系统同时支持两种方案,您可以根据需求选择使用。**
+
+---
+
+# 一、SSE(Server-Sent Events)使用说明
+
+## 一、SSE简介
+
+### 1.1 什么是SSE
+
+SSE(Server-Sent Events)是一种服务器推送技术,允许服务器主动向客户端推送数据。与WebSocket相比,SSE更简单、更轻量,特别适合单向数据推送场景。
+
+### 1.2 适用场景
+
+- ✅ 实时购物车更新(多人点餐场景)
+- ✅ 实时通知推送
+- ✅ 实时数据监控
+- ✅ 服务器主动推送消息
+
+### 1.3 技术特点
+
+- **单向通信**:服务器 → 客户端
+- **基于HTTP**:使用标准HTTP协议,无需额外协议
+- **自动重连**:浏览器自动处理连接断开和重连
+- **简单易用**:API简单,易于集成
+
+---
+
+## 二、后端实现说明
+
+### 2.1 接口地址
+
+```
+GET /api/store/order/sse/{tableId}
+Content-Type: text/event-stream
+```
+
+**参数说明:**
+- `tableId`:桌号ID(路径参数,必填)
+
+**响应类型:** `text/event-stream`
+
+### 2.2 后端实现代码
+
+#### 2.2.1 Controller层
+
+```java
+@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
+) {
+    log.info("建立SSE连接, tableId={}", tableId);
+    return sseService.createConnection(tableId);
+}
+```
+
+#### 2.2.2 Service层
+
+**接口定义:**
+```java
+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);
+}
+```
+
+#### 2.2.3 推送消息示例
+
+在购物车操作后推送更新:
+
+```java
+// 添加商品到购物车后
+CartDTO cart = cartService.addItem(dto);
+sseService.pushCartUpdate(dto.getTableId(), cart);
+
+// 更新购物车商品数量后
+CartDTO cart = cartService.updateQuantity(tableId, cuisineId, quantity);
+sseService.pushCartUpdate(tableId, cart);
+
+// 删除购物车商品后
+CartDTO cart = cartService.removeItem(tableId, cuisineId);
+sseService.pushCartUpdate(tableId, cart);
+
+// 清空购物车后
+cartService.clearCart(tableId);
+sseService.pushCartUpdate(tableId, new CartDTO());
+```
+
+### 2.3 消息事件类型
+
+后端会发送以下类型的事件:
+
+| 事件名称 | 说明 | 数据格式 |
+|---------|------|---------|
+| `connected` | 连接成功 | 字符串:"连接成功" |
+| `cart_update` | 购物车更新 | JSON格式的CartDTO对象 |
+| `heartbeat` | 心跳消息 | 字符串:"ping" |
+
+### 2.4 连接特性
+
+- **超时时间**:30分钟
+- **心跳间隔**:每30秒发送一次心跳
+- **多连接支持**:一个桌号可以同时有多个SSE连接(支持多人点餐)
+
+---
+
+## 三、前端实现说明
+
+### 3.1 基础使用(原生JavaScript)
+
+```javascript
+// 建立SSE连接
+const tableId = 123; // 桌号ID
+const eventSource = new EventSource(`/api/store/order/sse/${tableId}`);
+
+// 监听连接成功事件
+eventSource.addEventListener('connected', function(event) {
+    console.log('SSE连接成功:', event.data);
+    // event.data = "连接成功"
+});
+
+// 监听购物车更新事件
+eventSource.addEventListener('cart_update', function(event) {
+    console.log('购物车更新:', event.data);
+    // event.data 是JSON字符串,需要解析
+    const cart = JSON.parse(event.data);
+    updateCartUI(cart); // 更新购物车UI
+});
+
+// 监听心跳事件
+eventSource.addEventListener('heartbeat', function(event) {
+    console.log('收到心跳:', event.data);
+    // event.data = "ping"
+});
+
+// 监听错误事件
+eventSource.onerror = function(event) {
+    console.error('SSE连接错误:', event);
+    // 连接断开或出错时会触发
+    // 浏览器会自动尝试重连
+};
+
+// 关闭连接
+// eventSource.close();
+```
+
+### 3.2 Vue.js实现示例
+
+```vue
+<template>
+  <div>
+    <div v-if="!connected">正在连接...</div>
+    <div v-else>已连接</div>
+    <!-- 购物车UI -->
+    <div v-for="item in cart.items" :key="item.cuisineId">
+      {{ item.cuisineName }} x {{ item.quantity }}
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      eventSource: null,
+      connected: false,
+      cart: {
+        items: [],
+        totalAmount: 0,
+        totalQuantity: 0
+      }
+    }
+  },
+  mounted() {
+    this.initSSE();
+  },
+  beforeDestroy() {
+    this.closeSSE();
+  },
+  methods: {
+    // 初始化SSE连接
+    initSSE() {
+      const tableId = this.$route.params.tableId || 123;
+      const url = `/api/store/order/sse/${tableId}`;
+      
+      this.eventSource = new EventSource(url);
+      
+      // 连接成功
+      this.eventSource.addEventListener('connected', (event) => {
+        console.log('SSE连接成功');
+        this.connected = true;
+      });
+      
+      // 购物车更新
+      this.eventSource.addEventListener('cart_update', (event) => {
+        try {
+          const cart = JSON.parse(event.data);
+          this.cart = cart;
+          this.$message.success('购物车已更新');
+        } catch (error) {
+          console.error('解析购物车数据失败:', error);
+        }
+      });
+      
+      // 心跳
+      this.eventSource.addEventListener('heartbeat', (event) => {
+        console.log('收到心跳');
+      });
+      
+      // 错误处理
+      this.eventSource.onerror = (error) => {
+        console.error('SSE连接错误:', error);
+        this.connected = false;
+        // 可以在这里实现重连逻辑
+      };
+    },
+    
+    // 关闭SSE连接
+    closeSSE() {
+      if (this.eventSource) {
+        this.eventSource.close();
+        this.eventSource = null;
+        this.connected = false;
+      }
+    }
+  }
+}
+</script>
+```
+
+### 3.3 React实现示例
+
+```jsx
+import React, { useEffect, useState, useRef } from 'react';
+
+function CartComponent({ tableId }) {
+  const [connected, setConnected] = useState(false);
+  const [cart, setCart] = useState({ items: [], totalAmount: 0, totalQuantity: 0 });
+  const eventSourceRef = useRef(null);
+
+  useEffect(() => {
+    // 建立SSE连接
+    const url = `/api/store/order/sse/${tableId}`;
+    const eventSource = new EventSource(url);
+    eventSourceRef.current = eventSource;
+
+    // 连接成功
+    eventSource.addEventListener('connected', (event) => {
+      console.log('SSE连接成功');
+      setConnected(true);
+    });
+
+    // 购物车更新
+    eventSource.addEventListener('cart_update', (event) => {
+      try {
+        const cartData = JSON.parse(event.data);
+        setCart(cartData);
+        console.log('购物车已更新');
+      } catch (error) {
+        console.error('解析购物车数据失败:', error);
+      }
+    });
+
+    // 心跳
+    eventSource.addEventListener('heartbeat', (event) => {
+      console.log('收到心跳');
+    });
+
+    // 错误处理
+    eventSource.onerror = (error) => {
+      console.error('SSE连接错误:', error);
+      setConnected(false);
+    };
+
+    // 清理函数
+    return () => {
+      if (eventSourceRef.current) {
+        eventSourceRef.current.close();
+      }
+    };
+  }, [tableId]);
+
+  return (
+    <div>
+      <div>{connected ? '已连接' : '正在连接...'}</div>
+      <div>
+        {cart.items.map(item => (
+          <div key={item.cuisineId}>
+            {item.cuisineName} x {item.quantity}
+          </div>
+        ))}
+      </div>
+      <div>总金额: {cart.totalAmount}</div>
+    </div>
+  );
+}
+
+export default CartComponent;
+```
+
+### 3.4 微信小程序实现示例
+
+```javascript
+// 微信小程序不支持EventSource,需要使用wx.request或第三方库
+// 方案1:使用wx.request轮询(不推荐,但可用)
+// 方案2:使用WebSocket(推荐)
+// 方案3:使用第三方SSE库
+
+// 这里提供一个简单的轮询方案作为备选
+let pollingTimer = null;
+
+function startSSEPolling(tableId, onMessage) {
+  const poll = () => {
+    wx.request({
+      url: `https://your-api.com/api/store/order/cart/${tableId}`,
+      method: 'GET',
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          onMessage(res.data.data); // 购物车数据
+        }
+      },
+      complete: () => {
+        // 每2秒轮询一次
+        pollingTimer = setTimeout(poll, 2000);
+      }
+    });
+  };
+  
+  poll();
+}
+
+function stopSSEPolling() {
+  if (pollingTimer) {
+    clearTimeout(pollingTimer);
+    pollingTimer = null;
+  }
+}
+
+// 使用
+Page({
+  onLoad(options) {
+    const tableId = options.tableId;
+    startSSEPolling(tableId, (cart) => {
+      this.setData({ cart });
+    });
+  },
+  
+  onUnload() {
+    stopSSEPolling();
+  }
+});
+```
+
+---
+
+## 四、消息数据格式
+
+### 4.1 购物车更新消息(cart_update)
+
+**数据格式:** JSON字符串,解析后为CartDTO对象
+
+```json
+{
+  "tableId": 123,
+  "tableNumber": "A01",
+  "storeId": 1,
+  "items": [
+    {
+      "cuisineId": 1001,
+      "cuisineName": "宫保鸡丁",
+      "cuisineType": 1,
+      "cuisineImage": "https://example.com/image.jpg",
+      "unitPrice": 38.00,
+      "quantity": 2,
+      "subtotalAmount": 76.00,
+      "addUserId": 100,
+      "addUserPhone": "13800138000",
+      "remark": "不要花生"
+    }
+  ],
+  "totalAmount": 76.00,
+  "totalQuantity": 2
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 说明 |
+|-----|------|------|
+| tableId | Integer | 桌号ID |
+| tableNumber | String | 桌号 |
+| storeId | Integer | 门店ID |
+| items | Array | 购物车商品列表 |
+| totalAmount | BigDecimal | 总金额 |
+| totalQuantity | Integer | 商品总数量 |
+
+**CartItemDTO字段说明:**
+
+| 字段 | 类型 | 说明 |
+|-----|------|------|
+| cuisineId | Integer | 菜品ID |
+| cuisineName | String | 菜品名称 |
+| cuisineType | Integer | 菜品类型(1:单品, 2:套餐) |
+| cuisineImage | String | 菜品图片 |
+| unitPrice | BigDecimal | 单价 |
+| quantity | Integer | 数量 |
+| subtotalAmount | BigDecimal | 小计金额 |
+| addUserId | Integer | 添加该菜品的用户ID |
+| addUserPhone | String | 添加该菜品的用户手机号 |
+| remark | String | 备注 |
+
+### 4.2 连接成功消息(connected)
+
+```json
+"连接成功"
+```
+
+### 4.3 心跳消息(heartbeat)
+
+```json
+"ping"
+```
+
+---
+
+## 五、错误处理
+
+### 5.1 连接错误
+
+```javascript
+eventSource.onerror = function(event) {
+  console.error('SSE连接错误:', event);
+  
+  // 检查连接状态
+  if (eventSource.readyState === EventSource.CLOSED) {
+    console.log('连接已关闭');
+    // 可以在这里实现重连逻辑
+    reconnect();
+  } else if (eventSource.readyState === EventSource.CONNECTING) {
+    console.log('正在重连...');
+  }
+};
+```
+
+### 5.2 重连机制
+
+```javascript
+let reconnectAttempts = 0;
+const maxReconnectAttempts = 5;
+let reconnectTimer = null;
+
+function reconnect() {
+  if (reconnectAttempts >= maxReconnectAttempts) {
+    console.error('达到最大重连次数,停止重连');
+    return;
+  }
+  
+  reconnectAttempts++;
+  console.log(`尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})`);
+  
+  reconnectTimer = setTimeout(() => {
+    initSSE(); // 重新初始化SSE连接
+  }, 3000 * reconnectAttempts); // 指数退避
+}
+
+function initSSE() {
+  const eventSource = new EventSource(url);
+  
+  eventSource.addEventListener('connected', () => {
+    reconnectAttempts = 0; // 重置重连次数
+    clearTimeout(reconnectTimer);
+  });
+  
+  eventSource.onerror = () => {
+    reconnect();
+  };
+}
+```
+
+### 5.3 数据解析错误
+
+```javascript
+eventSource.addEventListener('cart_update', function(event) {
+  try {
+    const cart = JSON.parse(event.data);
+    updateCartUI(cart);
+  } catch (error) {
+    console.error('解析购物车数据失败:', error);
+    // 可以显示错误提示或使用默认值
+    showError('购物车数据解析失败,请刷新页面');
+  }
+});
+```
+
+---
+
+## 六、最佳实践
+
+### 6.1 连接管理
+
+1. **页面加载时建立连接**
+   ```javascript
+   mounted() {
+     this.initSSE();
+   }
+   ```
+
+2. **页面卸载时关闭连接**
+   ```javascript
+   beforeDestroy() {
+     this.closeSSE();
+   }
+   ```
+
+3. **路由切换时关闭旧连接**
+   ```javascript
+   watch: {
+     '$route'(to, from) {
+       if (to.params.tableId !== from.params.tableId) {
+         this.closeSSE();
+         this.initSSE();
+       }
+     }
+   }
+   ```
+
+### 6.2 性能优化
+
+1. **避免频繁更新UI**
+   ```javascript
+   let updateTimer = null;
+   eventSource.addEventListener('cart_update', function(event) {
+     // 防抖处理,避免频繁更新
+     clearTimeout(updateTimer);
+     updateTimer = setTimeout(() => {
+       const cart = JSON.parse(event.data);
+       updateCartUI(cart);
+     }, 100);
+   });
+   ```
+
+2. **使用虚拟滚动(如果商品很多)**
+   ```javascript
+   // 使用vue-virtual-scroll-list等库
+   ```
+
+### 6.3 用户体验
+
+1. **显示连接状态**
+   ```javascript
+   <div v-if="connected" class="status connected">已连接</div>
+   <div v-else class="status disconnected">连接中...</div>
+   ```
+
+2. **显示更新提示**
+   ```javascript
+   eventSource.addEventListener('cart_update', function(event) {
+     const cart = JSON.parse(event.data);
+     updateCartUI(cart);
+     showToast('购物车已更新'); // 显示提示
+   });
+   ```
+
+3. **处理网络异常**
+   ```javascript
+   eventSource.onerror = function(event) {
+     if (navigator.onLine === false) {
+       showError('网络连接已断开,请检查网络设置');
+     }
+   };
+   ```
+
+### 6.4 安全性
+
+1. **验证tableId权限**
+   - 后端需要验证用户是否有权限访问该桌号的SSE连接
+   - 可以在请求头中添加token进行验证
+
+2. **防止XSS攻击**
+   - 确保后端返回的数据是安全的
+   - 前端对接收到的数据进行验证和转义
+
+---
+
+## 七、常见问题
+
+### 7.1 连接立即断开
+
+**可能原因:**
+- 后端未正确设置`Content-Type: text/event-stream`
+- 后端连接超时设置过短
+- 网络代理或防火墙阻止了SSE连接
+
+**解决方案:**
+- 检查后端Controller的`produces`属性
+- 检查后端超时设置(当前为30分钟)
+- 检查网络配置
+
+### 7.2 收不到消息
+
+**可能原因:**
+- 事件名称不匹配
+- 数据格式错误
+- 连接已断开
+
+**解决方案:**
+- 检查事件名称是否正确(`cart_update`)
+- 检查数据是否为有效的JSON
+- 检查连接状态
+
+### 7.3 心跳消息过多
+
+**说明:**
+- 心跳消息每30秒发送一次,用于保持连接
+- 如果不需要处理心跳,可以忽略该事件
+
+**解决方案:**
+```javascript
+// 不监听heartbeat事件即可
+// eventSource.addEventListener('heartbeat', ...); // 注释掉
+```
+
+### 7.4 多个用户同时点餐
+
+**说明:**
+- 一个桌号可以同时有多个SSE连接
+- 每个用户的购物车操作都会推送给该桌号的所有连接
+
+**实现:**
+- 后端已支持多连接,无需额外配置
+- 前端每个用户建立自己的SSE连接即可
+
+---
+
+## 八、测试建议
+
+### 8.1 功能测试
+
+1. **连接测试**
+   - 测试正常连接
+   - 测试连接超时
+   - 测试连接断开重连
+
+2. **消息推送测试**
+   - 测试购物车添加商品
+   - 测试购物车更新数量
+   - 测试购物车删除商品
+   - 测试清空购物车
+
+3. **多用户测试**
+   - 测试多个用户同时连接
+   - 测试一个用户操作,其他用户是否收到推送
+
+### 8.2 性能测试
+
+1. **连接数测试**
+   - 测试单个桌号最多支持多少个连接
+   - 测试系统最多支持多少个连接
+
+2. **消息推送测试**
+   - 测试高频推送场景
+   - 测试大量数据推送
+
+### 8.3 兼容性测试
+
+1. **浏览器兼容性**
+   - Chrome/Edge(支持)
+   - Firefox(支持)
+   - Safari(支持)
+   - IE(不支持,需要使用polyfill)
+
+2. **移动端测试**
+   - iOS Safari
+   - Android Chrome
+   - 微信内置浏览器
+
+---
+
+## 九、API参考
+
+### 9.1 建立SSE连接
+
+```
+GET /api/store/order/sse/{tableId}
+```
+
+**请求参数:**
+- `tableId` (路径参数): 桌号ID
+
+**响应:**
+- Content-Type: `text/event-stream`
+- 事件流
+
+### 9.2 相关购物车API
+
+```
+POST /api/store/order/cart/add          # 添加商品到购物车
+PUT /api/store/order/cart/update         # 更新购物车商品数量
+DELETE /api/store/order/cart/remove      # 删除购物车商品
+DELETE /api/store/order/cart/clear      # 清空购物车
+GET /api/store/order/cart/{tableId}     # 获取购物车
+```
+
+---
+
+## 十、总结
+
+SSE是一种简单高效的服务器推送技术,特别适合购物车实时更新场景。通过本文档,您可以:
+
+1. ✅ 了解SSE的基本概念和适用场景
+2. ✅ 掌握后端SSE接口的实现方式
+3. ✅ 掌握前端SSE连接的使用方法
+4. ✅ 了解消息格式和事件类型
+5. ✅ 掌握错误处理和最佳实践
+
+如有问题,请参考代码实现或联系开发团队。
+
+---
+
+# 二、WebSocket(双向通信)使用说明
+
+## 1. WebSocket简介
+
+### 1.1 什么是WebSocket
+
+WebSocket是一种全双工通信协议,允许客户端和服务器之间进行双向实时通信。与SSE相比,WebSocket支持客户端主动向服务器发送消息。
+
+### 1.2 适用场景
+
+- ✅ 需要双向实时通信的场景
+- ✅ 前端需要主动向服务器发送消息
+- ✅ 实时购物车更新(前端可以直接发送变化)
+- ✅ 实时聊天、协作等场景
+
+### 1.3 技术特点
+
+- **双向通信**:客户端 ↔ 服务器
+- **实时性**:低延迟,实时推送
+- **持久连接**:建立连接后保持连接
+- **支持二进制和文本消息**
+
+---
+
+## 2. 后端实现说明
+
+### 2.1 WebSocket端点地址
+
+```
+ws://your-domain/ws/cart/{tableId}
+或
+wss://your-domain/ws/cart/{tableId}  (HTTPS环境)
+```
+
+**参数说明:**
+- `tableId`:桌号ID(路径参数,必填)
+
+### 2.2 后端实现代码
+
+后端已实现 `CartWebSocketProcess` 类,位于:
+```
+shop.alien.dining.config.CartWebSocketProcess
+```
+
+**主要功能:**
+- 建立WebSocket连接
+- 接收客户端消息(添加商品、更新数量、删除商品、清空购物车)
+- 推送购物车更新给所有连接的客户端
+- 自动处理连接断开和错误
+
+### 2.3 消息类型
+
+#### 2.3.1 客户端发送的消息类型
+
+| 消息类型 | 说明 | 数据格式 |
+|---------|------|---------|
+| `heartbeat` | 心跳消息 | `{"type": "heartbeat"}` |
+| `add_item` | 添加商品 | 见下方示例 |
+| `update_quantity` | 更新数量 | 见下方示例 |
+| `remove_item` | 删除商品 | 见下方示例 |
+| `clear_cart` | 清空购物车 | `{"type": "clear_cart"}` |
+
+#### 2.3.2 服务器推送的消息类型
+
+| 消息类型 | 说明 | 数据格式 |
+|---------|------|---------|
+| `connected` | 连接成功 | 见下方示例 |
+| `cart_update` | 购物车更新 | 见下方示例 |
+| `add_item_success` | 添加商品成功 | 见下方示例 |
+| `update_quantity_success` | 更新数量成功 | 见下方示例 |
+| `remove_item_success` | 删除商品成功 | 见下方示例 |
+| `clear_cart_success` | 清空购物车成功 | 见下方示例 |
+| `error` | 错误消息 | 见下方示例 |
+| `heartbeat` | 心跳回复 | `{"type": "heartbeat", "message": "pong"}` |
+
+---
+
+## 3. 前端实现说明
+
+### 3.1 原生JavaScript实现
+
+```javascript
+// 建立WebSocket连接
+const tableId = 123; // 桌号ID
+const ws = new WebSocket(`ws://your-domain/ws/cart/${tableId}`);
+
+// 连接成功
+ws.onopen = function(event) {
+    console.log('WebSocket连接成功');
+};
+
+// 接收消息
+ws.onmessage = function(event) {
+    const message = JSON.parse(event.data);
+    console.log('收到消息:', message);
+    
+    switch(message.type) {
+        case 'connected':
+            console.log('连接成功:', message.message);
+            break;
+        case 'cart_update':
+            // 更新购物车UI
+            updateCartUI(message.data);
+            break;
+        case 'add_item_success':
+            console.log('添加商品成功');
+            updateCartUI(message.data);
+            break;
+        case 'update_quantity_success':
+            console.log('更新数量成功');
+            updateCartUI(message.data);
+            break;
+        case 'remove_item_success':
+            console.log('删除商品成功');
+            updateCartUI(message.data);
+            break;
+        case 'clear_cart_success':
+            console.log('清空购物车成功');
+            updateCartUI(message.data);
+            break;
+        case 'error':
+            console.error('错误:', message.message);
+            showError(message.message);
+            break;
+        case 'heartbeat':
+            console.log('收到心跳');
+            break;
+    }
+};
+
+// 连接错误
+ws.onerror = function(error) {
+    console.error('WebSocket错误:', error);
+};
+
+// 连接关闭
+ws.onclose = function(event) {
+    console.log('WebSocket连接关闭');
+    // 可以实现重连逻辑
+    reconnect();
+};
+
+// 发送消息:添加商品
+function addItem(cuisineId, quantity, remark) {
+    const message = {
+        type: 'add_item',
+        data: {
+            cuisineId: cuisineId,
+            quantity: quantity,
+            remark: remark || null
+        }
+    };
+    ws.send(JSON.stringify(message));
+}
+
+// 发送消息:更新数量
+function updateQuantity(cuisineId, quantity) {
+    const message = {
+        type: 'update_quantity',
+        data: {
+            cuisineId: cuisineId,
+            quantity: quantity
+        }
+    };
+    ws.send(JSON.stringify(message));
+}
+
+// 发送消息:删除商品
+function removeItem(cuisineId) {
+    const message = {
+        type: 'remove_item',
+        data: {
+            cuisineId: cuisineId
+        }
+    };
+    ws.send(JSON.stringify(message));
+}
+
+// 发送消息:清空购物车
+function clearCart() {
+    const message = {
+        type: 'clear_cart'
+    };
+    ws.send(JSON.stringify(message));
+}
+
+// 发送心跳
+function sendHeartbeat() {
+    const message = {
+        type: 'heartbeat'
+    };
+    ws.send(JSON.stringify(message));
+}
+
+// 每30秒发送一次心跳
+setInterval(sendHeartbeat, 30000);
+
+// 关闭连接
+// ws.close();
+```
+
+### 3.2 Vue.js实现示例
+
+```vue
+<template>
+  <div>
+    <div v-if="!connected">正在连接...</div>
+    <div v-else>已连接</div>
+    
+    <!-- 购物车UI -->
+    <div v-for="item in cart.items" :key="item.cuisineId">
+      {{ item.cuisineName }} x {{ item.quantity }}
+      <button @click="updateQuantity(item.cuisineId, item.quantity + 1)">+</button>
+      <button @click="updateQuantity(item.cuisineId, item.quantity - 1)">-</button>
+      <button @click="removeItem(item.cuisineId)">删除</button>
+    </div>
+    
+    <button @click="clearCart">清空购物车</button>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      ws: null,
+      connected: false,
+      cart: {
+        items: [],
+        totalAmount: 0,
+        totalQuantity: 0
+      }
+    }
+  },
+  mounted() {
+    this.initWebSocket();
+  },
+  beforeDestroy() {
+    this.closeWebSocket();
+  },
+  methods: {
+    // 初始化WebSocket连接
+    initWebSocket() {
+      const tableId = this.$route.params.tableId || 123;
+      const wsUrl = `ws://your-domain/ws/cart/${tableId}`;
+      
+      this.ws = new WebSocket(wsUrl);
+      
+      // 连接成功
+      this.ws.onopen = () => {
+        console.log('WebSocket连接成功');
+        this.connected = true;
+      };
+      
+      // 接收消息
+      this.ws.onmessage = (event) => {
+        const message = JSON.parse(event.data);
+        this.handleMessage(message);
+      };
+      
+      // 连接错误
+      this.ws.onerror = (error) => {
+        console.error('WebSocket错误:', error);
+        this.connected = false;
+      };
+      
+      // 连接关闭
+      this.ws.onclose = () => {
+        console.log('WebSocket连接关闭');
+        this.connected = false;
+        // 可以在这里实现重连逻辑
+        this.reconnect();
+      };
+      
+      // 定时发送心跳
+      setInterval(() => {
+        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+          this.sendMessage({ type: 'heartbeat' });
+        }
+      }, 30000);
+    },
+    
+    // 处理消息
+    handleMessage(message) {
+      switch(message.type) {
+        case 'connected':
+          this.$message.success('连接成功');
+          break;
+        case 'cart_update':
+        case 'add_item_success':
+        case 'update_quantity_success':
+        case 'remove_item_success':
+        case 'clear_cart_success':
+          this.cart = message.data || this.cart;
+          break;
+        case 'error':
+          this.$message.error(message.message);
+          break;
+      }
+    },
+    
+    // 发送消息
+    sendMessage(message) {
+      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+        this.ws.send(JSON.stringify(message));
+      }
+    },
+    
+    // 添加商品
+    addItem(cuisineId, quantity, remark) {
+      this.sendMessage({
+        type: 'add_item',
+        data: {
+          cuisineId: cuisineId,
+          quantity: quantity,
+          remark: remark
+        }
+      });
+    },
+    
+    // 更新数量
+    updateQuantity(cuisineId, quantity) {
+      if (quantity <= 0) {
+        this.removeItem(cuisineId);
+        return;
+      }
+      this.sendMessage({
+        type: 'update_quantity',
+        data: {
+          cuisineId: cuisineId,
+          quantity: quantity
+        }
+      });
+    },
+    
+    // 删除商品
+    removeItem(cuisineId) {
+      this.sendMessage({
+        type: 'remove_item',
+        data: {
+          cuisineId: cuisineId
+        }
+      });
+    },
+    
+    // 清空购物车
+    clearCart() {
+      this.$confirm('确定要清空购物车吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.sendMessage({
+          type: 'clear_cart'
+        });
+      });
+    },
+    
+    // 关闭连接
+    closeWebSocket() {
+      if (this.ws) {
+        this.ws.close();
+        this.ws = null;
+      }
+    },
+    
+    // 重连
+    reconnect() {
+      setTimeout(() => {
+        this.initWebSocket();
+      }, 3000);
+    }
+  }
+}
+</script>
+```
+
+---
+
+## 4. 消息格式说明
+
+### 4.1 客户端发送消息格式
+
+#### 添加商品
+```json
+{
+  "type": "add_item",
+  "data": {
+    "cuisineId": 1001,
+    "quantity": 2,
+    "remark": "不要花生"
+  }
+}
+```
+
+#### 更新数量
+```json
+{
+  "type": "update_quantity",
+  "data": {
+    "cuisineId": 1001,
+    "quantity": 3
+  }
+}
+```
+
+#### 删除商品
+```json
+{
+  "type": "remove_item",
+  "data": {
+    "cuisineId": 1001
+  }
+}
+```
+
+#### 清空购物车
+```json
+{
+  "type": "clear_cart"
+}
+```
+
+### 4.2 服务器推送消息格式
+
+#### 连接成功
+```json
+{
+  "type": "connected",
+  "message": "连接成功",
+  "data": null,
+  "timestamp": 1704067200000
+}
+```
+
+#### 购物车更新
+```json
+{
+  "type": "cart_update",
+  "message": "购物车更新",
+  "data": {
+    "tableId": 123,
+    "tableNumber": "A01",
+    "storeId": 1,
+    "items": [
+      {
+        "cuisineId": 1001,
+        "cuisineName": "宫保鸡丁",
+        "cuisineType": 1,
+        "cuisineImage": "https://example.com/image.jpg",
+        "unitPrice": 38.00,
+        "quantity": 2,
+        "subtotalAmount": 76.00,
+        "addUserId": 100,
+        "addUserPhone": "13800138000",
+        "remark": "不要花生"
+      }
+    ],
+    "totalAmount": 76.00,
+    "totalQuantity": 2
+  },
+  "timestamp": 1704067200000
+}
+```
+
+---
+
+## 5. SSE vs WebSocket 选择建议
+
+### 5.1 使用SSE的场景
+
+- ✅ 只需要服务器向客户端推送数据
+- ✅ 前端通过HTTP API更新数据
+- ✅ 需要更好的浏览器兼容性
+- ✅ 需要更简单的实现
+
+### 5.2 使用WebSocket的场景
+
+- ✅ 需要双向实时通信
+- ✅ 前端需要主动向服务器发送消息
+- ✅ 需要更低的延迟
+- ✅ 需要支持二进制数据
+
+### 5.3 本系统支持
+
+**本系统同时支持SSE和WebSocket两种方式:**
+
+- **HTTP API + SSE**:前端通过HTTP API更新购物车,后端通过SSE推送更新
+- **WebSocket**:前端通过WebSocket发送购物车变化,后端通过WebSocket推送更新
+
+**两种方式可以同时使用,互不冲突。**
+
+---
+
+## 6. 注意事项
+
+1. **用户认证**:WebSocket连接建立时,用户信息从JWT Token中获取(如果使用HTTP API)
+2. **连接管理**:页面卸载时记得关闭WebSocket连接
+3. **心跳机制**:建议每30秒发送一次心跳,保持连接活跃
+4. **错误处理**:实现完善的错误处理和重连机制
+5. **多用户场景**:一个桌号可以同时有多个WebSocket连接,所有连接都会收到购物车更新
+
+---
+
+## 7. 总结
+
+WebSocket提供了双向实时通信能力,特别适合需要前端主动发送消息的场景。通过本文档,您可以:
+
+1. ✅ 了解WebSocket的基本概念和适用场景
+2. ✅ 掌握后端WebSocket接口的实现方式
+3. ✅ 掌握前端WebSocket连接的使用方法
+4. ✅ 了解消息格式和事件类型
+5. ✅ 掌握错误处理和重连机制
+
+如有问题,请参考代码实现或联系开发团队。

+ 28 - 0
alien-dining/README.md

@@ -0,0 +1,28 @@
+# alien-dining
+
+微信点餐程序模块
+
+## 模块说明
+
+本模块用于实现微信点餐相关功能,包括:
+- 菜单管理
+- 订单管理
+- 支付集成
+- 微信小程序对接
+
+## 技术栈
+
+- Spring Boot 2.3.2
+- Spring Cloud Hoxton.SR1
+- MyBatis Plus 3.2.0
+- Nacos 配置中心/注册中心
+- MySQL 8.0
+- Redis
+- 微信支付 SDK
+
+## 启动说明
+
+1. 确保已配置好 Nacos 服务
+2. 配置数据库连接信息
+3. 运行 `AlienDiningApplication` 主类
+

+ 15 - 0
alien-dining/doc/gateway-route-example.yml

@@ -0,0 +1,15 @@
+# 网关路由配置示例
+# 将此配置添加到 Nacos 的 alien-gateway.yml 配置文件中
+
+spring:
+  cloud:
+    gateway:
+      routes:
+        # 微信点餐模块路由配置
+        - id: aliendining
+          uri: http://${route_or_local_ip}:30014
+          predicates:
+            - Path=/aliendining/**
+          filters:
+            - StripPrefix=1
+

+ 84 - 0
alien-dining/doc/nacos-config-example.yml

@@ -0,0 +1,84 @@
+server:
+  port: 8080
+  servlet:
+    context-path: /dining
+
+spring:
+  application:
+    name: alien-dining
+  
+  # 数据源配置
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    druid:
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      url: jdbc:mysql://your-mysql-host:3306/alien_dining?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+      username: your-username
+      password: your-password
+      initial-size: 5
+      min-idle: 5
+      max-active: 20
+      max-wait: 60000
+      time-between-eviction-runs-millis: 60000
+      min-evictable-idle-time-millis: 300000
+      validation-query: SELECT 1
+      test-while-idle: true
+      test-on-borrow: false
+      test-on-return: false
+      pool-prepared-statements: true
+      max-pool-prepared-statement-per-connection-size: 20
+      filters: stat,wall,slf4j
+      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
+  
+  # Redis配置
+  redis:
+    host: your-redis-host
+    port: 6379
+    password: your-redis-password
+    database: 0
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 8
+        max-idle: 8
+        min-idle: 0
+        max-wait: -1
+  
+  # MyBatis Plus配置
+  mybatis-plus:
+    mapper-locations: classpath*:mapper/**/*.xml
+    type-aliases-package: shop.alien.entity
+    configuration:
+      map-underscore-to-camel-case: true
+      log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
+    global-config:
+      db-config:
+        id-type: auto
+        logic-delete-field: deleted
+        logic-delete-value: 1
+        logic-not-delete-value: 0
+
+# 日志配置
+logging:
+  level:
+    root: info
+    shop.alien.dining: debug
+    shop.alien.mapper: debug
+  path: C:/project/ext/log
+
+# 微信支付配置
+wechat:
+  pay:
+    app-id: your-wechat-app-id
+    mch-id: your-merchant-id
+    api-key: your-api-key
+    cert-path: your-cert-path
+    notify-url: https://your-domain.com/dining/pay/notify
+
+# 点餐业务配置
+dining:
+  # 订单超时时间(分钟)
+  order-timeout: 30
+  # 自动取消未支付订单时间(分钟)
+  auto-cancel-time: 15
+

+ 82 - 0
alien-dining/doc/nacos-config-ready.yml

@@ -0,0 +1,82 @@
+server:
+  port: 30014
+  servlet:
+    context-path: /dining
+
+spring:
+  application:
+    name: alien-dining
+  
+  # 数据源配置
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    druid:
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      url: jdbc:mysql://your-mysql-host:3306/alien_dining?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+      username: your-username
+      password: ENC(your-encrypted-password)
+      initial-size: 5
+      min-idle: 5
+      max-active: 20
+      max-wait: 60000
+      time-between-eviction-runs-millis: 60000
+      min-evictable-idle-time-millis: 300000
+      validation-query: SELECT 1
+      test-while-idle: true
+      test-on-borrow: false
+      test-on-return: false
+      pool-prepared-statements: true
+      max-pool-prepared-statement-per-connection-size: 20
+      filters: stat,wall,slf4j
+      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
+  
+  # Redis配置
+  redis:
+    host: your-redis-host
+    port: 6379
+    password: ENC(your-encrypted-redis-password)
+    database: 0
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 8
+        max-idle: 8
+        min-idle: 0
+        max-wait: -1
+  
+  # MyBatis Plus配置
+  mybatis-plus:
+    mapper-locations: classpath*:mapper/**/*.xml
+    type-aliases-package: shop.alien.entity
+    configuration:
+      map-underscore-to-camel-case: true
+      log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
+    global-config:
+      db-config:
+        id-type: auto
+        logic-delete-field: deleted
+        logic-delete-value: 1
+        logic-not-delete-value: 0
+
+# 日志配置
+logging:
+  level:
+    root: info
+    shop.alien.dining: debug
+    shop.alien.mapper: debug
+  path: C:/project/ext/log
+
+# 微信支付配置
+wechat:
+  pay:
+    app-id: your-wechat-app-id
+    mch-id: your-merchant-id
+    api-key: ENC(your-encrypted-api-key)
+    cert-path: your-cert-path
+    notify-url: https://your-domain.com/dining/pay/notify
+
+# 点餐业务配置
+dining:
+  order-timeout: 30
+  auto-cancel-time: 15
+

+ 152 - 0
alien-dining/doc/订单变更记录表使用说明.md

@@ -0,0 +1,152 @@
+# 订单变更记录表(store_order_change_log)使用说明
+
+## 表设计目的
+
+记录每次下单/加餐时商品种类和数量的变化,用于展示每次操作都加了什么商品。
+
+## 核心字段说明
+
+### 1. batch_no(批次号)
+- **作用**:同一时间点的操作使用同一批次号,用于分组展示
+- **格式建议**:`ORDER{orderId}_{timestamp}` 或 `ORDER{orderId}_{yyyyMMddHHmmss}`
+- **示例**:`ORDER123_20250202143025` 表示订单123在2025-02-02 14:30:25的操作批次
+
+### 2. operation_type(操作类型)
+- **1:首次下单** - 创建订单时的操作
+- **2:加餐** - 通过加餐接口添加商品
+- **3:更新订单** - 更新订单时重新下单
+
+### 3. quantity_change(数量变化)
+- **正数**:新增的数量
+- **负数**:减少的数量(如果支持减少)
+- **示例**:`+2` 表示新增2份,`-1` 表示减少1份
+
+### 4. quantity_before / quantity_after(变化前后数量)
+- **quantity_before**:变化前的数量
+- **quantity_after**:变化后的数量
+- **用途**:用于展示变化前后对比
+
+## 使用场景
+
+### 场景1:查询订单的所有变更记录(按时间排序)
+```sql
+SELECT 
+    batch_no,
+    operation_type,
+    operation_time,
+    cuisine_name,
+    quantity_change,
+    quantity_before,
+    quantity_after,
+    amount_change
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND delete_flag = 0
+ORDER BY operation_time ASC;
+```
+
+### 场景2:按批次分组展示每次操作
+```sql
+SELECT 
+    batch_no,
+    operation_type,
+    operation_time,
+    COUNT(*) as item_count,
+    SUM(quantity_change) as total_quantity_change,
+    SUM(amount_change) as total_amount_change,
+    GROUP_CONCAT(CONCAT(cuisine_name, ' x', quantity_change) SEPARATOR ', ') as items
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND delete_flag = 0
+GROUP BY batch_no, operation_type, operation_time
+ORDER BY operation_time ASC;
+```
+
+### 场景3:查询某次操作的所有商品
+```sql
+SELECT 
+    cuisine_name,
+    quantity_change,
+    unit_price,
+    amount_change
+FROM store_order_change_log 
+WHERE batch_no = 'ORDER123_20250202143025'
+  AND delete_flag = 0
+ORDER BY cuisine_id;
+```
+
+### 场景4:查询加餐记录
+```sql
+SELECT 
+    batch_no,
+    operation_time,
+    cuisine_name,
+    quantity_change,
+    amount_change
+FROM store_order_change_log 
+WHERE order_id = 123 
+  AND operation_type = 2  -- 加餐
+  AND delete_flag = 0
+ORDER BY operation_time DESC;
+```
+
+## 数据记录示例
+
+假设订单123的操作历史:
+
+### 首次下单(batch_no: ORDER123_20250202140000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 石板肉酱豆腐 | 1 | 0 | 1 | 19.90 |
+| 经典三杯鸡 | 1 | 0 | 1 | 26.90 |
+
+### 第一次加餐(batch_no: ORDER123_20250202143000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 经典三杯鸡 | 1 | 1 | 2 | 26.90 |
+| 宫保鸡丁 | 1 | 0 | 1 | 28.00 |
+
+### 第二次加餐(batch_no: ORDER123_20250202150000)
+| cuisine_name | quantity_change | quantity_before | quantity_after | amount_change |
+|-------------|----------------|-----------------|----------------|---------------|
+| 石板肉酱豆腐 | 1 | 1 | 2 | 19.90 |
+
+## 前端展示建议
+
+### 展示格式1:时间线展示
+```
+14:00 - 首次下单
+  • 石板肉酱豆腐 x1
+  • 经典三杯鸡 x1
+
+14:30 - 加餐
+  • 经典三杯鸡 x1(累计:2)
+  • 宫保鸡丁 x1
+
+15:00 - 加餐
+  • 石板肉酱豆腐 x1(累计:2)
+```
+
+### 展示格式2:批次卡片展示
+```
+批次1:首次下单(14:00)
+  石板肉酱豆腐 x1  ¥19.90
+  经典三杯鸡 x1    ¥26.90
+  小计:¥46.80
+
+批次2:加餐(14:30)
+  经典三杯鸡 x1    ¥26.90
+  宫保鸡丁 x1      ¥28.00
+  小计:¥54.90
+
+批次3:加餐(15:00)
+  石板肉酱豆腐 x1  ¥19.90
+  小计:¥19.90
+```
+
+## 注意事项
+
+1. **批次号生成**:同一时间点的所有商品变化应该使用同一个批次号
+2. **数量计算**:quantity_change 应该只记录本次变化,不是累计数量
+3. **逻辑删除**:使用 delete_flag 进行逻辑删除,保留历史记录
+4. **索引优化**:已创建必要的索引,查询性能有保障

+ 253 - 0
alien-dining/docs/扫码点餐商家功能实现情况检查报告.md

@@ -0,0 +1,253 @@
+# 扫码点餐(商家)功能实现情况检查报告
+
+## 检查时间
+2025-01-XX
+
+## 更新时间
+2025-01-XX(后端功能补充完成)
+
+## 总体情况
+✅ **核心功能已全部实现**,后端功能补充已完成。仅剩部分前端交互细节需要确认。
+
+---
+
+## 2.4.1 特性1:工作台 ✅ **已实现**
+
+### 实现情况
+- ✅ 前端页面已实现:`group_merchant/src/pages/scanOrder/index.vue`
+- ✅ 包含四个模块入口:
+  - 桌号管理
+  - 桌号状态
+  - 菜品分类
+  - 订单管理
+
+### 备注
+- 前端页面已完整实现,跳转逻辑正常
+
+---
+
+## 2.4.2 特性2:桌号管理 ✅ **已实现**
+
+### 实现情况
+
+#### 1. 新建功能 ✅
+- ✅ 后端接口:`StoreTableController.batchCreateTables`
+- ✅ 支持批量创建桌号(多个用逗号分隔)
+- ✅ 自动去重处理
+- ✅ 创建时自动生成二维码(通过 `qrcodeUrl` 字段)
+
+#### 2. 列表功能 ✅
+- ✅ 后端接口:`StoreTableController.getTablePage`(分页查询)
+- ✅ 排序:按桌号排序(后端实现)
+- ✅ 编辑功能:`StoreTableController.updateTable`
+- ✅ 删除功能:`StoreTableController.deleteTable`
+  - ✅ 逻辑删除
+  - ✅ 检查状态(空闲状态可删除)
+
+### 已实现的验证
+- ✅ **桌号长度限制**:后端已实现5字限制验证
+- ✅ **提示信息**:错误提示已统一为"此桌号已存在"
+- ⚠️ **二次确认**:"确认删除此桌号?" - 需要确认前端是否实现
+
+---
+
+## 2.4.3 特性3:桌号状态 ✅ **已实现**
+
+### 实现情况
+
+#### 1. 显示所有桌号 ✅
+- ✅ 前端页面:`group_merchant/src/pages/scanOrder/table/status.vue`
+- ✅ 后端接口:`StoreTableController.getTablePage`
+- ✅ 分页显示(每页12个,符合"12个一组"需求)
+- ⚠️ **布局**:前端需要确认是否"一行3个,显示4行"的布局
+
+#### 2. 点击桌号显示菜品信息 ✅
+- ✅ 前端已实现抽屉组件显示菜品信息
+- ✅ 显示内容:图片、名称、数量、标签、备注
+- ✅ 加菜标志:前端已实现 `hasNewItems` 判断
+- ✅ 排序:按加餐、点餐顺序排列
+- ✅ 超过5个可滑动
+
+#### 3. 桌号状态 ✅
+- ✅ 状态定义:`status` 字段(0:空闲, 1:就餐中)
+- ✅ 前端显示:"就餐中" / "空闲"
+
+#### 4. 用户加餐 ✅
+- ✅ 后端接口:`StoreOrderController.addDishToOrder`
+- ✅ 加餐标志:前端已实现显示
+
+#### 5. 换桌功能 ✅
+- ✅ 后端接口:`StoreTableController.changeTable`
+- ✅ 前端页面:`group_merchant/src/pages/scanOrder/table/change.vue`
+- ✅ 显示所有空闲桌号
+- ✅ 订单信息转移逻辑已实现
+
+### 需要确认的细节
+- ⚠️ **无菜品信息提示**:需要确认前端是否显示"无菜品信息"提示
+
+---
+
+## 2.4.4 特性4:菜品分类 ✅ **已实现**
+
+### 实现情况
+
+#### 1. 新建功能 ✅
+- ✅ 后端接口:`StoreCuisineCategoryController.batchCreateCategories`
+- ✅ 支持批量创建分类(多个用逗号分隔)
+- ✅ 自动去重处理
+
+#### 2. 列表功能 ✅
+- ✅ 后端接口:`StoreCuisineCategoryController.getCategoryList`
+- ✅ 排序功能:`StoreCuisineCategoryController.updateCategorySort`
+  - ✅ 支持拖拽排序(通过 `sort` 字段)
+  - ✅ 按创建时间倒序(多次创建)
+- ✅ 编辑功能:`StoreCuisineCategoryController.updateCategory`
+- ✅ 删除功能:`StoreCuisineCategoryController.deleteCategory`
+  - ✅ 检查分类下是否有菜品
+
+### 已实现的验证
+- ✅ **分类名称长度限制**:后端已实现5字限制验证
+- ✅ **提示信息**:错误提示已统一为"此分类名称已存在"
+- ⚠️ **删除提示**:"此分类下有菜品信息,不可删除" - 需要确认错误提示
+- ⚠️ **二次确认**:"确认删除此分类?" - 需要确认前端是否实现
+
+---
+
+## 2.4.5 特性5:创建菜品-价目表 ⚠️ **部分实现**
+
+### 实现情况
+
+#### 1. 新建功能 ✅
+- ✅ 后端接口:`StoreCuisineController.addCuisineCombo`
+- ✅ 审核流程:提交后状态为"审核中"(0),审核通过后状态为1
+
+#### 2. 新增字段检查
+
+| 字段 | 需求 | 实现情况 | 备注 |
+|------|------|----------|------|
+| 菜品分类 | 必选,可多选 | ✅ 已实现 | `categoryIds` 字段,支持多选 |
+| 首页展示 | 单选,默认是 | ✅ 已实现 | `homeDisplay` 字段(0:否, 1:是) |
+| 菜品标签 | 选填,限5字,最多3个 | ✅ 已实现 | `tags` 字段(JSON数组) |
+| 菜品短评 | 选填,限20字 | ✅ 已实现 | `dishReview` 字段 |
+| 菜品描述 | 选填,限300字 | ✅ 已实现 | `description` 字段 |
+| 餐位费 | 选填,限2位正整数 | ✅ 已实现 | `tablewareFee` 字段(在 `StoreInfo` 表中) |
+
+#### 3. 列表功能 ✅
+- ✅ 后端接口:`StoreCuisineController.getPriceList`
+- ✅ 餐位费:已实现保存和查询接口
+
+### 已实现的验证
+- ✅ **字段长度验证**:
+  - ✅ 菜品标签:限5字,最多3个 - 后端已实现验证
+  - ✅ 菜品短评:限20字 - 后端已实现验证
+  - ✅ 菜品描述:限300字 - 后端已实现验证
+  - ⚠️ 餐位费:限2位正整数 - 需要确认后端验证(餐位费在 `StoreInfo` 表中)
+- ⚠️ **首页展示默认值**:需求要求"默认是",需要确认前端默认值设置
+
+---
+
+## 2.4.6 特性6:订单管理 ✅ **已实现**
+
+### 实现情况
+
+#### 1. 搜索功能 ✅ **已实现**
+- ✅ **按订单编号搜索**:接口 `getOrderPage` 已支持按订单编号模糊搜索
+- ✅ **按菜品名称搜索**:接口已支持按菜品名称模糊搜索
+- ✅ **模糊搜索**:支持同时按订单编号和菜品名称搜索(OR逻辑)
+- ✅ **搜索长度限制**:搜索关键词自动限制为15字
+- ✅ **接口参数**:`StoreOrderController.getOrderPage` 已添加 `keyword` 参数
+
+#### 2. 列表显示 ✅
+- ✅ 后端接口:`StoreOrderController.getOrderPage`
+- ✅ 显示内容:
+  - ✅ 图片(菜品图片)
+  - ✅ 名称(菜品名称)
+  - ✅ 数量
+  - ✅ 标签(菜品标签)
+  - ✅ 价格
+  - ✅ 状态
+  - ✅ 备注
+  - ✅ 桌号
+  - ✅ 就餐人数
+  - ✅ 应付金额/实付金额(根据状态显示)
+  - ✅ 菜品超过3个可滑动(前端实现)
+
+#### 3. 详情显示 ✅
+- ✅ 后端接口:`StoreOrderController.getOrderDetail`
+- ✅ 显示内容:
+  - ✅ 订单编号
+  - ✅ 状态
+  - ✅ 桌号
+  - ✅ 就餐人数
+  - ✅ 下单时间
+  - ✅ 菜品总价
+  - ✅ 餐具费
+  - ✅ 优惠金额
+  - ✅ 实付金额
+  - ✅ 支付方式
+  - ✅ 手机号码
+  - ✅ 备注
+  - ✅ 菜品名称、价格、数量、标签
+
+### 需要补充的功能
+1. **订单搜索功能**:
+   - 在 `StoreOrderController.getOrderPage` 接口中添加搜索参数
+   - 支持按订单编号(`orderNo`)模糊搜索
+   - 支持按菜品名称(通过关联 `StoreOrderDetail.cuisineName`)模糊搜索
+   - 搜索关键词长度限制15字
+
+---
+
+## 总结
+
+### ✅ 已完全实现的功能
+1. 工作台(点餐管理模块入口)
+2. 桌号管理(新建、编辑、删除、列表)
+3. 桌号状态(显示、菜品信息、换桌)
+4. 菜品分类(新建、编辑、删除、排序)
+5. 创建菜品(所有字段已实现)
+6. 订单列表和详情显示
+
+### ⚠️ 需要补充/确认的功能
+1. **餐位费验证**(可选):
+   - 餐位费格式验证(2位正整数)- 餐位费在 `StoreInfo` 表中,可能需要单独验证
+
+2. **前端交互确认**:
+   - 删除操作的二次确认弹窗
+   - 无菜品信息时的提示
+   - 首页展示默认值设置
+
+### ✅ 已完成的后端功能
+1. **订单搜索功能** ✅:
+   - ✅ 按订单编号搜索(模糊搜索)
+   - ✅ 按菜品名称搜索(模糊搜索)
+   - ✅ 同时支持两种搜索方式(OR逻辑)
+   - ✅ 搜索关键词长度限制(15字)
+
+2. **字段验证和提示** ✅:
+   - ✅ 桌号长度限制(5字)及提示"此桌号已存在"
+   - ✅ 分类名称长度限制(5字)及提示"此分类名称已存在"
+   - ✅ 菜品标签验证(限5字,最多3个)
+   - ✅ 菜品短评验证(限20字)
+   - ✅ 菜品描述验证(限300字)
+
+### 建议优先级
+1. **已完成**:✅ 订单搜索功能、✅ 字段验证和错误提示
+2. **低优先级**:前端交互细节优化(二次确认、默认值等)
+
+---
+
+## 附录:相关文件路径
+
+### 后端文件
+- 桌号管理:`alien-store/src/main/java/shop/alien/store/controller/StoreTableController.java`
+- 菜品分类:`alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java`
+- 菜品管理:`alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java`
+- 订单管理:`alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java`
+
+### 前端文件
+- 工作台:`group_merchant/src/pages/scanOrder/index.vue`
+- 桌号管理:`group_merchant/src/pages/scanOrder/table/list.vue`
+- 桌号状态:`group_merchant/src/pages/scanOrder/table/status.vue`
+- 换桌页面:`group_merchant/src/pages/scanOrder/table/change.vue`
+- 订单管理:`group_merchant/src/pages/scanOrder/order/list.vue`

+ 354 - 0
alien-dining/pom.xml

@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>shop.alien</groupId>
+        <artifactId>alien-cloud</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <artifactId>alien-dining</artifactId>
+    <version>1.0.0</version>
+    <name>alien-dining</name>
+    <description>微信点餐程序</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!--redis连接池需要此依赖-->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- mybatis-plus代码生成器 Start -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>jsqlparser</artifactId>
+                    <groupId>com.github.jsqlparser</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>mybatis</artifactId>
+                    <groupId>org.mybatis</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>mybatis-spring</artifactId>
+                    <groupId>org.mybatis</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-annotation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-extension</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <!-- mybatisPlus Freemarker 模版引擎 -->
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+        </dependency>
+        <!-- mybatis-plus代码生成器 End -->
+
+        <!--Swagger Start-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>swagger-annotations</artifactId>
+                    <groupId>io.swagger</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>swagger-models</artifactId>
+                    <groupId>io.swagger</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+        </dependency>
+        <!--Swagger End-->
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <!--token-->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dysmsapi20170525</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.netease.yidun</groupId>
+            <artifactId>yidun-java-sdk</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+
+        <!-- openfeign -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+        </dependency>
+        <!-- openfeign -->
+
+        <dependency>
+            <groupId>shop.alien</groupId>
+            <artifactId>alien-entity</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>shop.alien</groupId>
+            <artifactId>alien-config</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>shop.alien</groupId>
+            <artifactId>alien-util</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <!-- 微信支付 -->
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-java</artifactId>
+            <version>0.2.17</version>
+        </dependency>
+
+        <!-- Google Gson -->
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.11.0</version>
+        </dependency>
+        <!-- OkHttp -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.12.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <!-- 指定项目编译时的java版本和编码方式 -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <target>1.8</target>
+                    <source>1.8</source>
+                    <encoding>UTF-8</encoding>
+                    <parameters>true</parameters>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <!-- 是否要把第三方jar加入到类构建路径 -->
+                            <addClasspath>true</addClasspath>
+                            <!-- 外部依赖jar包的最终位置 -->
+                            <classpathPrefix>lib/</classpathPrefix>
+                            <!--指定jar程序入口-->
+                            <mainClass>shop.alien.dining.AlienDiningApplication</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <!--拷贝依赖到jar外面的lib目录-->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <!-- 拷贝项目依赖包到lib/目录下 -->
+                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                            <overWriteIfNewer>true</overWriteIfNewer>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+

+ 24 - 0
alien-dining/src/main/java/shop/alien/dining/AlienDiningApplication.java

@@ -0,0 +1,24 @@
+package shop.alien.dining;
+
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@ComponentScan({"shop.alien.dining.*","shop.alien.util.*","shop.alien.config.http","shop.alien.config.properties"})
+@EnableSwaggerBootstrapUI
+@MapperScan({"shop.alien.mapper"})
+@SpringBootApplication
+@EnableFeignClients(basePackages = "shop.alien.dining.feign")
+@EnableScheduling
+public class   AlienDiningApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(AlienDiningApplication.class, args);
+    }
+
+}
+

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

@@ -0,0 +1,26 @@
+package shop.alien.dining.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 "";
+}
+

+ 28 - 0
alien-dining/src/main/java/shop/alien/dining/aspect/OperationLogAspect.java

@@ -0,0 +1,28 @@
+package shop.alien.dining.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.dining.annotation.OperationLog;
+
+/**
+ * 操作日志切面
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Aspect
+@Component
+public class OperationLogAspect {
+
+    @Before("@annotation(operationLog)")
+    public void before(JoinPoint joinPoint, OperationLog operationLog) {
+        // TODO: 实现操作日志记录逻辑
+        log.info("操作日志: {}", operationLog.value());
+    }
+}
+

+ 261 - 0
alien-dining/src/main/java/shop/alien/dining/config/BaseRedisService.java

@@ -0,0 +1,261 @@
+package shop.alien.dining.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.geo.*;
+import org.springframework.data.redis.connection.RedisGeoCommands;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis基础服务类
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2022/3/14 15:14
+ */
+@Component
+@RequiredArgsConstructor
+public class BaseRedisService {
+
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 添加List值, 向右
+     *
+     * @param key   键
+     * @param value 值
+     */
+    public void setListRight(String key, String value) {
+        stringRedisTemplate.opsForList().rightPush(key, value);
+    }
+
+    /**
+     * 取出并删除列表中的所有元素(原子操作)
+     */
+    public List<String> popBatchFromList(String key) {
+        String luaScript =
+                "local elements = redis.call('LRANGE', KEYS[1], 0, -1) " +
+                        "redis.call('DEL', KEYS[1]) " +
+                        "return elements ";
+
+        RedisScript<List> script = RedisScript.of(luaScript, List.class);
+        return stringRedisTemplate.execute(script, Collections.singletonList(key));
+    }
+
+    /**
+     * 判断是否存在指定key
+     * @param key
+     * @return
+     */
+    public boolean hasKey(String key) {
+        Boolean exists = stringRedisTemplate.hasKey(key);
+        return exists != null && exists;
+    }
+
+    /**
+     * 添加List, 所有
+     *
+     * @param key   键
+     * @param value 值
+     */
+    public void setList(String key, List<String> value) {
+        stringRedisTemplate.opsForList().rightPushAll(key, value);
+    }
+
+    /**
+     * 从List左边取值并删除
+     *
+     * @param key 键
+     * @return 删除的值
+     */
+    public String getListLeft(String key) {
+        return stringRedisTemplate.opsForList().leftPop(key);
+    }
+
+    /**
+     * 获取List所有
+     *
+     * @param key 键
+     * @return List<String>
+     */
+    public List<String> getList(String key) {
+        return stringRedisTemplate.opsForList().range(key, 0, -1);
+    }
+
+    /**
+     * 添加Set
+     *
+     * @param key   键
+     * @param value 值
+     */
+    public void setSetList(String key, String value) {
+        stringRedisTemplate.opsForSet().add(key, value);
+    }
+
+    /**
+     * 获取Set
+     *
+     * @param key 键
+     * @return Set<String>
+     */
+    public Set<String> getSetList(String key) {
+        return stringRedisTemplate.opsForSet().members(key);
+    }
+
+    /**
+     * 添加String值, 不设置过期时间
+     *
+     * @param key   键
+     * @param value 值
+     */
+    public void setString(String key, String value) {
+        set(key, value, null);
+    }
+
+    /**
+     * 添加String值, 并设置过期时间
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeOut 时长(秒)
+     */
+    public void setString(String key, String value, Long timeOut) {
+        set(key, value, timeOut);
+    }
+
+    /**
+     * 设置超时时间
+     *
+     * @param key     键
+     * @param timeOut 时长(秒)
+     */
+    public void setTimeOut(String key, Long timeOut) {
+        stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 传入object对象
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeOut 时长(秒)
+     */
+    private void set(String key, Object value, Long timeOut) {
+        if (value != null) {
+            if (value instanceof String) {
+                String setValue = (String) value;
+                stringRedisTemplate.opsForValue().set(key, setValue);
+            }
+            //设置有效期
+            if (timeOut != null) {
+                stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    /**
+     * 使用key查找redis信息
+     *
+     * @param key 键
+     * @return
+     */
+    public String getString(String key) {
+        return stringRedisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * 使用key删除redis信息
+     *
+     * @param key 键
+     */
+    public void delete(String key) {
+        stringRedisTemplate.delete(key);
+    }
+
+    /**
+     * 获取分布式锁
+     * @param lockKey 锁的标识
+     * @return 锁的value值,用于释放锁时验证,null表示获取失败
+     */
+    public String lock(String lockKey) {
+        return lock(lockKey, 30000, 10000);
+    }
+
+    /**
+     * 获取分布式锁
+     * @param lockKey 锁的标识
+     * @param expireTime 锁的过期时间(毫秒)
+     * @param acquireTimeout 获取锁的超时时间(毫秒)
+     * @return 锁的value值,用于释放锁时验证,null表示获取失败
+     */
+    public String lock(String lockKey, long expireTime, long acquireTimeout) {
+        // 生成唯一标识,用于释放锁时验证
+        String identifier = UUID.randomUUID().toString();
+        // 完整的锁键
+        String lockKeyWithPrefix = "dining:lock:" + lockKey;
+        // 超时时间戳
+        long endTime = System.currentTimeMillis() + acquireTimeout;
+
+        while (System.currentTimeMillis() < endTime) {
+            // 尝试获取锁:SET NX PX
+            Boolean success = stringRedisTemplate.opsForValue()
+                    .setIfAbsent(lockKeyWithPrefix, identifier, expireTime, TimeUnit.MILLISECONDS);
+
+            if (Boolean.TRUE.equals(success)) {
+                // 获取锁成功
+                return identifier;
+            }
+
+            try {
+                // 短暂休眠,避免频繁尝试
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return null;
+            }
+        }
+
+        // 获取锁超时
+        return null;
+    }
+
+    /**
+     * 释放分布式锁
+     * @param lockKey 锁的标识
+     * @param identifier 锁的value值,用于验证
+     * @return 是否释放成功
+     */
+    public boolean unlock(String lockKey, String identifier) {
+        if (identifier == null) {
+            return false;
+        }
+
+        String lockKeyWithPrefix = "dining:lock:" + lockKey;
+
+        // 使用Lua脚本确保释放锁的原子性
+        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                "return redis.call('del', KEYS[1]) " +
+                "else " +
+                "return 0 " +
+                "end";
+
+        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
+        Long result = stringRedisTemplate.execute(script,
+                Collections.singletonList(lockKeyWithPrefix),
+                identifier);
+
+        return result != null && result > 0;
+    }
+
+}
+

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

@@ -0,0 +1,387 @@
+package shop.alien.dining.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+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 javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 购物车WebSocket处理类
+ * 支持双向通信:前端可以发送购物车变化,后端可以推送购物车更新
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Component
+@ServerEndpoint(value = "/ws/cart/{tableId}", configurator = WebSocketConfig.WebSocketConfigurator.class)
+public class CartWebSocketProcess implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+    private static CartService cartService;
+
+    // 存储每个桌号的WebSocket连接,一个桌号可以有多个连接(多个用户)
+    private static final ConcurrentHashMap<Integer, ConcurrentHashMap<String, CartWebSocketProcess>> connections = new ConcurrentHashMap<>();
+
+    /**
+     * 会话对象
+     */
+    private Session session;
+
+    /**
+     * 桌号ID
+     */
+    private Integer tableId;
+
+    /**
+     * 连接ID
+     */
+    private String connectionId;
+
+    @Override
+    public void setApplicationContext(ApplicationContext context) {
+        CartWebSocketProcess.applicationContext = context;
+        CartWebSocketProcess.cartService = context.getBean(CartService.class);
+    }
+
+    /**
+     * 客户端创建连接时触发
+     */
+    @OnOpen
+    public void onOpen(@PathParam("tableId") Integer tableId, Session session) {
+        try {
+            this.session = session;
+            this.tableId = tableId;
+            // 设置30分钟超时
+            this.session.setMaxIdleTimeout(30 * 60 * 1000);
+            
+            // 生成连接ID
+            this.connectionId = "conn_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId();
+
+            // 存储连接
+            connections.computeIfAbsent(tableId, k -> new ConcurrentHashMap<>()).put(connectionId, this);
+
+            log.info("购物车WebSocket连接建立, tableId={}, connectionId={}", tableId, connectionId);
+
+            // 发送连接成功消息
+            sendMessage(createMessage("connected", "连接成功", null));
+
+            // 发送当前购物车数据
+            try {
+                CartDTO cart = cartService.getCart(tableId);
+                sendMessage(createMessage("cart_update", "购物车数据", cart));
+            } catch (Exception e) {
+                log.error("获取购物车数据失败, tableId={}", tableId, e);
+            }
+
+        } catch (Exception e) {
+            log.error("WebSocket连接建立失败, tableId={}", tableId, e);
+        }
+    }
+
+    /**
+     * 接收到客户端消息时触发
+     */
+    @OnMessage
+    public void onMessage(String message) {
+        try {
+            log.info("收到客户端消息, tableId={}, connectionId={}, message={}", tableId, connectionId, message);
+
+            // 解析消息
+            JSONObject jsonMessage = JSON.parseObject(message);
+            String type = jsonMessage.getString("type");
+
+            if (type == null) {
+                sendError("消息类型不能为空");
+                return;
+            }
+
+            // 处理不同类型的消息
+            switch (type) {
+                case "heartbeat":
+                    // 心跳消息,直接回复
+                    sendMessage(createMessage("heartbeat", "pong", null));
+                    break;
+
+                case "add_item":
+                    // 添加商品到购物车
+                    handleAddItem(jsonMessage);
+                    break;
+
+                case "update_quantity":
+                    // 更新商品数量
+                    handleUpdateQuantity(jsonMessage);
+                    break;
+
+                case "remove_item":
+                    // 删除商品
+                    handleRemoveItem(jsonMessage);
+                    break;
+
+                case "clear_cart":
+                    // 清空购物车
+                    handleClearCart();
+                    break;
+
+                default:
+                    sendError("未知的消息类型: " + type);
+                    break;
+            }
+
+        } catch (Exception e) {
+            log.error("处理客户端消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            sendError("处理消息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 客户端连接关闭时触发
+     */
+    @OnClose
+    public void onClose() {
+        try {
+            removeConnection();
+            log.info("购物车WebSocket连接关闭, tableId={}, connectionId={}", tableId, connectionId);
+        } catch (Exception e) {
+            log.error("关闭WebSocket连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+        }
+    }
+
+    /**
+     * 连接发生异常时触发
+     */
+    @OnError
+    public void onError(Throwable error) {
+        log.error("WebSocket连接错误, tableId={}, connectionId={}", tableId, connectionId, error);
+        removeConnection();
+    }
+
+    /**
+     * 处理添加商品
+     */
+    private void handleAddItem(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("添加商品数据不能为空");
+                return;
+            }
+
+            AddCartItemDTO dto = new AddCartItemDTO();
+            dto.setTableId(tableId);
+            dto.setCuisineId(data.getInteger("cuisineId"));
+            dto.setQuantity(data.getInteger("quantity"));
+            dto.setRemark(data.getString("remark"));
+
+            // 调用服务添加商品
+            CartDTO cart = cartService.addItem(dto);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("add_item_success", "添加商品成功", cart));
+
+        } catch (Exception e) {
+            log.error("添加商品失败, tableId={}", tableId, e);
+            sendError("添加商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理更新商品数量
+     */
+    private void handleUpdateQuantity(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("更新数量数据不能为空");
+                return;
+            }
+
+            Integer cuisineId = data.getInteger("cuisineId");
+            Integer quantity = data.getInteger("quantity");
+
+            if (cuisineId == null || quantity == null) {
+                sendError("菜品ID和数量不能为空");
+                return;
+            }
+
+            // 调用服务更新数量
+            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("update_quantity_success", "更新数量成功", cart));
+
+        } catch (Exception e) {
+            log.error("更新商品数量失败, tableId={}", tableId, e);
+            sendError("更新数量失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理删除商品
+     */
+    private void handleRemoveItem(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("删除商品数据不能为空");
+                return;
+            }
+
+            Integer cuisineId = data.getInteger("cuisineId");
+            if (cuisineId == null) {
+                sendError("菜品ID不能为空");
+                return;
+            }
+
+            // 调用服务删除商品
+            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("remove_item_success", "删除商品成功", cart));
+
+        } catch (Exception e) {
+            log.error("删除商品失败, tableId={}", tableId, e);
+            sendError("删除商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理清空购物车
+     */
+    private void handleClearCart() {
+        try {
+            // 调用服务清空购物车
+            cartService.clearCart(tableId);
+
+            // 创建空的购物车对象
+            CartDTO cart = new CartDTO();
+            cart.setTableId(tableId);
+            cart.setItems(java.util.Collections.emptyList());
+            cart.setTotalAmount(java.math.BigDecimal.ZERO);
+            cart.setTotalQuantity(0);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("clear_cart_success", "清空购物车成功", cart));
+
+        } catch (Exception e) {
+            log.error("清空购物车失败, tableId={}", tableId, e);
+            sendError("清空购物车失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 推送购物车更新给该桌号的所有连接
+     */
+    private void pushCartUpdateToAll(Integer tableId, CartDTO cart) {
+        ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+        if (tableConnections != null && !tableConnections.isEmpty()) {
+            tableConnections.forEach((connId, ws) -> {
+                try {
+                    ws.sendMessage(createMessage("cart_update", "购物车更新", cart));
+                } catch (Exception e) {
+                    log.error("推送购物车更新失败, tableId={}, connectionId={}", tableId, connId, e);
+                }
+            });
+        }
+    }
+
+    /**
+     * 发送消息
+     */
+    public void sendMessage(String message) {
+        try {
+            if (session != null && session.isOpen()) {
+                session.getBasicRemote().sendText(message);
+            }
+        } catch (IOException e) {
+            log.error("发送WebSocket消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            removeConnection();
+        }
+    }
+
+    /**
+     * 发送错误消息
+     */
+    private void sendError(String errorMessage) {
+        sendMessage(createMessage("error", errorMessage, null));
+    }
+
+    /**
+     * 创建消息JSON字符串
+     */
+    private String createMessage(String type, String message, Object data) {
+        JSONObject json = new JSONObject();
+        json.put("type", type);
+        json.put("message", message);
+        json.put("data", data);
+        json.put("timestamp", System.currentTimeMillis());
+        return json.toJSONString();
+    }
+
+    /**
+     * 移除连接
+     */
+    private void removeConnection() {
+        if (tableId != null && connectionId != null) {
+            ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+            if (tableConnections != null) {
+                tableConnections.remove(connectionId);
+                if (tableConnections.isEmpty()) {
+                    connections.remove(tableId);
+                }
+            }
+        }
+    }
+
+    /**
+     * 静态方法:推送购物车更新(供外部调用,如HTTP接口)
+     */
+    public static void pushCartUpdate(Integer tableId, CartDTO cart) {
+        ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+        if (tableConnections != null && !tableConnections.isEmpty()) {
+            String message = createStaticMessage("cart_update", "购物车更新", cart);
+            tableConnections.forEach((connectionId, ws) -> {
+                try {
+                    ws.sendMessage(message);
+                } catch (Exception e) {
+                    log.error("推送购物车更新失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            });
+        }
+    }
+
+    /**
+     * 静态方法:创建消息
+     */
+    private static String createStaticMessage(String type, String message, Object data) {
+        JSONObject json = new JSONObject();
+        json.put("type", type);
+        json.put("message", message);
+        json.put("data", data);
+        json.put("timestamp", System.currentTimeMillis());
+        return json.toJSONString();
+    }
+}

+ 34 - 0
alien-dining/src/main/java/shop/alien/dining/config/CustomAccessDeniedHandler.java

@@ -0,0 +1,34 @@
+package shop.alien.dining.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class CustomAccessDeniedHandler implements AccessDeniedHandler {
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
+            throws IOException {
+        response.setStatus(HttpStatus.FORBIDDEN.value());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding("UTF-8");
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", 403);
+        result.put("msg", "权限不足,无法访问该接口");
+        result.put("detail", e.getMessage());
+
+        new ObjectMapper().writeValue(response.getWriter(), result);
+    }
+}
+

+ 59 - 0
alien-dining/src/main/java/shop/alien/dining/config/DruidConfig.java

@@ -0,0 +1,59 @@
+package shop.alien.dining.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.support.http.StatViewServlet;
+import com.alibaba.druid.support.http.WebStatFilter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.sql.DataSource;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Druid配置
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4 9:13
+ */
+@Configuration
+public class DruidConfig {
+
+    @ConfigurationProperties(prefix = "spring.datasource.druid")
+    @Bean
+    public DataSource druid() {
+        return new DruidDataSource();
+    }
+
+    //配置Druid的监控
+    //1、配置一个管理后台的Servlet
+    @Bean
+    public ServletRegistrationBean statViewServlet() {
+        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
+        Map<String, String> initParams = new HashMap<>();
+        initParams.put("loginUsername", "admin");
+        initParams.put("loginPassword", "alien1234");
+        initParams.put("allow", "");//默认就是允许所有访问
+        //initParams.put("deny","127.0.0.1");
+        bean.setInitParameters(initParams);
+        return bean;
+    }
+
+    //2、配置一个web监控的filter
+    @Bean
+    public FilterRegistrationBean webStatFilter() {
+        FilterRegistrationBean bean = new FilterRegistrationBean();
+        bean.setFilter(new WebStatFilter());
+        Map<String, String> initParams = new HashMap<>();
+        initParams.put("exclusions", "*.js,*.css,/druid/*");
+        bean.setInitParameters(initParams);
+        bean.setUrlPatterns(Arrays.asList("/*"));
+        return bean;
+    }
+}
+

+ 14 - 0
alien-dining/src/main/java/shop/alien/dining/config/MyBatisPlusPageConfig.java

@@ -0,0 +1,14 @@
+package shop.alien.dining.config;
+
+import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusPageConfig {
+    @Bean
+    public PaginationInterceptor paginationInterceptor() {
+        return new PaginationInterceptor();
+    }
+}
+

+ 69 - 0
alien-dining/src/main/java/shop/alien/dining/config/SecurityConfig.java

@@ -0,0 +1,69 @@
+package shop.alien.dining.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import shop.alien.dining.filter.PreAuthFilter;
+
+import javax.annotation.Resource;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+    @Resource
+    private CustomAccessDeniedHandler customAccessDeniedHandler;
+
+    @Resource
+    private PreAuthFilter preAuthFilter;
+
+    /**
+     * 核心:跳过前端静态资源的Security过滤(不走过滤器链,性能最优)
+     * 适用于前端打包后的静态资源(JS/CSS/图片/字体等)
+     */
+    @Override
+    public void configure(WebSecurity web) throws Exception {
+        web.ignoring()
+                // 前端静态资源路径(根据实际项目调整)
+                .antMatchers("/static/**", "/css/**", "/js/**", "/images/**", "/fonts/**")
+                // 若前端部署在后端,放行打包后的dist目录(Vue/React打包后)
+                .antMatchers("/dist/**", "/public/**")
+                // 放行Swagger(若有)
+                .antMatchers("/swagger-ui/**", "/v3/api-docs/**","/doc.html","/webjars/**");
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http
+                // 1. 禁用CSRF(前后端分离无Cookie,无需保护)
+                .csrf().disable()
+                // 2. 禁用Session(无状态,仅授权)
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                .and()
+                // 3. 关闭所有默认认证入口(表单登录、HTTP Basic)
+                .formLogin().disable()
+                .httpBasic().disable()
+                // 4. 禁用注销
+                .logout().disable()
+                // 5. 配置授权规则
+                .authorizeRequests()
+                // 其他接口:需任意认证(即请求头带X-User-Roles)
+                .anyRequest().permitAll()
+                .and()
+                // 6. 添加自定义预认证过滤器(核心:解析权限)
+                .addFilterBefore(preAuthFilter, UsernamePasswordAuthenticationFilter.class)
+                // 7. 配置授权异常处理器
+                .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
+
+        // 允许跨域
+        http.cors();
+    }
+
+}
+

+ 59 - 0
alien-dining/src/main/java/shop/alien/dining/config/SwaggerConfig.java

@@ -0,0 +1,59 @@
+package shop.alien.dining.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * swagger配置类
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4 9:17
+ */
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+
+    /**
+     * controller扫描
+     *
+     * @return controller扫描
+     */
+    @Bean
+    public Docket docket() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .apiInfo(apiInfo())
+                .groupName("微信点餐Api服务")
+                .select()
+                // 只扫描点餐模块的 Controller 包,避免展示系统默认的错误控制器等无关接口
+                .apis(RequestHandlerSelectors.basePackage("shop.alien.dining.controller"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    /**
+     * Api标题信息
+     *
+     * @return api标题Api标题信息
+     */
+    public ApiInfo apiInfo() {
+        Contact contact = new Contact("爱丽恩", "https://www.alien.shop", "");
+        return new ApiInfoBuilder()
+                .title("微信点餐Api服务")
+                .description("微信点餐Api服务")
+                .license("爱丽恩")
+                .contact(contact)
+                .termsOfServiceUrl("https://www.alien.shop")
+                .version("1.0.0")
+                .build();
+    }
+}
+

+ 19 - 0
alien-dining/src/main/java/shop/alien/dining/config/WebConfig.java

@@ -0,0 +1,19 @@
+package shop.alien.dining.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
+        // 如果需要TokenInfoArgumentResolver,可以从alien-util中引入
+        // resolvers.add(new TokenInfoArgumentResolver());
+    }
+}
+

+ 103 - 0
alien-dining/src/main/java/shop/alien/dining/config/WebSocketConfig.java

@@ -0,0 +1,103 @@
+package shop.alien.dining.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * WebSocketConfig
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/2/29 14:40
+ */
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig {
+
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+    
+    /**
+     * WebSocket配置器,用于在握手时获取IP地址和User-Agent
+     */
+    public static class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
+        @Override
+        public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+            Map<String, Object> userProperties = sec.getUserProperties();
+            Map<String, List<String>> headers = request.getHeaders();
+            
+            // 获取User-Agent
+            List<String> userAgentList = headers.get("User-Agent");
+            String userAgent = (userAgentList != null && !userAgentList.isEmpty()) ? userAgentList.get(0) : null;
+            userProperties.put("userAgent", userAgent);
+            
+            // 获取IP地址(从请求头中获取)
+            String ipAddress = getIpAddress(headers);
+            userProperties.put("ipAddress", ipAddress);
+        }
+        
+        /**
+         * 从请求头中获取客户端IP地址
+         */
+        private String getIpAddress(Map<String, List<String>> headers) {
+            String ip = null;
+            
+            // 尝试从各种请求头中获取IP
+            if (headers.containsKey("X-Forwarded-For")) {
+                List<String> xffList = headers.get("X-Forwarded-For");
+                if (xffList != null && !xffList.isEmpty()) {
+                    ip = xffList.get(0);
+                }
+            }
+            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+                if (headers.containsKey("Proxy-Client-IP")) {
+                    List<String> proxyList = headers.get("Proxy-Client-IP");
+                    if (proxyList != null && !proxyList.isEmpty()) {
+                        ip = proxyList.get(0);
+                    }
+                }
+            }
+            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+                if (headers.containsKey("WL-Proxy-Client-IP")) {
+                    List<String> wlProxyList = headers.get("WL-Proxy-Client-IP");
+                    if (wlProxyList != null && !wlProxyList.isEmpty()) {
+                        ip = wlProxyList.get(0);
+                    }
+                }
+            }
+            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+                if (headers.containsKey("HTTP_CLIENT_IP")) {
+                    List<String> httpClientList = headers.get("HTTP_CLIENT_IP");
+                    if (httpClientList != null && !httpClientList.isEmpty()) {
+                        ip = httpClientList.get(0);
+                    }
+                }
+            }
+            if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+                if (headers.containsKey("HTTP_X_FORWARDED_FOR")) {
+                    List<String> httpXffList = headers.get("HTTP_X_FORWARDED_FOR");
+                    if (httpXffList != null && !httpXffList.isEmpty()) {
+                        ip = httpXffList.get(0);
+                    }
+                }
+            }
+            
+            // 如果是多级代理,取第一个IP
+            if (ip != null && ip.contains(",")) {
+                ip = ip.split(",")[0].trim();
+            }
+            
+            return ip;
+        }
+    }
+}

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

@@ -0,0 +1,48 @@
+package shop.alien.dining.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.dining.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());
+        }
+    }
+}

+ 292 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -0,0 +1,292 @@
+package shop.alien.dining.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.dining.service.DiningService;
+import shop.alien.dining.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=就餐中 且 diner_count 有值 判定为就餐中")
+    @GetMapping("/table-dining-status")
+    public R<TableDiningStatusVO> getTableDiningStatus(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer 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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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());
+        }
+    }
+}

+ 177 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java

@@ -0,0 +1,177 @@
+package shop.alien.dining.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.dining.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)
+    })
+    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) {
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getUserCouponList(authorization, page, size, tabType, type, couponType);
+    }
+
+    /**
+     * 根据优惠券 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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        String authorization = request.getHeader("Authorization");
+        return diningCouponService.getStoreAllCouponList(authorization, page, size, storeId, couponName, tab, couponsFromType, couponStatus, couponType);
+    }
+}

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

@@ -0,0 +1,60 @@
+package shop.alien.dining.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.dining.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.getOriginalFilename());
+        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());
+        }
+    }
+}

+ 187 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningUserController.java

@@ -0,0 +1,187 @@
+package shop.alien.dining.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.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.dto.VerifyTokenDto;
+import shop.alien.dining.dto.WeChatLoginDto;
+import shop.alien.dining.service.DiningUserService;
+import shop.alien.dining.util.TokenUtil;
+import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.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 不能为空");
+        }
+
+        com.alibaba.fastjson.JSONObject userInfo = shop.alien.dining.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;
+    }
+}

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

@@ -0,0 +1,120 @@
+package shop.alien.dining.controller;
+
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.dining.strategy.payment.PaymentStrategyFactory;
+import shop.alien.entity.result.R;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author lyx
+ * @version 1.0
+ * @date 2025/11/20 17:34
+ */
+@Slf4j
+@Api(tags = {"2.5-支付接口"})
+@CrossOrigin
+@RestController
+@RequestMapping("/payment")
+@RequiredArgsConstructor
+public class PaymentController {
+
+    private final PaymentStrategyFactory paymentStrategyFactory;
+
+
+    @ApiOperation("创建预支付订单")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "price", value = "订单金额", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "subject", value = "订单标题", required = true, paramType = "query", dataType = "String"),
+            @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")
+    })
+    @RequestMapping("/prePay")
+    public R prePay(String price, String subject, String payType, String payer, String orderNo, Integer storeId) {
+        log.info("PaymentController:prePay, price: {}, subject: {}, payType: {}, payer: {}, orderNo: {}, storeId: {}", price, subject, payType, payer, orderNo, storeId);
+        try {
+            return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject, payer, orderNo, storeId);
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 微信支付回调通知接口(符合微信支付 V3 规范)
+     * 验签通过:返回 204 无包体;验签失败:返回 5XX + {"code":"FAIL","message":"失败"}
+     * @param notifyData 回调 JSON 报文
+     * @return 204 无 body 或 5XX + 失败报文
+     */
+    @RequestMapping("/weChatMininNotify")
+    public ResponseEntity<?> notify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[微信支付回调] 收到回调请求, Content-Length={}, Wechatpay-Serial={}", request.getContentLength(), request.getHeader("Wechatpay-Serial"));
+        try {
+            R result = paymentStrategyFactory.getStrategy(PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType()).handleNotify(notifyData, request);
+            if (R.isSuccess(result)) {
+                // 文档要求:验签通过时 HTTP 200 或 204,无需返回应答报文
+                return ResponseEntity.noContent().build();
+            } else {
+                // 文档要求:验签不通过时 4XX/5XX,并返回 {"code":"FAIL","message":"失败"}
+                String message = result.getMsg() != null ? result.getMsg() : "失败";
+                Map<String, String> failBody = new HashMap<>(2);
+                failBody.put("code", "FAIL");
+                failBody.put("message", message);
+                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+            }
+        } catch (Exception e) {
+            log.error("[微信支付回调] 处理异常", e);
+            String msg = e.getMessage() != null ? e.getMessage() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", msg);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        }
+    }
+
+    /**
+     * 查询订单状态
+     * @param transactionId 交易订单号(微信支付订单号/商户订单号)
+     * @param payType 支付类型
+     * @param storeId 店铺ID,用于从 MySQL 获取该店铺支付配置
+     * @return 订单状态信息
+     */
+    @RequestMapping("/searchOrderByOutTradeNoPath")
+    public R searchOrderByOutTradeNoPath(String transactionId, String payType, Integer storeId) {
+        try {
+            return paymentStrategyFactory.getStrategy(payType).searchOrderByOutTradeNoPath(transactionId, storeId);
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 退款接口
+     * @param params 退款参数(包含订单号、退款金额等)
+     * @return 退款结果
+     */
+    @RequestMapping("/refunds")
+    public R refunds(@RequestBody Map<String, String> params) {
+        try {
+            return R.data(paymentStrategyFactory.getStrategy(params.get("payType")).handleRefund(params));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,95 @@
+package shop.alien.dining.controller;
+
+import io.swagger.annotations.*;
+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.dining.service.StoreInfoService;
+
+import java.util.List;
+
+/**
+ * 门店信息查询控制器
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Slf4j
+@Api(tags = {"小程序-门店信息查询"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/info")
+@RequiredArgsConstructor
+public class StoreInfoController {
+
+    private final StoreInfoService storeInfoService;
+
+    @ApiOperation(value = "根据门店ID查询桌号列表", notes = "查询指定门店下的所有桌号")
+    @GetMapping("/tables")
+    public R<List<StoreTable>> getTablesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        try {
+            if (storeId == null) {
+                return R.fail("门店ID不能为空");
+            }
+            List<StoreTable> tables = storeInfoService.getTablesByStoreId(storeId);
+            return R.data(tables);
+        } catch (Exception e) {
+            log.error("查询桌号列表失败: {}", e.getMessage(), e);
+            return R.fail("查询桌号列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据门店ID查询菜品种类列表", notes = "查询指定门店下的所有菜品种类")
+    @GetMapping("/categories")
+    public R<List<StoreCuisineCategory>> getCategoriesByStoreId(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        try {
+            if (storeId == null) {
+                return R.fail("门店ID不能为空");
+            }
+            List<StoreCuisineCategory> categories = storeInfoService.getCategoriesByStoreId(storeId);
+            return R.data(categories);
+        } catch (Exception e) {
+            log.error("查询菜品种类列表失败: {}", e.getMessage(), e);
+            return R.fail("查询菜品种类列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据菜品种类ID查询菜品信息列表", notes = "查询指定分类下的所有菜品信息")
+    @GetMapping("/cuisines")
+    public R<List<StoreCuisine>> getCuisinesByCategoryId(
+            @ApiParam(value = "菜品种类ID", required = true) @RequestParam Integer categoryId) {
+        try {
+            if (categoryId == null) {
+                return R.fail("菜品种类ID不能为空");
+            }
+            List<StoreCuisine> cuisines = storeInfoService.getCuisinesByCategoryId(categoryId);
+            return R.data(cuisines);
+        } catch (Exception e) {
+            log.error("查询菜品信息列表失败: {}", e.getMessage(), e);
+            return R.fail("查询菜品信息列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据商铺ID查询店铺信息和首页展示美食价目表", notes = "查询店铺信息和当前店铺绑定的首页展示美食价目表信息")
+    @GetMapping("/detail/{storeId}")
+    public R<StoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(
+            @ApiParam(value = "商铺ID", required = true) @PathVariable Integer storeId) {
+        try {
+            if (storeId == null) {
+                return R.fail("商铺ID不能为空");
+            }
+            StoreInfoWithHomepageCuisinesDTO result = storeInfoService.getStoreInfoWithHomepageCuisines(storeId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("查询店铺信息和首页展示美食价目表失败: {}", e.getMessage(), e);
+            return R.fail("查询店铺信息和首页展示美食价目表失败: " + e.getMessage());
+        }
+    }
+}

+ 562 - 0
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -0,0 +1,562 @@
+package shop.alien.dining.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.dining.service.CartService;
+import shop.alien.dining.service.SseService;
+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.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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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消息")
+    @PutMapping("/cart/update-tableware")
+    public R<CartDTO> updateTablewareQuantity(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "餐具数量", required = true) @RequestParam Integer quantity) {
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            if (quantity == null || quantity < 0) {
+                return R.fail("餐具数量不能小于0");
+            }
+            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) {
+        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 = "在已有订单基础上添加菜品")
+    @PostMapping("/add-dish")
+    public R<StoreOrder> addDishToOrder(@Valid @RequestBody shop.alien.entity.store.dto.AddDishDTO dto) {
+        try {
+            // 验证 token(用户信息在 StoreOrderService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            StoreOrder order = orderService.addDishToOrder(dto.getOrderId(), dto.getCuisineId(), dto.getQuantity(), dto.getRemark());
+            return R.data(order);
+        } 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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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 = "订单状态") @RequestParam(required = false) Integer orderStatus,
+            @ApiParam(value = "搜索关键词(订单编号或菜品名称,限15字)") @RequestParam(required = false) String 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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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) {
+        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-dining/src/main/java/shop/alien/dining/dto/ChangePhoneDto.java

@@ -0,0 +1,34 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/dto/UserProfileUpdateDto.java

@@ -0,0 +1,56 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/dto/VerifyTokenDto.java

@@ -0,0 +1,20 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/dto/WeChatLoginDto.java

@@ -0,0 +1,29 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/enums/OrderStatus.java

@@ -0,0 +1,48 @@
+package shop.alien.dining.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;
+    }
+}
+

+ 160 - 0
alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java

@@ -0,0 +1,160 @@
+package shop.alien.dining.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(发码、校验、优惠券等)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@FeignClient(name = "alienStoreFeign", url = "${feign.alienStore.url:}")
+public interface AlienStoreFeign {
+
+    /**
+     * 校验短信验证码(复用 store 阿里云短信)
+     *
+     * @param phone        手机号
+     * @param appType      0:用户 1:商家 2:商家web
+     * @param businessType 3:修改手机号
+     * @param code         验证码
+     */
+    @GetMapping("ali/checkSmsCode")
+    R<?> checkSmsCode(
+            @RequestParam("phone") String phone,
+            @RequestParam("appType") Integer appType,
+            @RequestParam("businessType") Integer businessType,
+            @RequestParam("code") Integer code);
+
+    // ==================== 优惠券接口(Feign 调 store life-discount-coupon) ====================
+
+    /**
+     * 获取用户优惠券列表(分 tab:全部/即将过期/已使用/已过期)
+     *
+     * @param authorization 请求头 Authorization,供 store 解析当前用户
+     * @param tabType       0:全部(未使用),1:即将过期,2:已使用,3:已过期
+     * @param page          页码(默认1)
+     * @param size          每页条数(默认10)
+     * @param type          券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)(可选)
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    @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);
+
+    /**
+     * 根据优惠券 ID 获取优惠券详情(含规则、门槛等)
+     *
+     * @param authorization 请求头 Authorization
+     * @param counponId      优惠券 id(接口拼写为 counponId)
+     * @return R.data 为 LifeDiscountCouponVo
+     */
+    @GetMapping("life-discount-coupon/getCounponDetailById")
+    R<LifeDiscountCouponVo> getCounponDetailById(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("counponId") String counponId);
+
+    /**
+     * 获取该门店下用户可用/不可用优惠券列表(按消费金额区分)
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization
+     * @param storeId        门店 id
+     * @param amount         当前消费金额(用于满减门槛判断)
+     * @param couponType     优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 Map:canUseLifeDiscountCouponVos、forbidUseLifeDiscountCouponVos
+     */
+    @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);
+
+    /**
+     * 获取该用户该店铺优惠券列表
+     * couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券
+     *
+     * @param authorization 请求头 Authorization,供 store 解析当前用户
+     * @param storeId       商户id
+     * @param couponType    优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)
+     * @return R.data 为 List&lt;LifeDiscountCouponVo&gt;
+     */
+    @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);
+
+    /**
+     * 获取该店铺所有优惠券(分页), 好友优惠券
+     * 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 为 Page&lt;LifeDiscountCouponVo&gt;(使用 Page 而不是 IPage,因为 Feign 需要具体类型进行反序列化)
+     */
+    @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);
+
+    // ==================== 收藏接口 ====================
+
+    /**
+     * 添加收藏
+     *
+     * @param lifeCollect 收藏对象
+     * @return R.data 为 Boolean(是否成功)
+     */
+    @PostMapping("/collect/addCollect")
+    R<Boolean> addCollect(@RequestBody LifeCollect lifeCollect);
+
+    // ==================== 文件上传接口 ====================
+
+    /**
+     * 上传图片到OSS(单个文件上传,支持图片、视频或PDF)
+     *
+     * @param file 文件对象
+     * @return R.data 为文件路径(String)
+     */
+    @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    R<String> uploadFile(@RequestPart("file") MultipartFile file);
+}

+ 17 - 0
alien-dining/src/main/java/shop/alien/dining/feign/DiningServiceFeign.java

@@ -0,0 +1,17 @@
+package shop.alien.dining.feign;
+
+import org.springframework.cloud.openfeign.FeignClient;
+
+/**
+ * 点餐服务Feign客户端
+ * 用于调用其他微服务
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@FeignClient(name = "dining-service", path = "/api")
+public interface DiningServiceFeign {
+    // TODO: 定义需要调用的其他服务接口
+}
+

+ 67 - 0
alien-dining/src/main/java/shop/alien/dining/filter/PreAuthFilter.java

@@ -0,0 +1,67 @@
+package shop.alien.dining.filter;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 预认证过滤器
+ * 用于处理微信点餐系统的权限验证
+ */
+@Component
+public class PreAuthFilter extends OncePerRequestFilter {
+    // 自定义权限请求头(前端传递角色/权限)
+    private static final String USER_TOKEN_HEADER = "Authorization";
+    private static final String PLATFORM_TYPE_HEADER = "X-platform-type";
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 1. 从请求头获取token信息
+        String token = request.getHeader(USER_TOKEN_HEADER);
+        String platformType = request.getHeader(PLATFORM_TYPE_HEADER);
+
+        /**
+         * 如果不是平台端请求,直接放行,加admin权放行
+         */
+        if(null == platformType || !"platform".equalsIgnoreCase(platformType)) {
+            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                    "notPlatformUser", // 无认证场景,用户名可自定义
+                    null, // 无需密码,置空
+                    AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", "ROLE_ADMIN")));// 权限列表
+            chain.doFilter(request, response);
+            return;
+        }
+
+        if (token == null) {
+            // 无权限头:视为匿名用户(仅能访问公开接口)
+            chain.doFilter(request, response);
+            return;
+        }
+
+        // TODO: 根据实际需求解析token并设置权限
+        // 2. 解析角色为权限集合(Spring Security 角色需以ROLE_开头)
+        // 3. 构建认证令牌
+        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                "diningUser", // 用户名
+                null, // 无需密码,置空
+                AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", "ROLE_USER")));// 权限列表
+        // 4. 设置请求上下文
+        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+        // 5. 将认证信息存入SecurityContext(标记用户已"认证",仅用于授权)
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
+        // 继续执行过滤器链
+        chain.doFilter(request, response);
+    }
+}
+

+ 26 - 0
alien-dining/src/main/java/shop/alien/dining/listener/RedisKeyExpirationListener.java

@@ -0,0 +1,26 @@
+package shop.alien.dining.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * Redis键过期监听器
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Component
+public class RedisKeyExpirationListener implements MessageListener {
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        String expiredKey = message.toString();
+        log.info("Redis键过期: {}", expiredKey);
+        // TODO: 实现键过期后的业务逻辑
+    }
+}
+

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

@@ -0,0 +1,121 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/service/DiningCollectService.java

@@ -0,0 +1,22 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/service/DiningCouponService.java

@@ -0,0 +1,101 @@
+package shop.alien.dining.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);
+
+    /**
+     * 根据优惠券 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-dining/src/main/java/shop/alien/dining/service/DiningService.java

@@ -0,0 +1,150 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/service/DiningUserService.java

@@ -0,0 +1,58 @@
+package shop.alien.dining.service;
+
+import shop.alien.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.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-dining/src/main/java/shop/alien/dining/service/OrderLockService.java

@@ -0,0 +1,61 @@
+package shop.alien.dining.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);
+}

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

@@ -0,0 +1,35 @@
+package shop.alien.dining.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);
+}

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

@@ -0,0 +1,49 @@
+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 java.util.List;
+
+/**
+ * 门店信息查询服务接口
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+public interface StoreInfoService {
+
+    /**
+     * 根据门店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
+     * @return 店铺信息和首页展示美食价目表信息
+     */
+    StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId);
+}

+ 200 - 0
alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java

@@ -0,0 +1,200 @@
+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 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
+     * @param cuisineId 菜品ID
+     * @param quantity 数量
+     * @param remark 备注
+     * @return 订单信息
+     */
+    StoreOrder addDishToOrder(Integer orderId, Integer cuisineId, Integer quantity, String remark);
+
+    /**
+     * 完成订单(支付完成后调用)
+     *
+     * @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);
+}

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

@@ -0,0 +1,1016 @@
+package shop.alien.dining.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.dining.config.BaseRedisService;
+import shop.alien.dining.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.dining.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) {
+            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) {
+                // 合并数量
+                existingItem.setQuantity(existingItem.getQuantity() + fromItem.getQuantity());
+                existingItem.setSubtotalAmount(existingItem.getUnitPrice()
+                        .multiply(BigDecimal.valueOf(existingItem.getQuantity())));
+            } 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);
+
+        // 查找是否已存在餐具项
+        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) {
+            // 更新餐具数量和单价
+            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); // 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) {
+            // 如果购物车中没有门店ID,从桌号获取
+            StoreTable table = storeTableMapper.selectById(tableId);
+            if (table != null) {
+                storeId = table.getStoreId();
+            }
+        }
+        BigDecimal tablewareUnitPrice = getTablewareUnitPrice(storeId);
+
+        // 查找餐具项
+        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 {
+            // 如果已存在,更新单价(可能门店修改了餐具费)
+            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-dining/src/main/java/shop/alien/dining/service/impl/DiningCollectServiceImpl.java

@@ -0,0 +1,39 @@
+package shop.alien.dining.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.dining.feign.AlienStoreFeign;
+import shop.alien.dining.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-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java

@@ -0,0 +1,471 @@
+package shop.alien.dining.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.dining.feign.AlienStoreFeign;
+import shop.alien.dining.service.DiningCouponService;
+import shop.alien.dining.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) {
+        log.info("DiningCouponService.getUserCouponList page={}, size={}, tabType={}, type={}, couponType={}", page, size, tabType, type, couponType);
+        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);
+            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;
+    }
+}

+ 644 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java

@@ -0,0 +1,644 @@
+package shop.alien.dining.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.dining.config.BaseRedisService;
+import shop.alien.dining.service.CartService;
+import shop.alien.dining.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.dining.service.StoreOrderService storeOrderService;
+    private final shop.alien.mapper.StoreOrderMapper storeOrderMapper;
+    private final shop.alien.dining.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);
+        }
+        boolean inDining = Integer.valueOf(1).equals(table.getStatus())
+                && 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);
+
+        // 检查订单锁定
+        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(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);
+
+        // 转换为CartItemDTO
+        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());
+            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);
+    }
+}

+ 690 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java

@@ -0,0 +1,690 @@
+package shop.alien.dining.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.dining.config.BaseRedisService;
+import shop.alien.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.feign.AlienStoreFeign;
+import shop.alien.dining.service.DiningUserService;
+import shop.alien.dining.util.TokenUtil;
+import shop.alien.dining.util.WeChatMiniProgramUtil;
+import shop.alien.dining.util.WeChatMiniProgramUtil.WeChatSessionInfo;
+import shop.alien.entity.result.R;
+import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.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可能已过期或无效");
+            }
+        }
+
+        // 3. 查找或创建用户(传入解析后的手机号,避免重复解析)
+        LifeUser user = findOrCreateUser(openid, parsedPhone);
+        if (user == null) {
+            return null;
+        }
+
+        // 4. 检查用户状态(提前检查,避免不必要的操作)
+        if (!isUserValid(user)) {
+            return null;
+        }
+
+        // 5. 生成并存储 token
+        String token = generateAndStoreToken(openid, user);
+
+        // 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
+     */
+    private String generateAndStoreToken(String openid, LifeUser user) {
+        // 构建 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);
+        
+        // 兼容旧版本:如果用户有手机号,也存储
+        if (StringUtils.isNotBlank(user.getUserPhone())) {
+            baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + user.getUserPhone(), 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", "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-dining/src/main/java/shop/alien/dining/service/impl/OrderLockServiceImpl.java

@@ -0,0 +1,118 @@
+package shop.alien.dining.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.dining.config.BaseRedisService;
+import shop.alien.dining.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;
+    }
+}

+ 152 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/SseServiceImpl.java

@@ -0,0 +1,152 @@
+package shop.alien.dining.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.dining.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) -> {
+            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) {
+                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);
+        }
+    }
+
+    /**
+     * 移除连接
+     */
+    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) {
+                log.error("发送心跳失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                removeConnection(tableId, connectionId);
+            }
+        }, 30, 30, TimeUnit.SECONDS); // 每30秒发送一次心跳
+    }
+}

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

@@ -0,0 +1,137 @@
+package shop.alien.dining.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.mapper.StoreCuisineCategoryMapper;
+import shop.alien.mapper.StoreCuisineMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.dining.service.StoreInfoService;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 门店信息查询服务实现类
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StoreInfoServiceImpl implements StoreInfoService {
+
+    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<>();
+        }
+        
+        // 查询该门店下所有上架的菜品
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, category.getStoreId());
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的菜品
+        
+        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(java.util.stream.Collectors.toList());
+    }
+
+    @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;
+    }
+}

+ 2007 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -0,0 +1,2007 @@
+package shop.alien.dining.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.dining.config.BaseRedisService;
+import shop.alien.dining.service.CartService;
+import shop.alien.dining.service.StoreOrderService;
+import shop.alien.dining.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.dining.service.SseService sseService;
+    private final shop.alien.dining.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;
+        
+        // 验证优惠券(可选,couponId 可以为 null)
+        // 注意:订单总金额、优惠金额、实付金额都由前端计算并传入,后端不再验证
+        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("优惠券不存在");
+            }
+
+            // 验证优惠券是否属于该门店
+            if (!coupon.getStoreId().equals(String.valueOf(table.getStoreId()))) {
+                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; // 是否是更新订单
+        boolean isFirstAddDish = 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);
+                
+                // 在更新订单之前,检查订单明细中是否已经有 is_add_dish=1 的记录
+                // 如果没有,说明这是首次加餐(首次订单发生变化)
+                LambdaQueryWrapper<StoreOrderDetail> checkBeforeAddDishWrapper = new LambdaQueryWrapper<>();
+                checkBeforeAddDishWrapper.eq(StoreOrderDetail::getOrderId, order.getId())
+                        .eq(StoreOrderDetail::getDeleteFlag, 0)
+                        .eq(StoreOrderDetail::getIsAddDish, 1);
+                Integer addDishCountBefore = orderDetailMapper.selectCount(checkBeforeAddDishWrapper);
+                isFirstAddDish = (addDishCountBefore == null || addDishCountBefore == 0);
+                
+                // 更新订单信息(使用前端计算的金额,但经过后端验证)
+                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 表达式
+        final boolean finalIsFirstAddDish = isFirstAddDish; // 用于后续判断
+
+        // 更新优惠券使用记录状态为已下单
+        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和状态
+        if (!finalIsUpdate) {
+            // 新订单:设置订单ID,状态为就餐中(1)
+            table.setCurrentOrderId(finalOrder.getId());
+            table.setStatus(1); // 就餐中
+            storeTableMapper.updateById(table);
+        } else if (finalIsFirstAddDish) {
+            // 更新订单且是首次加餐:更新餐桌状态为加餐状态(3)
+            table.setStatus(3); // 加餐状态
+            table.setUpdatedTime(now);
+            if (userId != null) {
+                table.setUpdatedUserId(userId);
+            }
+            storeTableMapper.updateById(table);
+            log.info("首次加餐(通过更新订单),更新餐桌状态为加餐状态, tableId={}, orderId={}", table.getId(), 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());
+
+        // 更新桌号状态(检查购物车是否为空,如果为空则设为空闲并清空就餐人数)
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            // 检查购物车是否为空,如果为空则设为空闲
+            CartDTO cart = cartService.getCart(order.getTableId());
+            if (cart.getItems() == null || cart.getItems().isEmpty()) {
+                table.setStatus(0); // 空闲
+                table.setDinerCount(null);
+            } else {
+                // 购物车还有商品,保持当前状态或设为就餐中
+                table.setStatus(1); // 就餐中
+            }
+            storeTableMapper.updateById(table);
+        }
+
+        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);
+    }
+
+    @Override
+    public IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus, String keyword) {
+        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 (orderStatus != null) {
+            wrapper.eq(StoreOrder::getOrderStatus, orderStatus);
+        }
+        
+        // 搜索功能:按订单编号模糊搜索
+        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) {
+        log.info("分页查询订单列表(包含菜品信息), storeId={}, tableId={}, orderStatus={}, keyword={}", storeId, tableId, orderStatus, 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 (orderStatus != null) {
+                orderNoWrapper.eq(StoreOrder::getOrderStatus, orderStatus);
+            }
+            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 || orderStatus != null) {
+                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 (orderStatus != null) {
+                    filterWrapper.eq(StoreOrder::getOrderStatus, orderStatus);
+                }
+                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 (orderStatus != null) {
+            wrapper.eq(StoreOrder::getOrderStatus, orderStatus);
+        }
+        // 如果有关键词搜索,只查询匹配的订单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));
+        
+        // 5. 构建返回结果
+        List<StoreOrderPageVO> voList = orders.stream().map(order -> {
+            StoreOrderPageVO vo = new StoreOrderPageVO();
+            vo.setOrder(order);
+            
+            // 获取该订单的菜品列表
+            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());
+                        return item;
+                    })
+                    .collect(Collectors.toList());
+            vo.setCuisineItems(cuisineItems);
+            
+            return vo;
+        }).collect(Collectors.toList());
+        
+        // 6. 构建分页结果
+        Page<StoreOrderPageVO> resultPage = new Page<>(page.getCurrent(), page.getSize());
+        resultPage.setRecords(voList);
+        resultPage.setTotal(orderPage.getTotal());
+        resultPage.setPages(orderPage.getPages());
+        
+        return resultPage;
+    }
+
+    @Override
+    public StoreOrder addDishToOrder(Integer orderId, Integer cuisineId, Integer quantity, String remark) {
+        log.info("加餐, orderId={}, cuisineId={}, quantity={}", orderId, cuisineId, quantity);
+
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "加餐");
+
+        // 验证菜品
+        StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        if (cuisine.getShelfStatus() != 1) {
+            throw new RuntimeException("菜品已下架");
+        }
+
+        // 获取当前用户信息
+        Object[] userInfo = getCurrentUserInfo();
+        Integer userId = (Integer) userInfo[0];
+        String userPhone = (String) userInfo[1];
+
+        // 在加餐之前,检查订单明细中是否已经有 is_add_dish=1 的记录
+        // 如果没有,说明这是首次加餐(首次订单发生变化)
+        LambdaQueryWrapper<StoreOrderDetail> checkBeforeAddDishWrapper = new LambdaQueryWrapper<>();
+        checkBeforeAddDishWrapper.eq(StoreOrderDetail::getOrderId, orderId)
+                .eq(StoreOrderDetail::getDeleteFlag, 0)
+                .eq(StoreOrderDetail::getIsAddDish, 1);
+        Integer addDishCountBefore = orderDetailMapper.selectCount(checkBeforeAddDishWrapper);
+        boolean isFirstAddDish = (addDishCountBefore == null || addDishCountBefore == 0);
+
+        // 查询是否已有该菜品
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(StoreOrderDetail::getCuisineId, cuisineId);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        StoreOrderDetail existingDetail = orderDetailMapper.selectOne(detailWrapper);
+
+        Date now = new Date();
+        Integer quantityBefore = 0; // 加餐前的数量(用于记录变更日志)
+        BigDecimal addAmount; // 新增的金额
+        
+        if (existingDetail != null) {
+            // 记录加餐前的数量
+            quantityBefore = existingDetail.getQuantity();
+            // 计算新增金额(单价 * 新增数量)
+            addAmount = existingDetail.getUnitPrice().multiply(BigDecimal.valueOf(quantity));
+            // 更新数量
+            existingDetail.setQuantity(existingDetail.getQuantity() + quantity);
+            existingDetail.setSubtotalAmount(existingDetail.getUnitPrice()
+                    .multiply(BigDecimal.valueOf(existingDetail.getQuantity())));
+            if (StringUtils.hasText(remark)) {
+                existingDetail.setRemark(remark);
+            }
+            // 如果之前不是加餐,现在标记为加餐
+            if (existingDetail.getIsAddDish() == null || existingDetail.getIsAddDish() == 0) {
+                existingDetail.setIsAddDish(1); // 标记为加餐
+                existingDetail.setAddDishTime(now);
+            }
+            existingDetail.setUpdatedTime(now);
+            existingDetail.setUpdatedUserId(userId);
+            orderDetailMapper.updateById(existingDetail);
+        } else {
+            // 添加新菜品(加餐)
+            quantityBefore = 0; // 新菜品,加餐前数量为0
+            // 计算新增金额(单价 * 数量)
+            addAmount = cuisine.getTotalPrice().multiply(BigDecimal.valueOf(quantity));
+            StoreOrderDetail detail = new StoreOrderDetail();
+            detail.setOrderId(orderId);
+            detail.setOrderNo(order.getOrderNo());
+            detail.setCuisineId(cuisine.getId());
+            detail.setCuisineName(cuisine.getName());
+            
+            // 设置菜品类型:如果为null,默认为1(单品)
+            Integer cuisineType = cuisine.getCuisineType();
+            if (cuisineType == null) {
+                cuisineType = 1; // 默认为单品
+                log.warn("菜品类型为null,使用默认值1, cuisineId={}", cuisine.getId());
+            }
+            detail.setCuisineType(cuisineType);
+            
+            detail.setCuisineImage(cuisine.getImages());
+            detail.setUnitPrice(cuisine.getTotalPrice());
+            detail.setQuantity(quantity);
+            detail.setSubtotalAmount(addAmount);
+            detail.setAddUserId(userId);
+            detail.setAddUserPhone(userPhone);
+            detail.setIsAddDish(1); // 标记为加餐
+            detail.setAddDishTime(now);
+            detail.setRemark(remark);
+            detail.setCreatedUserId(userId);
+            detail.setCreatedTime(now);
+            detail.setUpdatedTime(now);
+            orderDetailMapper.insert(detail);
+        }
+
+        // 增量计算订单总金额(避免重新查询所有订单明细)
+        BigDecimal newTotalAmount = order.getTotalAmount().add(addAmount);
+
+        // 加餐时只更新订单总金额,不计算优惠金额和实付金额(因为可能还会继续加菜,支付时再统一计算)
+        // 如果订单使用了优惠券,不验证最低消费(因为可能还会继续加菜,支付时再验证)
+        order.setTotalAmount(newTotalAmount);
+        // order.setDiscountAmount 和 order.setPayAmount 保持不变,支付时再计算
+        order.setUpdatedTime(new Date());
+        order.setUpdatedUserId(userId);
+        this.updateById(order);
+
+        // 记录加餐变更日志
+        List<shop.alien.entity.store.dto.CartItemDTO> addDishItems = new ArrayList<>();
+        shop.alien.entity.store.dto.CartItemDTO addDishItem = new shop.alien.entity.store.dto.CartItemDTO();
+        addDishItem.setCuisineId(cuisine.getId());
+        addDishItem.setCuisineName(cuisine.getName());
+        addDishItem.setCuisineType(cuisine.getCuisineType());
+        addDishItem.setCuisineImage(cuisine.getImages());
+        addDishItem.setUnitPrice(cuisine.getTotalPrice());
+        // 设置当前数量(加餐后的总数量)
+        addDishItem.setQuantity(quantityBefore + quantity);
+        // 设置已下单数量(加餐前的数量,用于计算变化)
+        addDishItem.setLockedQuantity(quantityBefore);
+        addDishItem.setSubtotalAmount(cuisine.getTotalPrice().multiply(BigDecimal.valueOf(quantity)));
+        addDishItem.setRemark(remark);
+        addDishItems.add(addDishItem);
+        
+        recordOrderChangeLog(orderId, order.getOrderNo(), addDishItems, 2, now, userId, userPhone);
+
+        // 如果是首次加餐(首次订单发生变化),更新餐桌状态为加餐状态(3)
+        if (isFirstAddDish) {
+            StoreTable table = storeTableMapper.selectById(order.getTableId());
+            if (table != null) {
+                table.setStatus(3); // 加餐状态
+                table.setUpdatedTime(new Date());
+                if (userId != null) {
+                    table.setUpdatedUserId(userId);
+                }
+                storeTableMapper.updateById(table);
+                log.info("首次加餐,更新餐桌状态为加餐状态, tableId={}, orderId={}", table.getId(), orderId);
+            }
+        }
+
+        log.info("加餐成功, orderId={}", orderId);
+        return order;
+    }
+
+    @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);
+
+        // 更新桌号状态
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            table.setStatus(0); // 空闲
+            table.setDinerCount(null);
+            storeTableMapper.updateById(table);
+        }
+
+        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);
+
+        // 更新桌号状态
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            table.setStatus(0); // 空闲
+            table.setDinerCount(null);
+            storeTableMapper.updateById(table);
+        }
+
+        log.info("商家手动完成订单成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public StoreOrder updateOrderCoupon(Integer orderId, Integer couponId) {
+        log.info("更新订单优惠券, orderId={}, couponId={}", orderId, couponId);
+
+        // 验证订单状态
+        StoreOrder order = validateOrderForOperation(orderId, 0, "修改优惠券");
+
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponId != null) {
+            // 验证优惠券
+            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));
+
+        // 5. 构建返回结果
+        List<StoreOrderPageVO> voList = orders.stream().map(order -> {
+            StoreOrderPageVO vo = new StoreOrderPageVO();
+            vo.setOrder(order);
+
+            // 获取该订单的菜品列表
+            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());
+                        return item;
+                    })
+                    .collect(Collectors.toList());
+            vo.setCuisineItems(cuisineItems);
+
+            return vo;
+        }).collect(Collectors.toList());
+
+        // 6. 构建分页结果
+        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);
+        
+        // 转换为CartItemDTO
+        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());
+            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));
+        
+        // 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());
+            
+            // 计算批次统计信息
+            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列表
+            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());
+                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 2:
+                return "加餐";
+            case 3:
+                return "更新订单";
+            default:
+                return "未知";
+        }
+    }
+
+    /**
+     * 记录订单变更日志
+     *
+     * @param orderId 订单ID
+     * @param orderNo 订单号
+     * @param items 商品列表
+     * @param operationType 操作类型(1:首次下单, 2:加餐, 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 == 2) {
+                // 加餐:记录加餐的商品
+                quantityBefore = lockedQuantity != null ? lockedQuantity : 0;
+                quantityAfter = currentQuantity != null ? currentQuantity : 0;
+                quantityChange = quantityAfter - quantityBefore;
+            } 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);
+
+        // 1. 迁移购物车
+        shop.alien.entity.store.dto.CartDTO cart = cartService.migrateCart(fromTableId, toTableId);
+
+        // 2. 迁移所有关联数据(订单、订单变更记录、优惠券使用记录等)
+        migrateTableData(fromTableId, toTableId, userId);
+
+        // 3. 查询桌号信息
+        StoreTable fromTable = storeTableMapper.selectById(fromTableId);
+        StoreTable toTable = storeTableMapper.selectById(toTableId);
+
+        // 4. 记录换桌日志
+        StoreTableLog tableLog = new StoreTableLog();
+        tableLog.setStoreId(cart.getStoreId());
+        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);
+        storeTableLogMapper.insert(tableLog);
+
+        // 5. 推送购物车更新消息到新桌号
+        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;
+    }
+
+}

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

@@ -0,0 +1,75 @@
+package shop.alien.dining.strategy.payment;
+
+import shop.alien.entity.result.R;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * 支付策略接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+public interface PaymentStrategy {
+    /** 生成预支付订单(本系统调用时需传 storeId 以从 MySQL 获取店铺支付配置)
+     *
+     * @param price 订单金额
+     * @param subject 订单标题
+     * @param payer 支付者 openid
+     * @param orderNo 订单号
+     * @param storeId 店铺ID,本系统调用时必传,用于获取 StorePaymentConfig
+     * @return 预支付订单信息
+     * @throws Exception 生成异常
+     */
+    R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId) throws Exception;
+
+
+    /**
+     * 处理支付通知
+     *
+     * @param notifyData 支付通知数据
+     * @return 处理结果
+     * @throws Exception 处理异常
+     */
+    R handleNotify(String notifyData, HttpServletRequest request) throws Exception;
+
+    /**
+     * 查询订单状态(本系统调用时需传 storeId 以获取店铺支付配置)
+     *
+     * @param transactionId 交易订单号
+     * @param storeId 店铺ID,本系统调用时必传
+     * @return 订单状态信息
+     * @throws Exception 查询异常
+     */
+    R searchOrderByOutTradeNoPath(String transactionId, Integer storeId) throws Exception;
+
+    /**
+     * 处理退款请求
+     *
+     * @param params 退款请求参数
+     * @return 处理结果
+     * @throws Exception 处理异常
+     */
+    String handleRefund(Map<String,String> params) throws Exception;
+
+    /**
+     * 查询退款记录(本系统调用时需传 storeId 以获取店铺支付配置)
+     *
+     * @param outRefundNo 退款订单号
+     * @param storeId 店铺ID,本系统调用时必传
+     * @return 退款记录信息
+     * @throws Exception 查询异常
+     */
+    R searchRefundRecordByOutRefundNo(String outRefundNo, Integer storeId) throws Exception;
+
+
+    /**
+     * 获取策略类型字符串
+     *
+     * @return 策略类型字符串
+     */
+    String getType();
+}
+

+ 64 - 0
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategyFactory.java

@@ -0,0 +1,64 @@
+package shop.alien.dining.strategy.payment;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付策略工厂
+ *
+ * @author lyx
+ * @date 2025/11/20
+ */
+@Slf4j
+@Component
+public class PaymentStrategyFactory {
+
+    @Autowired
+    private List<PaymentStrategy> paymentStrategies;
+
+    private final Map<String, PaymentStrategy> strategyMap = new HashMap<>();
+
+        /**
+         * 初始化策略映射
+         */
+        @PostConstruct
+        public void init() {
+            if (paymentStrategies != null && !paymentStrategies.isEmpty()) {
+                for (PaymentStrategy strategy : paymentStrategies) {
+                    strategyMap.put(strategy.getType(), strategy);
+                    log.info("注册支付策略: {} -> {}", strategy.getType(), strategy.getClass().getSimpleName());
+                }
+            }
+        }
+
+    /**
+     * 根据类型获取OCR策略
+     *
+     * @param type OCR类型
+     * @return OCR策略实例
+     * @throws IllegalArgumentException 如果类型不存在
+     */
+    public PaymentStrategy getStrategy(String type) {
+        PaymentStrategy strategy = strategyMap.get(type);
+        if (strategy == null) {
+            throw new IllegalArgumentException("不支持的支付类型: " + type);
+        }
+        return strategy;
+    }
+
+    /**
+     * 检查是否支持指定的OCR类型
+     *
+     * @param type OCR类型
+     * @return 是否支持
+     */
+    public boolean supports(String type) {
+        return strategyMap.containsKey(type);
+    }
+}

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

@@ -0,0 +1,773 @@
+package shop.alien.dining.strategy.payment.impl;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
+import com.wechat.pay.java.service.refund.model.Refund;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import shop.alien.dining.service.StoreOrderService;
+import shop.alien.dining.strategy.payment.PaymentStrategy;
+import shop.alien.dining.util.WXPayUtility;
+import shop.alien.dining.util.WeChatPayUtil;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.mapper.StorePaymentConfigMapper;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+
+/**
+ * 微信支付小程序策略
+ *
+ * @author lyx
+ * @date 2025/11/20
+ */
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@RefreshScope
+public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
+    /**
+     * 微信支付api主机地址
+     */
+    @Value("${payment.wechatPay.host}")
+    private String wechatPayApiHost;
+    /**
+     * 微信支付预支付路径
+     */
+    @Value("${payment.wechatPay.business.miniProgram.prePayPath:/v3/pay/transactions/jsapi}")
+    private String prePayPath;
+    /**
+     * 微信支付预支付通知路径
+     */
+    @Value("${payment.wechatPay.business.miniProgram.prePayNotifyUrl:https://frp-way.com:12473/aliendining/payment/weChatMininNotify}")
+    private String prePayNotifyUrl;
+    /**
+     * 微信支付退款通知路径
+     */
+    @Value("${payment.wechatPay.business.miniProgram.refundNotifyUrl:https://www.weixin.qq.com/wxpay/pay.php}")
+    private String refundNotifyUrl;
+    /**
+     * 微信支付查询退款状态路径
+     */
+    @Value("${payment.wechatPay.searchRefundStatusByOutRefundNoPath:/v3/refund/domestic/refunds/{out_refund_no}}")
+    private String searchRefundStatusByOutRefundNoPath;
+
+    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath}")
+    private String searchOrderByOutTradeNoPath;
+
+    private final StoreOrderService storeOrderService;
+    private final StorePaymentConfigMapper storePaymentConfigMapper;
+    private final ObjectMapper objectMapper;
+
+    private static String POSTMETHOD = "POST";
+    private static String GETMETHOD = "GET";
+
+    /**
+     * 根据店铺ID从 MySQL 获取支付配置(本系统内部调用时使用)
+     */
+    private StorePaymentConfig getConfigByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getStoreId, storeId);
+        return storePaymentConfigMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 根据微信支付公钥序列号获取支付配置(外部回调如支付通知时使用,通过请求头 Wechatpay-Serial 判断)
+     */
+    private StorePaymentConfig getConfigByWechatPayPublicKeyId(String wechatPayPublicKeyId) {
+        if (!StringUtils.hasText(wechatPayPublicKeyId)) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getWechatPayPublicKeyId, wechatPayPublicKeyId);
+        return storePaymentConfigMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 从 StorePaymentConfig 加载商户私钥
+     */
+    private PrivateKey loadPrivateKeyFromConfig(StorePaymentConfig config) {
+        if (config == null) {
+            return null;
+        }
+        if (config.getWechatPrivateKeyFile() != null && config.getWechatPrivateKeyFile().length > 0) {
+            String keyStr = new String(config.getWechatPrivateKeyFile(), StandardCharsets.UTF_8);
+            return WXPayUtility.loadPrivateKeyFromString(keyStr);
+        }
+        if (StringUtils.hasText(config.getWechatPrivateKeyPath())) {
+            return WXPayUtility.loadPrivateKeyFromPath(config.getWechatPrivateKeyPath());
+        }
+        return null;
+    }
+
+    /**
+     * 从 StorePaymentConfig 加载微信支付公钥
+     */
+    private PublicKey loadPublicKeyFromConfig(StorePaymentConfig config) {
+        if (config == null) {
+            return null;
+        }
+        if (config.getWechatPayPublicKeyFile() != null && config.getWechatPayPublicKeyFile().length > 0) {
+            String keyStr = new String(config.getWechatPayPublicKeyFile(), StandardCharsets.UTF_8);
+            return WXPayUtility.loadPublicKeyFromString(keyStr);
+        }
+        if (StringUtils.hasText(config.getWechatPayPublicKeyFilePath())) {
+            return WXPayUtility.loadPublicKeyFromPath(config.getWechatPayPublicKeyFilePath());
+        }
+        return null;
+    }
+
+    @Override
+    public R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId) throws Exception {
+        // 本系统调用:通过 storeId 从 MySQL 获取店铺支付配置
+        if (storeId == null) {
+            log.warn("createPrePayOrder 缺少 storeId,无法获取支付配置");
+            return R.fail("店铺ID不能为空");
+        }
+        StorePaymentConfig config = getConfigByStoreId(storeId);
+        if (config == null) {
+            log.warn("未找到店铺支付配置 storeId={}", storeId);
+            return R.fail("未找到该店铺的微信支付配置");
+        }
+        PrivateKey privateKey = loadPrivateKeyFromConfig(config);
+        if (privateKey == null) {
+            return R.fail("店铺微信支付私钥未配置或加载失败");
+        }
+        String appId = config.getWechatMiniAppId() != null ? config.getWechatMiniAppId() : config.getWechatAppId();
+        if (!StringUtils.hasText(appId)) {
+            return R.fail("店铺微信小程序 appId 未配置");
+        }
+        String mchId = config.getWechatMchId();
+        if (!StringUtils.hasText(mchId)) {
+            return R.fail("店铺微信支付商户号未配置");
+        }
+
+        DirectAPIv3JsapiPrepayRequest request = new DirectAPIv3JsapiPrepayRequest();
+        request.appid = appId;
+        request.mchid = mchId;
+        request.description = subject;
+        request.outTradeNo = orderNo;
+        request.notifyUrl = prePayNotifyUrl;
+        request.amount = new CommonAmountInfo();
+        request.amount.total = Long.parseLong(price);
+        request.payer = new JsapiReqPayerInfo();
+        request.payer.openid = payer;
+        try {
+            DirectAPIv3JsapiPrepayResponse response = doCreatePrePayOrder(request, config, privateKey);
+            log.info("微信预支付订单创建成功,预支付ID:{}", response.prepayId);
+            Map<String, String> result = new HashMap<>();
+            response.prepayId = "prepay_id=" + response.prepayId;
+            result.put("prepayId", response.prepayId);
+            result.put("appId", appId);
+            result.put("mchId", mchId);
+            result.put("orderNo", request.outTradeNo);
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String message = String.format("%s\n%s\n%s\n%s\n", appId, timestamp, nonce, response.prepayId);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            result.put("signType", "RSA");
+            result.put("sign", Base64.getEncoder().encodeToString(sign.sign()));
+            result.put("timestamp", String.valueOf(timestamp));
+            result.put("nonce", nonce);
+            return R.data(result);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("微信支付预支付失败,状态码:{},错误信息:{}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @Override
+    public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[微信支付回调] 进入 handleNotify, notifyData 长度={}", notifyData != null ? notifyData.length() : 0);
+        // 外部回调:通过请求头 Wechatpay-Serial 从 MySQL 查找对应店铺的支付配置
+        String serial = request.getHeader("Wechatpay-Serial");
+        String signature = request.getHeader("Wechatpay-Signature");
+        String timestamp = request.getHeader("Wechatpay-Timestamp");
+        String nonce = request.getHeader("Wechatpay-Nonce");
+
+        if (serial == null || signature == null || timestamp == null || nonce == null) {
+            log.warn("微信支付回调验签失败:核心头参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
+                    serial, signature, timestamp, nonce);
+            return R.fail("验签参数缺失");
+        }
+
+        if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
+            log.info("[微信支付回调] 收到签名探测请求,直接应答成功");
+            return R.success("OK");
+        }
+
+        StorePaymentConfig config = getConfigByWechatPayPublicKeyId(serial);
+        if (config == null) {
+            log.warn("微信支付回调:未找到公钥序列号对应的店铺配置 serial={}", serial);
+            return R.fail("未找到对应支付配置");
+        }
+        PublicKey wechatPayPublicKey = loadPublicKeyFromConfig(config);
+        if (wechatPayPublicKey == null) {
+            log.warn("微信支付回调:加载公钥失败 serial={}", serial);
+            return R.fail("验签公钥加载失败");
+        }
+
+        StringBuilder signStr = new StringBuilder();
+        signStr.append(timestamp).append("\n");
+        signStr.append(nonce).append("\n");
+        signStr.append(notifyData).append("\n");
+        Signature sign = Signature.getInstance("SHA256withRSA");
+        byte[] signatureBytes = Base64.getDecoder().decode(signature);
+        sign.initVerify(wechatPayPublicKey);
+        sign.update(signStr.toString().getBytes(StandardCharsets.UTF_8));
+
+        if (sign.verify(signatureBytes)) {
+            final String notifyDataCopy = notifyData;
+            final StorePaymentConfig configCopy = config;
+            CompletableFuture.runAsync(() -> processNotifyBusiness(notifyDataCopy, configCopy));
+            return R.success("OK");
+        } else {
+            return R.fail("Verified error");
+        }
+    }
+
+    /**
+     * 异步处理回调业务:解密并更新订单状态(文档建议应答后再处理业务,避免超时)
+     * @param config 已通过 Wechatpay-Serial 解析得到的店铺支付配置,内含 apiV3Key
+     */
+    private void processNotifyBusiness(String notifyData, StorePaymentConfig config) {
+        try {
+            if (config == null || !StringUtils.hasText(config.getApiV3Key())) {
+                log.warn("微信支付回调:无可用 apiV3Key 无法解密");
+                return;
+            }
+            JsonNode rootNode = objectMapper.readTree(notifyData);
+            JsonNode resourceNode = rootNode.get("resource");
+            if (resourceNode == null) {
+                log.warn("微信支付回调报文无resource字段");
+                return;
+            }
+            String encryptAlgorithm = resourceNode.get("algorithm").asText();
+            String resourceNonce = resourceNode.get("nonce").asText();
+            String associatedData = resourceNode.has("associated_data") && !resourceNode.get("associated_data").isNull()
+                    ? resourceNode.get("associated_data").asText() : "";
+            String ciphertext = resourceNode.get("ciphertext").asText();
+            if (!"AEAD_AES_256_GCM".equals(encryptAlgorithm)) {
+                log.warn("不支持的加密算法:{}", encryptAlgorithm);
+                return;
+            }
+            String plainBusinessData = WeChatPayUtil.decrypt(
+                    config.getApiV3Key(), resourceNonce, associatedData, ciphertext);
+            log.info("微信支付回调解密后的业务信息:{}", plainBusinessData);
+            JSONObject jsonObject = JSONObject.parseObject(plainBusinessData);
+            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));
+                if (storeOrder != null && storeOrder.getPayStatus() != 1) {
+                    storeOrder.setPayStatus(1);
+                    storeOrder.setOrderStatus(1);
+                    if (storeOrderService.updateById(storeOrder)) {
+                        log.info("小程序更新订单成功,订单号outTradeNo:{}", outTradeNo);
+                        // 支付完成后,清空购物车和重置餐桌状态(保留订单数据,不删除订单)
+                        try {
+                            storeOrderService.resetTableAfterPayment(storeOrder.getTableId());
+                            log.info("支付完成后重置餐桌成功, tableId={}", storeOrder.getTableId());
+                        } catch (Exception e) {
+                            log.error("支付完成后重置餐桌失败, tableId={}, error={}", storeOrder.getTableId(), e.getMessage(), e);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[微信支付回调] 异步处理业务异常", e);
+        }
+    }
+
+    @Override
+    public R searchOrderByOutTradeNoPath(String transactionId, Integer storeId) throws Exception {
+        log.info("查询微信支付订单状态,交易订单号:{}, storeId:{}", transactionId, storeId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        StorePaymentConfig config = getConfigByStoreId(storeId);
+        if (config == null) {
+            return R.fail("未找到该店铺的微信支付配置");
+        }
+        PrivateKey privateKey = loadPrivateKeyFromConfig(config);
+        PublicKey publicKey = loadPublicKeyFromConfig(config);
+        if (privateKey == null || publicKey == null) {
+            return R.fail("店铺微信支付密钥未配置或加载失败");
+        }
+        QueryByWxTradeNoRequest request = new QueryByWxTradeNoRequest();
+        request.transactionId = transactionId;
+        request.mchid = config.getWechatMchId();
+        try {
+            DirectAPIv3QueryResponse response = searchOrderRun(request, config, privateKey, publicKey);
+            return R.data(response);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("查询微信支付订单状态失败,状态码:{},错误信息:{}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    public DirectAPIv3QueryResponse searchOrderRun(QueryByWxTradeNoRequest request,
+                                                   StorePaymentConfig config,
+                                                   PrivateKey privateKey,
+                                                   PublicKey wechatPayPublicKey) {
+        String uri = searchOrderByOutTradeNoPath;
+        uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
+        Map<String, Object> args = new HashMap<>();
+        args.put("mchid", request.mchid);
+        String queryString = WXPayUtility.urlEncode(args);
+        if (!queryString.isEmpty()) {
+            uri = uri + "?" + queryString;
+        }
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(request.mchid, config.getMerchantSerialNumber(), privateKey, GETMETHOD, uri, null));
+        reqBuilder.method(GETMETHOD, null);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey,
+                        httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
+            } else {
+                throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+        }
+    }
+
+    @Override
+    public String handleRefund(Map<String, String> params) throws Exception {
+        return null;
+    }
+
+    @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo, Integer storeId) throws Exception {
+        Logger logger = LoggerFactory.getLogger(this.getClass());
+
+        if (outRefundNo == null || outRefundNo.trim().isEmpty()) {
+            logger.error("微信退款查询失败:外部退款单号为空");
+            return R.fail("外部退款单号不能为空");
+        }
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        StorePaymentConfig config = getConfigByStoreId(storeId);
+        if (config == null) {
+            return R.fail("未找到该店铺的微信支付配置");
+        }
+        PrivateKey privateKey = loadPrivateKeyFromConfig(config);
+        PublicKey publicKey = loadPublicKeyFromConfig(config);
+        if (privateKey == null || publicKey == null) {
+            return R.fail("店铺微信支付密钥未配置或加载失败");
+        }
+
+        QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
+        request.setOutRefundNo(outRefundNo);
+
+        try {
+            Refund response = doSearchRefundRecordByOutRefundNo(request, config, privateKey, publicKey);
+
+            // 3. 空值校验(避免response为空导致空指针)
+            if (response == null) {
+                logger.error("微信退款查询失败:外部退款单号{},返回结果为空", outRefundNo);
+                return R.fail("微信支付查询退款记录失败:返回结果为空");
+            }
+
+            logger.info("微信退款查询结果:外部退款单号{},退款状态{}",
+                    outRefundNo, response.getStatus());
+
+            // 4. 细化退款状态判断(覆盖微信支付核心退款状态)
+            String refundStatus = String.valueOf(response.getStatus());
+            switch (refundStatus) {
+                case "SUCCESS":
+                    // 退款成功:执行成功业务逻辑(如更新订单状态、通知用户等)
+                    logger.info("退款成功:外部退款单号{}", outRefundNo);
+                    // TODO: 补充你的成功业务逻辑(例:updateOrderRefundStatus(outRefundNo, "SUCCESS");)
+                    return R.data(response);
+
+                case "REFUNDCLOSE":
+                    // 退款关闭:执行关闭逻辑(如记录关闭原因、人工介入等)
+                    logger.warn("退款关闭:外部退款单号{},原因{}", outRefundNo);
+                    return R.fail("微信支付退款已关闭");
+
+                case "PROCESSING":
+                    // 退款处理中:执行等待逻辑(如提示用户等待、定时任务重试等)
+                    logger.info("退款处理中:外部退款单号{}", outRefundNo);
+                    return R.fail("微信支付退款处理中,请稍后再查");
+
+                case "CHANGE":
+                    // 退款异常:执行异常处理(如记录异常、人工核查等)
+                    logger.error("退款异常:外部退款单号{}", outRefundNo);
+                    return R.fail("微信支付退款异常");
+
+                default:
+                    // 未知状态:兜底处理
+                    logger.error("退款状态未知:外部退款单号{},状态码{}", outRefundNo, refundStatus);
+                    return R.fail("微信支付查询退款记录失败:未知状态码" + refundStatus);
+            }
+
+        } catch (WXPayUtility.ApiException e) {
+            // 5. 异常处理:细化异常日志,便于排查
+            logger.error("微信退款查询API异常:外部退款单号{},错误码{},错误信息{}",
+                    outRefundNo, e.getErrorCode(), e.getMessage(), e);
+            return R.fail("微信支付查询退款记录失败:" + e.getMessage() + "(错误码:" + e.getErrorCode() + ")");
+        } catch (Exception e) {
+            // 6. 兜底异常:捕获非API异常(如空指针、网络异常等)
+            logger.error("微信退款查询系统异常:外部退款单号{}", outRefundNo, e);
+            return R.fail("微信支付查询退款记录失败:系统异常,请联系管理员");
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType();
+    }
+
+    public Refund doSearchRefundRecordByOutRefundNo(QueryByOutRefundNoRequest request,
+                                                   StorePaymentConfig config,
+                                                   PrivateKey privateKey,
+                                                   PublicKey wechatPayPublicKey) {
+        String uri = searchRefundStatusByOutRefundNoPath;
+        uri = uri.replace("{out_refund_no}", WXPayUtility.urlEncode(request.getOutRefundNo()));
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, GETMETHOD, uri, null));
+        reqBuilder.method(GETMETHOD, null);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey,
+                        httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, Refund.class);
+            } else {
+                throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+        }
+    }
+
+    public DirectAPIv3JsapiPrepayResponse doCreatePrePayOrder(DirectAPIv3JsapiPrepayRequest request,
+                                                             StorePaymentConfig config,
+                                                             PrivateKey privateKey) {
+        String uri = prePayPath;
+        String reqBody = WXPayUtility.toJson(request);
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+        reqBuilder.method(POSTMETHOD, requestBody);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                PublicKey wechatPayPublicKey = loadPublicKeyFromConfig(config);
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey,
+                        httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
+            } else {
+                throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+        }
+    }
+
+    public static class DirectAPIv3JsapiPrepayRequest {
+        @SerializedName("appid")
+        public String appid;
+
+        @SerializedName("mchid")
+        public String mchid;
+
+        @SerializedName("description")
+        public String description;
+
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+
+        @SerializedName("time_expire")
+        public String timeExpire;
+
+        @SerializedName("attach")
+        public String attach;
+
+        @SerializedName("notify_url")
+        public String notifyUrl;
+
+        @SerializedName("goods_tag")
+        public String goodsTag;
+
+        @SerializedName("support_fapiao")
+        public Boolean supportFapiao;
+
+        @SerializedName("amount")
+        public CommonAmountInfo amount;
+
+        @SerializedName("payer")
+        public JsapiReqPayerInfo payer;
+
+        @SerializedName("detail")
+        public CouponInfo detail;
+
+        @SerializedName("scene_info")
+        public CommonSceneInfo sceneInfo;
+
+        @SerializedName("settle_info")
+        public SettleInfo settleInfo;
+    }
+
+    public static class DirectAPIv3JsapiPrepayResponse {
+        @SerializedName("prepay_id")
+        public String prepayId;
+    }
+
+    public static class CommonAmountInfo {
+        @SerializedName("total")
+        public Long total;
+
+        @SerializedName("currency")
+        public String currency;
+    }
+
+    public static class JsapiReqPayerInfo {
+        @SerializedName("openid")
+        public String openid;
+    }
+
+    public static class CouponInfo {
+        @SerializedName("cost_price")
+        public Long costPrice;
+
+        @SerializedName("invoice_id")
+        public String invoiceId;
+
+        @SerializedName("goods_detail")
+        public List<GoodsDetail> goodsDetail;
+    }
+
+    public static class CommonSceneInfo {
+        @SerializedName("payer_client_ip")
+        public String payerClientIp;
+
+        @SerializedName("device_id")
+        public String deviceId;
+
+        @SerializedName("store_info")
+        public StoreInfo storeInfo;
+    }
+
+    public static class SettleInfo {
+        @SerializedName("profit_sharing")
+        public Boolean profitSharing;
+    }
+
+    public static class GoodsDetail {
+        @SerializedName("merchant_goods_id")
+        public String merchantGoodsId;
+
+        @SerializedName("wechatpay_goods_id")
+        public String wechatpayGoodsId;
+
+        @SerializedName("goods_name")
+        public String goodsName;
+
+        @SerializedName("quantity")
+        public Long quantity;
+
+        @SerializedName("unit_price")
+        public Long unitPrice;
+    }
+
+    public static class StoreInfo {
+        @SerializedName("id")
+        public String id;
+
+        @SerializedName("name")
+        public String name;
+
+        @SerializedName("area_code")
+        public String areaCode;
+
+        @SerializedName("address")
+        public String address;
+    }
+
+    public static class QueryByWxTradeNoRequest {
+        @SerializedName("mchid")
+        @Expose(serialize = false)
+        public String mchid;
+
+        @SerializedName("transaction_id")
+        @Expose(serialize = false)
+        public String transactionId;
+    }
+
+    public static class DirectAPIv3QueryResponse {
+        @SerializedName("appid")
+        public String appid;
+
+        @SerializedName("mchid")
+        public String mchid;
+
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+
+        @SerializedName("transaction_id")
+        public String transactionId;
+
+        @SerializedName("trade_type")
+        public String tradeType;
+
+        @SerializedName("trade_state")
+        public String tradeState;
+
+        @SerializedName("trade_state_desc")
+        public String tradeStateDesc;
+
+        @SerializedName("bank_type")
+        public String bankType;
+
+        @SerializedName("attach")
+        public String attach;
+
+        @SerializedName("success_time")
+        public String successTime;
+
+        @SerializedName("payer")
+        public CommRespPayerInfo payer;
+
+        @SerializedName("amount")
+        public CommRespAmountInfo amount;
+
+        @SerializedName("scene_info")
+        public CommRespSceneInfo sceneInfo;
+
+        @SerializedName("promotion_detail")
+        public List<PromotionDetail> promotionDetail;
+    }
+    public static class CommRespPayerInfo {
+        @SerializedName("openid")
+        public String openid;
+    }
+
+    public static class CommRespAmountInfo {
+        @SerializedName("total")
+        public Long total;
+
+        @SerializedName("payer_total")
+        public Long payerTotal;
+
+        @SerializedName("currency")
+        public String currency;
+
+        @SerializedName("payer_currency")
+        public String payerCurrency;
+    }
+
+    public static class CommRespSceneInfo {
+        @SerializedName("device_id")
+        public String deviceId;
+    }
+
+    public static class PromotionDetail {
+        @SerializedName("coupon_id")
+        public String couponId;
+
+        @SerializedName("name")
+        public String name;
+
+        @SerializedName("scope")
+        public String scope;
+
+        @SerializedName("type")
+        public String type;
+
+        @SerializedName("amount")
+        public Long amount;
+
+        @SerializedName("stock_id")
+        public String stockId;
+
+        @SerializedName("wechatpay_contribute")
+        public Long wechatpayContribute;
+
+        @SerializedName("merchant_contribute")
+        public Long merchantContribute;
+
+        @SerializedName("other_contribute")
+        public Long otherContribute;
+
+        @SerializedName("currency")
+        public String currency;
+
+        @SerializedName("goods_detail")
+        public List<GoodsDetailInPromotion> goodsDetail;
+    }
+
+    public static class GoodsDetailInPromotion {
+        @SerializedName("goods_id")
+        public String goodsId;
+
+        @SerializedName("quantity")
+        public Long quantity;
+
+        @SerializedName("unit_price")
+        public Long unitPrice;
+
+        @SerializedName("discount_amount")
+        public Long discountAmount;
+
+        @SerializedName("goods_remark")
+        public String goodsRemark;
+    }
+}
+

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

@@ -0,0 +1,227 @@
+package shop.alien.dining.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;
+        }
+    }
+}

+ 856 - 0
alien-dining/src/main/java/shop/alien/dining/util/WXPayUtility.java

@@ -0,0 +1,856 @@
+package shop.alien.dining.util;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.bouncycastle.crypto.digests.SM3Digest;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.Map.Entry;
+
+public class WXPayUtility {
+    private static final Gson gson = new GsonBuilder()
+            .disableHtmlEscaping()
+            .addSerializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.serialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.deserialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .create();
+    private static final char[] SYMBOLS =
+            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+    private static final SecureRandom random = new SecureRandom();
+
+    /**
+     * 将 Object 转换为 JSON 字符串
+     */
+    public static String toJson(Object object) {
+        return gson.toJson(object);
+    }
+
+    /**
+     * 将 JSON 字符串解析为特定类型的实例
+     */
+    public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+        return gson.fromJson(json, classOfT);
+    }
+
+    /**
+     * 从公私钥文件路径中读取文件内容
+     *
+     * @param keyPath 文件路径
+     * @return 文件内容
+     */
+    private static String readKeyStringFromPath(String keyPath) {
+        try {
+            return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+     *
+     * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePrivate(
+                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的私钥文件中加载私钥
+     *
+     * @param keyPath 私钥文件路径
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+        return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+     *
+     * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+                    .replace("-----END PUBLIC KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePublic(
+                    new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的公钥文件中加载公钥
+     *
+     * @param keyPath 公钥文件路径
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromPath(String keyPath) {
+        return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+     */
+    public static String createNonce(int length) {
+        char[] buf = new char[length];
+        for (int i = 0; i < length; ++i) {
+            buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+        }
+        return new String(buf);
+    }
+
+    /**
+     * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+     *
+     * @param publicKey 加密用公钥对象
+     * @param plaintext 待加密明文
+     * @return 加密后密文
+     */
+    public static String encrypt(PublicKey publicKey, String plaintext) {
+        final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+        } catch (BadPaddingException | IllegalBlockSizeException e) {
+            throw new IllegalArgumentException("Plaintext is too long", e);
+        }
+    }
+
+    public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
+                                        byte[] ciphertext) {
+        final String transformation = "AES/GCM/NoPadding";
+        final String algorithm = "AES";
+        final int tagLengthBit = 128;
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(
+                    Cipher.DECRYPT_MODE,
+                    new SecretKeySpec(key, algorithm),
+                    new GCMParameterSpec(tagLengthBit, nonce));
+            if (associatedData != null) {
+                cipher.updateAAD(associatedData);
+            }
+            return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
+        } catch (InvalidKeyException
+                 | InvalidAlgorithmParameterException
+                 | BadPaddingException
+                 | IllegalBlockSizeException
+                 | NoSuchAlgorithmException
+                 | NoSuchPaddingException e) {
+            throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
+                    transformation), e);
+        }
+    }
+
+    /**
+     * 使用私钥按照指定算法进行签名
+     *
+     * @param message    待签名串
+     * @param algorithm  签名算法,如 SHA256withRSA
+     * @param privateKey 签名用私钥对象
+     * @return 签名结果
+     */
+    public static String sign(String message, String algorithm, PrivateKey privateKey) {
+        byte[] sign;
+        try {
+            Signature signature = Signature.getInstance(algorithm);
+            signature.initSign(privateKey);
+            signature.update(message.getBytes(StandardCharsets.UTF_8));
+            sign = signature.sign();
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+        } catch (SignatureException e) {
+            throw new RuntimeException("An error occurred during the sign process.", e);
+        }
+        return Base64.getEncoder().encodeToString(sign);
+    }
+
+    /**
+     * 使用公钥按照特定算法验证签名
+     *
+     * @param message   待签名串
+     * @param signature 待验证的签名内容
+     * @param algorithm 签名算法,如:SHA256withRSA
+     * @param publicKey 验签用公钥对象
+     * @return 签名验证是否通过
+     */
+    public static boolean verify(String message, String signature, String algorithm,
+                                 PublicKey publicKey) {
+        try {
+            Signature sign = Signature.getInstance(algorithm);
+            sign.initVerify(publicKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            return sign.verify(Base64.getDecoder().decode(signature));
+        } catch (SignatureException e) {
+            return false;
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+     *
+     * @param mchid               商户号
+     * @param certificateSerialNo 商户API证书序列号
+     * @param privateKey          商户API证书私钥
+     * @param method              请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+     * @param uri                 请求接口的URL
+     * @param body                请求接口的Body
+     * @return 构造好的微信支付APIv3 Authorization 头
+     */
+    public static String buildAuthorization(String mchid, String certificateSerialNo,
+                                            PrivateKey privateKey,
+                                            String method, String uri, String body) {
+        String nonce = createNonce(32);
+        long timestamp = Instant.now().getEpochSecond();
+
+        String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+                body == null ? "" : body);
+
+        String signature = sign(message, "SHA256withRSA", privateKey);
+
+        return String.format(
+                "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+                        "timestamp=\"%d\",serial_no=\"%s\"",
+                mchid, nonce, signature, timestamp, certificateSerialNo);
+    }
+
+    /**
+     * 计算输入流的哈希值
+     *
+     * @param inputStream 输入流
+     * @param algorithm   哈希算法名称,如 "SHA-256", "SHA-1"
+     * @return 哈希值的十六进制字符串
+     */
+    private static String calculateHash(InputStream inputStream, String algorithm) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance(algorithm);
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                digest.update(buffer, 0, bytesRead);
+            }
+            byte[] hashBytes = digest.digest();
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hashBytes) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(algorithm + " algorithm not available", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error reading from input stream", e);
+        }
+    }
+
+    /**
+     * 计算输入流的 SHA256 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SHA256 哈希值的十六进制字符串
+     */
+    public static String sha256(InputStream inputStream) {
+        return calculateHash(inputStream, "SHA-256");
+    }
+
+    /**
+     * 计算输入流的 SHA1 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SHA1 哈希值的十六进制字符串
+     */
+    public static String sha1(InputStream inputStream) {
+        return calculateHash(inputStream, "SHA-1");
+    }
+
+    /**
+     * 计算输入流的 SM3 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SM3 哈希值的十六进制字符串
+     */
+    public static String sm3(InputStream inputStream) {
+        // 确保Bouncy Castle Provider已注册
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+
+        try {
+            SM3Digest digest = new SM3Digest();
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                digest.update(buffer, 0, bytesRead);
+            }
+            byte[] hashBytes = new byte[digest.getDigestSize()];
+            digest.doFinal(hashBytes, 0);
+
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hashBytes) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (IOException e) {
+            throw new RuntimeException("Error reading from input stream", e);
+        }
+    }
+
+    /**
+     * 对参数进行 URL 编码
+     *
+     * @param content 参数内容
+     * @return 编码后的内容
+     */
+    public static String urlEncode(String content) {
+        try {
+            return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 对参数Map进行 URL 编码,生成 QueryString
+     *
+     * @param params Query参数Map
+     * @return QueryString
+     */
+    public static String urlEncode(Map<String, Object> params) {
+        if (params == null || params.isEmpty()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        for (Entry<String, Object> entry : params.entrySet()) {
+            if (entry.getValue() == null) {
+                continue;
+            }
+
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value instanceof List) {
+                List<?> list = (List<?>) entry.getValue();
+                for (Object temp : list) {
+                    appendParam(result, key, temp);
+                }
+            } else {
+                appendParam(result, key, value);
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 将键值对 放入返回结果
+     *
+     * @param result 返回的query string
+     * @param key 属性
+     * @param value 属性值
+     */
+    private static void appendParam(StringBuilder result, String key, Object value) {
+        if (result.length() > 0) {
+            result.append("&");
+        }
+
+        String valueString;
+        // 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON
+        if (value instanceof String || value instanceof Number ||
+                value instanceof Boolean || value instanceof Enum) {
+            valueString = value.toString();
+        } else {
+            valueString = toJson(value);
+        }
+
+        result.append(key)
+                .append("=")
+                .append(urlEncode(valueString));
+    }
+
+    /**
+     * 从应答中提取 Body
+     *
+     * @param response HTTP 请求应答对象
+     * @return 应答中的Body内容,Body为空时返回空字符串
+     */
+    public static String extractBody(Response response) {
+        if (response.body() == null) {
+            return "";
+        }
+
+        try {
+            BufferedSource source = response.body().source();
+            return source.readUtf8();
+        } catch (IOException e) {
+            throw new RuntimeException(String.format("An error occurred during reading response body. " +
+                    "Status: %d", response.code()), e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+     *
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付应答 Header 列表
+     * @param body                 微信支付应答 Body
+     */
+    public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+                                        Headers headers,
+                                        String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        String requestId = headers.get("Request-ID");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
+                                timestamp, requestId));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
+                            timestamp, requestId));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
+                            "%s", wechatpayPublicKeyId, serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+                                    + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+                            headers.get("Request-ID"), headers, body));
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey 微信支付公钥对象
+     * @param headers 微信支付通知 Header 列表
+     * @param body 微信支付通知 Body
+     */
+    public static void validateNotification(String wechatpayPublicKeyId,
+                                            PublicKey wechatpayPublicKey, Headers headers,
+                                            String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
+                                    "Remote: %s",
+                            wechatpayPublicKeyId,
+                            serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, WechatPay signature is incorrect.\n"
+                                    + "responseHeader[%s]\tresponseBody[%.1024s]",
+                            headers, body));
+        }
+    }
+
+    /**
+     * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常
+     * @param apiv3Key 商户的 APIv3 Key
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付请求 Header 列表
+     * @param body                 微信支付请求 Body
+     * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问
+     */
+    public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
+                                                 PublicKey wechatpayPublicKey, Headers headers,
+                                                 String body) {
+        validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
+        Notification notification = gson.fromJson(body, Notification.class);
+        notification.decrypt(apiv3Key);
+        return notification;
+    }
+
+    /**
+     * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+     */
+    public static class ApiException extends RuntimeException {
+        private static final long serialVersionUID = 2261086748874802175L;
+
+        private final int statusCode;
+        private final String body;
+        private final Headers headers;
+        private final String errorCode;
+        private final String errorMessage;
+
+        public ApiException(int statusCode, String body, Headers headers) {
+            super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
+                    body, headers));
+            this.statusCode = statusCode;
+            this.body = body;
+            this.headers = headers;
+
+            if (body != null && !body.isEmpty()) {
+                JsonElement code;
+                JsonElement message;
+
+                try {
+                    JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
+                    code = jsonObject.get("code");
+                    message = jsonObject.get("message");
+                } catch (JsonSyntaxException ignored) {
+                    code = null;
+                    message = null;
+                }
+                this.errorCode = code == null ? null : code.getAsString();
+                this.errorMessage = message == null ? null : message.getAsString();
+            } else {
+                this.errorCode = null;
+                this.errorMessage = null;
+            }
+        }
+
+        /**
+         * 获取 HTTP 应答状态码
+         */
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        /**
+         * 获取 HTTP 应答包体内容
+         */
+        public String getBody() {
+            return body;
+        }
+
+        /**
+         * 获取 HTTP 应答 Header
+         */
+        public Headers getHeaders() {
+            return headers;
+        }
+
+        /**
+         * 获取 错误码 (错误应答中的 code 字段)
+         */
+        public String getErrorCode() {
+            return errorCode;
+        }
+
+        /**
+         * 获取 错误消息 (错误应答中的 message 字段)
+         */
+        public String getErrorMessage() {
+            return errorMessage;
+        }
+    }
+
+    public static class Notification {
+        @SerializedName("id")
+        private String id;
+        @SerializedName("create_time")
+        private String createTime;
+        @SerializedName("event_type")
+        private String eventType;
+        @SerializedName("resource_type")
+        private String resourceType;
+        @SerializedName("summary")
+        private String summary;
+        @SerializedName("resource")
+        private Resource resource;
+        private String plaintext;
+
+        public String getId() {
+            return id;
+        }
+
+        public String getCreateTime() {
+            return createTime;
+        }
+
+        public String getEventType() {
+            return eventType;
+        }
+
+        public String getResourceType() {
+            return resourceType;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public Resource getResource() {
+            return resource;
+        }
+
+        /**
+         * 获取解密后的业务数据(JSON字符串,需要自行解析)
+         */
+        public String getPlaintext() {
+            return plaintext;
+        }
+
+        private void validate() {
+            if (resource == null) {
+                throw new IllegalArgumentException("Missing required field `resource` in notification");
+            }
+            resource.validate();
+        }
+
+        /**
+         * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。
+         * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public
+         * @param apiv3Key 商户APIv3 Key
+         */
+        private void decrypt(String apiv3Key) {
+            validate();
+
+            plaintext = aesAeadDecrypt(
+                    apiv3Key.getBytes(StandardCharsets.UTF_8),
+                    resource.associatedData.getBytes(StandardCharsets.UTF_8),
+                    resource.nonce.getBytes(StandardCharsets.UTF_8),
+                    Base64.getDecoder().decode(resource.ciphertext)
+            );
+        }
+
+        public static class Resource {
+            @SerializedName("algorithm")
+            private String algorithm;
+
+            @SerializedName("ciphertext")
+            private String ciphertext;
+
+            @SerializedName("associated_data")
+            private String associatedData;
+
+            @SerializedName("nonce")
+            private String nonce;
+
+            @SerializedName("original_type")
+            private String originalType;
+
+            public String getAlgorithm() {
+                return algorithm;
+            }
+
+            public String getCiphertext() {
+                return ciphertext;
+            }
+
+            public String getAssociatedData() {
+                return associatedData;
+            }
+
+            public String getNonce() {
+                return nonce;
+            }
+
+            public String getOriginalType() {
+                return originalType;
+            }
+
+            private void validate() {
+                if (algorithm == null || algorithm.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
+                            ".Resource");
+                }
+                if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
+                    throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
+                            "Notification.Resource", algorithm));
+                }
+
+                if (ciphertext == null || ciphertext.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
+                            ".Resource");
+                }
+
+                if (associatedData == null || associatedData.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `associatedData` in " +
+                            "Notification.Resource");
+                }
+
+                if (nonce == null || nonce.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
+                            ".Resource");
+                }
+
+                if (originalType == null || originalType.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `originalType` in " +
+                            "Notification.Resource");
+                }
+            }
+        }
+    }
+    /**
+     * 根据文件名获取对应的Content-Type
+     * @param fileName 文件名
+     * @return Content-Type字符串
+     */
+    public static String getContentTypeByFileName(String fileName) {
+        if (fileName == null || fileName.isEmpty()) {
+            return "application/octet-stream";
+        }
+
+        // 获取文件扩展名
+        String extension = "";
+        int lastDotIndex = fileName.lastIndexOf('.');
+        if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
+            extension = fileName.substring(lastDotIndex + 1).toLowerCase();
+        }
+
+        // 常见文件类型映射
+        Map<String, String> contentTypeMap = new HashMap<>();
+        // 图片类型
+        contentTypeMap.put("png", "image/png");
+        contentTypeMap.put("jpg", "image/jpeg");
+        contentTypeMap.put("jpeg", "image/jpeg");
+        contentTypeMap.put("gif", "image/gif");
+        contentTypeMap.put("bmp", "image/bmp");
+        contentTypeMap.put("webp", "image/webp");
+        contentTypeMap.put("svg", "image/svg+xml");
+        contentTypeMap.put("ico", "image/x-icon");
+
+        // 文档类型
+        contentTypeMap.put("pdf", "application/pdf");
+        contentTypeMap.put("doc", "application/msword");
+        contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+        contentTypeMap.put("xls", "application/vnd.ms-excel");
+        contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        contentTypeMap.put("ppt", "application/vnd.ms-powerpoint");
+        contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
+
+        // 文本类型
+        contentTypeMap.put("txt", "text/plain");
+        contentTypeMap.put("html", "text/html");
+        contentTypeMap.put("css", "text/css");
+        contentTypeMap.put("js", "application/javascript");
+        contentTypeMap.put("json", "application/json");
+        contentTypeMap.put("xml", "application/xml");
+        contentTypeMap.put("csv", "text/csv");
+
+        // 音视频类型
+        contentTypeMap.put("mp3", "audio/mpeg");
+        contentTypeMap.put("wav", "audio/wav");
+        contentTypeMap.put("mp4", "video/mp4");
+        contentTypeMap.put("avi", "video/x-msvideo");
+        contentTypeMap.put("mov", "video/quicktime");
+
+        // 压缩文件类型
+        contentTypeMap.put("zip", "application/zip");
+        contentTypeMap.put("rar", "application/x-rar-compressed");
+        contentTypeMap.put("7z", "application/x-7z-compressed");
+
+
+        return contentTypeMap.getOrDefault(extension, "application/octet-stream");
+    }
+}

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

@@ -0,0 +1,375 @@
+package shop.alien.dining.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.dining.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-dining/src/main/java/shop/alien/dining/util/WeChatPayUtil.java

@@ -0,0 +1,65 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/vo/DiningUserVo.java

@@ -0,0 +1,76 @@
+package shop.alien.dining.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-dining/src/main/java/shop/alien/dining/vo/TokenVerifyVo.java

@@ -0,0 +1,40 @@
+package shop.alien.dining.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;
+}

+ 26 - 0
alien-dining/src/main/resources/bootstrap-bug.yml

@@ -0,0 +1,26 @@
+spring:
+  application:
+    name: alien-dining
+
+  cloud:
+    nacos:
+      #注册中心
+      discovery:
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+
+      #配置中心
+      config:
+        enabled: true
+        refresh-enabled: true
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+        group: DEFAULT_GROUP
+        file-extension: yml
+        shared-configs:
+          - data-id: common.yml
+            group: DEFAULT_GROUP
+            refresh: true
+

+ 26 - 0
alien-dining/src/main/resources/bootstrap-dev.yml

@@ -0,0 +1,26 @@
+spring:
+  application:
+    name: alien-dining
+
+  cloud:
+    nacos:
+      #注册中心
+      discovery:
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+
+      #配置中心
+      config:
+        enabled: true
+        refresh-enabled: true
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+        group: DEFAULT_GROUP
+        file-extension: yml
+        shared-configs:
+          - data-id: common.yml
+            group: DEFAULT_GROUP
+            refresh: true
+

+ 26 - 0
alien-dining/src/main/resources/bootstrap-prod.yml

@@ -0,0 +1,26 @@
+spring:
+  application:
+    name: alien-dining
+
+  cloud:
+    nacos:
+      #注册中心
+      discovery:
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+
+      #配置中心
+      config:
+        enabled: true
+        refresh-enabled: true
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+        group: DEFAULT_GROUP
+        file-extension: yml
+        shared-configs:
+          - data-id: common.yml
+            group: DEFAULT_GROUP
+            refresh: true
+

+ 26 - 0
alien-dining/src/main/resources/bootstrap-test.yml

@@ -0,0 +1,26 @@
+spring:
+  application:
+    name: alien-dining
+
+  cloud:
+    nacos:
+      #注册中心
+      discovery:
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+
+      #配置中心
+      config:
+        enabled: true
+        refresh-enabled: true
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+        group: DEFAULT_GROUP
+        file-extension: yml
+        shared-configs:
+          - data-id: common.yml
+            group: DEFAULT_GROUP
+            refresh: true
+

+ 26 - 0
alien-dining/src/main/resources/bootstrap-uat.yml

@@ -0,0 +1,26 @@
+spring:
+  application:
+    name: alien-dining
+
+  cloud:
+    nacos:
+      #注册中心
+      discovery:
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+
+      #配置中心
+      config:
+        enabled: true
+        refresh-enabled: true
+        server-addr: 120.26.186.130:8848
+        username: dev
+        password: Alien123456
+        group: DEFAULT_GROUP
+        file-extension: yml
+        shared-configs:
+          - data-id: common.yml
+            group: DEFAULT_GROUP
+            refresh: true
+

+ 4 - 0
alien-dining/src/main/resources/bootstrap.yml

@@ -0,0 +1,4 @@
+spring:
+  profiles:
+    active: dev
+

+ 178 - 0
alien-dining/src/main/resources/logback-spring.xml

@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
+<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
+<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
+<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
+<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
+    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
+<configuration scan="false" scanPeriod="60 seconds" debug="true">
+<!--    <contextName>logback-spring</contextName>-->
+
+    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使"${}"来使用变量。 -->
+    <!-- 定义全局参数常量 -->
+    <property name="log.level" value="debug"/>
+    <property name="log.maxHistory" value="30"/><!-- 30表示30个 -->
+    <springProperty scope="context" name="logging.path" source="logging.path"  defaultValue="C:/project/ext/log"/>
+    <!--输出文件前缀-->
+    <property name="FILENAME" value="alien-dining"/>
+
+    <!--0. 日志格式和颜色渲染 -->
+    <!-- 彩色日志依赖的渲染类 -->
+    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
+    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+
+    <!-- 文件输出格式 -->
+    <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
+    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+
+    <!--1. 输出到控制台-->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>${log.level}</level>
+        </filter>
+        <encoder>
+            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
+            <!-- 设置字符集 -->
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!--2. 输出到文档-->
+    <!-- DEBUG 日志 -->
+    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 当前的日志文件存放路径 -->
+        <file>${logging.path}/DEBUG.log</file>
+        <!-- 日志滚动策略 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 历史日志文件的存放路径和名称 -->
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
+            <!-- 日志文件最大的保存历史 数量-->
+            <maxHistory>${log.maxHistory}</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+        <!--日志文件最大的大小-->
+        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
+        <!--            <MaxFileSize>10MB</MaxFileSize>-->
+        <!--        </triggeringPolicy>-->
+        <!-- 此日志文档只记录debug级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>DEBUG</level>
+            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- INFO 日志 -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${logging.path}/INFO.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_INFO.log.gz</fileNamePattern>
+            <maxHistory>${log.maxHistory}</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- WARN 日志 -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${logging.path}/WARN.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_WARN.log.gz</fileNamePattern>
+            <maxHistory>${log.maxHistory}</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${logging.path}/ERROR.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_ERROR.log.gz</fileNamePattern>
+            <maxHistory>${log.maxHistory}</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!--
+      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
+      以及指定<appender>。<logger>仅有一个name属性,
+      一个可选的level和一个可选的addtivity属性。
+      name:用来指定受此logger约束的某一个包或者具体的某一个类。
+      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
+         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
+         如果未设置此属性,那么当前logger将会继承上级的级别。
+      addtivity:是否向上级logger传递打印信息。默认是true。
+      <logger name="org.springframework.web" level="info"/>
+      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
+    -->
+
+    <!--
+      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
+      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
+      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
+      【logging.level.org.mybatis=debug logging.level.dao=debug】
+     -->
+    <!-- mybatis显示sql,修改此处扫描包名 -->
+
+
+    <!--
+      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
+      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
+      不能设置为INHERITED或者同义词NULL。默认是DEBUG
+      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
+    -->
+
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+
+    <!-- 4. 最终的策略 -->
+    <!-- 4.1 开发环境:打印控制台-->
+    <!--打印sql-->
+    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
+
+    <!--打印log-->
+    <root level="info">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="DEBUG_FILE"/>
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+    </root>
+
+    <!--   4.2 生产环境:输出到文档-->
+    <springProfile name="pro">
+        <root level="info">
+            <appender-ref ref="CONSOLE"/>
+            <appender-ref ref="DEBUG_FILE"/>
+            <appender-ref ref="INFO_FILE"/>
+            <appender-ref ref="ERROR_FILE"/>
+            <appender-ref ref="WARN_FILE"/>
+        </root>
+    </springProfile>
+</configuration>
+

+ 4 - 0
alien-entity/pom.xml

@@ -92,6 +92,10 @@
             <artifactId>alien-util</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/result/R.java

@@ -204,4 +204,7 @@ public class R<T> implements Serializable {
         return flag ? success(APPConstant.DEFAULT_SUCCESS_MESSAGE) : fail(APPConstant.DEFAULT_FAILURE_MESSAGE);
     }
 
+    public static R ok(String 菜单列表功能待实现) {
+        return R.ok("菜单列表功能待实现");
+    }
 }

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

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

+ 58 - 15
alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java

@@ -3,77 +3,92 @@ package shop.alien.entity.store;
 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.io.Serializable;
+import java.math.BigDecimal;
 import java.util.Date;
 
 /**
- * 用户
+ * 用户表
+ *
+ * @author system
+ * @since 2025-01-XX
  */
 @Data
 @JsonInclude
 @TableName("life_user")
+@ApiModel(value = "LifeUser对象", description = "用户表")
 public class LifeUser implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    @ApiModelProperty(value = "主键")
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
+    @ApiModelProperty(value = "用户姓名")
     @TableField("user_name")
     private String userName;
 
-    @TableField("real_name")
-    private String realName;
-
+    @ApiModelProperty(value = "用户性别")
     @TableField("user_sex")
     private String userSex;
 
+    @ApiModelProperty(value = "用户生日")
     @TableField(value = "user_brithday")
     @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
     private Date userBirthday;
 
+    @ApiModelProperty(value = "官方号码")
     @TableField("guanfang_phone")
     private String guanfangPhone;
 
+    @ApiModelProperty(value = "用户手机号")
     @TableField("user_phone")
     private String userPhone;
 
-    @TableField("id_card")
-    private String idCard;
-
+    @ApiModelProperty(value = "用户头像照片")
     @TableField("user_image")
     private String userImage;
 
+    @ApiModelProperty(value = "消费时间")
     @TableField("xiaofei_time")
     private Integer xiaofeiTime;
 
+    @ApiModelProperty(value = "消费金额")
     @TableField("xiaofei_amount")
-    private double xiaofeiAmount;
+    private BigDecimal xiaofeiAmount;
 
+    @ApiModelProperty(value = "差评时间")
     @TableField(value = "chaping_time")
     private Integer chapingTime;
 
+    @ApiModelProperty(value = "真实姓名")
+    @TableField("real_name")
+    private String realName;
+
+    @ApiModelProperty(value = "省")
     @TableField(value = "province")
     private String province;
 
+    @ApiModelProperty(value = "市")
     @TableField(value = "city")
     private String city;
 
+    @ApiModelProperty(value = "区")
     @TableField(value = "district")
     private String district;
 
+    @ApiModelProperty(value = "地址")
     @TableField(value = "address")
     private String address;
 
+    @ApiModelProperty(value = "个人简介")
     @TableField(value = "jianjie")
     private String jianjie;
 
-    @ApiModelProperty(value = "邀请码")
-    @TableField("invited_num")
-    private String invitedNum;
-
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic
@@ -89,7 +104,7 @@ public class LifeUser implements Serializable {
     private Integer createdUserId;
 
     @ApiModelProperty(value = "修改时间")
-    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date updatedTime;
 
@@ -97,6 +112,14 @@ public class LifeUser implements Serializable {
     @TableField("updated_user_id")
     private Integer updatedUserId;
 
+    @ApiModelProperty(value = "身份证")
+    @TableField("id_card")
+    private String idCard;
+
+    @ApiModelProperty(value = "邀请码")
+    @TableField("invited_num")
+    private String invitedNum;
+
     @ApiModelProperty(value = "注销标记, 0:未注销, 1:已注销")
     @TableField("logout_flag")
     private Integer logoutFlag;
@@ -106,7 +129,7 @@ public class LifeUser implements Serializable {
     private String logoutReason;
 
     @ApiModelProperty(value = "注销申请时间")
-    @TableField(value = "logout_time", fill = FieldFill.INSERT)
+    @TableField(value = "logout_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date logoutTime;
 
@@ -114,7 +137,11 @@ public class LifeUser implements Serializable {
     @TableField("clock_img_id")
     private Integer clockImgId;
 
-    @ApiModelProperty(value = "个人邀请码")
+    @ApiModelProperty(value = "是否同步")
+    @TableField("is_sync")
+    private Integer isSync;
+
+    @ApiModelProperty(value = "邀请码")
     @TableField("invite_code")
     private String inviteCode;
 
@@ -122,6 +149,22 @@ public class LifeUser implements Serializable {
     @TableField("bind_invite_code")
     private String bindInviteCode;
 
+    @ApiModelProperty(value = "用户积分")
+    @TableField("user_point")
+    private Integer userPoint;
+
+    @ApiModelProperty(value = "支付宝账户")
+    @TableField("alipay_account")
+    private String alipayAccount;
+
+    @ApiModelProperty(value = "账户余额")
+    @TableField("money")
+    private BigDecimal money;
+
+    @ApiModelProperty(value = "支付密码")
+    @TableField("pay_password")
+    private String payPassword;
+
     @ApiModelProperty(value = "是否被封禁(0-否  1-是)")
     @TableField("is_banned")
     private Integer isBanned;

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

@@ -0,0 +1,99 @@
+package shop.alien.entity.store;
+
+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("store_cart")
+@ApiModel(value = "StoreCart对象", description = "购物车表")
+public class StoreCart {
+
+    @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/StoreCouponUsage.java

@@ -0,0 +1,88 @@
+package shop.alien.entity.store;
+
+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("store_coupon_usage")
+@ApiModel(value = "StoreCouponUsage对象", description = "优惠券使用记录表")
+public class StoreCouponUsage {
+
+    @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;
+}

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

@@ -0,0 +1,141 @@
+package shop.alien.entity.store;
+
+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("store_order")
+@ApiModel(value = "StoreOrder对象", description = "订单表")
+public class StoreOrder {
+
+    @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/StoreOrderChangeLog.java

@@ -0,0 +1,120 @@
+package shop.alien.entity.store;
+
+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("store_order_change_log")
+@ApiModel(value = "StoreOrderChangeLog对象", description = "订单变更记录表")
+public class StoreOrderChangeLog {
+
+    @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:首次下单, 2:加餐, 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/StoreOrderDetail.java

@@ -0,0 +1,108 @@
+package shop.alien.entity.store;
+
+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("store_order_detail")
+@ApiModel(value = "StoreOrderDetail对象", description = "订单明细表")
+public class StoreOrderDetail {
+
+    @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/StoreOrderLock.java

@@ -0,0 +1,76 @@
+package shop.alien.entity.store;
+
+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("store_order_lock")
+@ApiModel(value = "StoreOrderLock对象", description = "订单锁定记录表")
+public class StoreOrderLock {
+
+    @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;
+}

+ 17 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java

@@ -37,14 +37,30 @@ public class StoreTable {
     @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:其他)")
+    @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;

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AddCartItemDTO.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Positive;
+
+/**
+ * 添加购物车商品DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AddCartItemDTO对象", description = "添加购物车商品")
+public class AddCartItemDTO {
+
+    @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;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AddDishDTO.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 加餐DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AddDishDTO对象", description = "加餐")
+public class AddDishDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    @NotNull(message = "订单ID不能为空")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "菜品ID", required = true)
+    @NotNull(message = "菜品ID不能为空")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "数量", required = true)
+    @NotNull(message = "数量不能为空")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

+ 37 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CartDTO.java

@@ -0,0 +1,37 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 购物车DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CartDTO对象", description = "购物车")
+public class CartDTO {
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "购物车商品列表")
+    private List<CartItemDTO> items;
+
+    @ApiModelProperty(value = "总金额")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "商品总数量")
+    private Integer totalQuantity;
+}

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

@@ -0,0 +1,51 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 购物车商品项DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CartItemDTO对象", description = "购物车商品项")
+public class CartItemDTO {
+
+    @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;
+}

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

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 换桌DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "ChangeTableDTO对象", description = "换桌")
+public class ChangeTableDTO {
+
+    @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;
+}

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

@@ -0,0 +1,49 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 创建订单DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CreateOrderDTO对象", description = "创建订单")
+public class CreateOrderDTO {
+
+    @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 java.math.BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费(由前端计算,基于门店餐具费单价 × 就餐人数)")
+    private java.math.BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠金额(由前端计算,如果使用优惠券则必传,不使用优惠券时传 0 或不传)")
+    private java.math.BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "实付金额(由前端计算,订单总金额 + 餐具费 - 优惠金额)")
+    private java.math.BigDecimal payAmount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注(限30字)")
+    private String remark;
+
+    @ApiModelProperty(value = "是否立即支付(0:否,创建订单但不支付; 1:是,创建订单并支付)")
+    private Integer immediatePay;
+}

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

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreInfo;
+
+import java.util.List;
+
+/**
+ * 店铺信息和首页展示美食价目表DTO
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "StoreInfoWithHomepageCuisinesDTO对象", description = "店铺信息和首页展示美食价目表")
+public class StoreInfoWithHomepageCuisinesDTO {
+
+    @ApiModelProperty(value = "店铺信息")
+    private StoreInfo storeInfo;
+
+    @ApiModelProperty(value = "首页展示的美食价目表列表")
+    private List<StoreCuisine> homepageCuisines;
+}

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/UpdateOrderCouponDTO.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 更新订单优惠券DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "UpdateOrderCouponDTO对象", description = "更新订单优惠券")
+public class UpdateOrderCouponDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    @NotNull(message = "订单ID不能为空")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "优惠券ID(可为空,表示不使用优惠券)")
+    private Integer couponId;
+}

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

@@ -0,0 +1,40 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 可领取优惠券VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AvailableCouponVO对象", description = "可领取优惠券")
+public class AvailableCouponVO {
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String name;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal nominalValue;
+
+    @ApiModelProperty(value = "使用标准(最低消费)")
+    private BigDecimal minimumSpendingAmount;
+
+    @ApiModelProperty(value = "到期时间")
+    private LocalDate endDate;
+
+    @ApiModelProperty(value = "是否已领取")
+    private Boolean isReceived;
+
+    @ApiModelProperty(value = "是否可用(库存>0且未过期)")
+    private Boolean isAvailable;
+}

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineComboItemVO.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 套餐包含的菜品项VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineComboItemVO对象", description = "套餐包含的菜品项")
+public class CuisineComboItemVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "数量")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "类别")
+    private String category;
+}

+ 79 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineDetailVO.java

@@ -0,0 +1,79 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 菜品详情VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineDetailVO对象", description = "菜品详情")
+public class CuisineDetailVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "商户id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String name;
+
+    @ApiModelProperty(value = "菜品图片列表")
+    private List<String> images;
+
+    @ApiModelProperty(value = "价格(总价)")
+    private BigDecimal totalPrice;
+
+    @ApiModelProperty(value = "月售数量")
+    private Integer monthlySales;
+
+    @ApiModelProperty(value = "标签(多个标签用逗号分隔)")
+    private String tags;
+
+    @ApiModelProperty(value = "菜品短评")
+    private String dishReview;
+
+    @ApiModelProperty(value = "菜品描述")
+    private String description;
+
+    @ApiModelProperty(value = "图文详情-文字")
+    private String detailContent;
+
+    @ApiModelProperty(value = "图文详情-图片")
+    private String imageContent;
+
+    @ApiModelProperty(value = "补充说明")
+    private String extraNote;
+
+    @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+    private String categoryIds;
+
+    @ApiModelProperty(value = "是否需要预约:0=否,1=是")
+    private Integer needReserve;
+
+    @ApiModelProperty(value = "预约规则")
+    private String reserveRule;
+
+    @ApiModelProperty(value = "适用人数")
+    private String peopleLimit;
+
+    @ApiModelProperty(value = "使用规则")
+    private String usageRule;
+
+    @ApiModelProperty(value = "购物车中的数量")
+    private Integer cartQuantity;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "套餐包含的菜品列表(仅套餐有)")
+    private List<CuisineComboItemVO> comboItems;
+}

+ 49 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineListVO.java

@@ -0,0 +1,49 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 菜品列表VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineListVO对象", description = "菜品列表")
+public class CuisineListVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String name;
+
+    @ApiModelProperty(value = "菜品图片(首张)")
+    private String firstImage;
+
+    @ApiModelProperty(value = "价格")
+    private BigDecimal price;
+
+    @ApiModelProperty(value = "短评")
+    private String shortComment;
+
+    @ApiModelProperty(value = "标签(多个标签用逗号分隔)")
+    private String tags;
+
+    @ApiModelProperty(value = "月售数量")
+    private Integer monthlySales;
+
+    @ApiModelProperty(value = "购物车中的数量")
+    private Integer cartQuantity;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "上下架状态:1-上架,2-下架")
+    private Integer shelfStatus;
+}

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

@@ -0,0 +1,31 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 点餐页面信息VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "DiningPageInfoVO对象", description = "点餐页面信息")
+public class DiningPageInfoVO {
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer tableId;
+}

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

@@ -0,0 +1,50 @@
+package shop.alien.entity.store.vo;
+
+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;
+
+/**
+ * 订单变更记录批次VO(按批次分组)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderChangeLogBatchVO对象", description = "订单变更记录批次")
+public class OrderChangeLogBatchVO {
+
+    @ApiModelProperty(value = "批次号")
+    private String batchNo;
+
+    @ApiModelProperty(value = "操作类型(1:首次下单, 2:加餐, 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 Integer totalQuantityChange;
+
+    @ApiModelProperty(value = "该批次金额变化总和")
+    private BigDecimal totalAmountChange;
+
+    @ApiModelProperty(value = "该批次商品数量")
+    private Integer itemCount;
+
+    @ApiModelProperty(value = "该批次的商品明细列表")
+    private List<OrderChangeLogItemVO> items;
+}

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

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单变更记录商品项VO
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderChangeLogItemVO对象", description = "订单变更记录商品项")
+public class OrderChangeLogItemVO {
+
+    @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;
+}

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderConfirmVO.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 订单确认页面VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "OrderConfirmVO对象", description = "订单确认页面")
+public class OrderConfirmVO {
+
+    @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<CartItemDTO> 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/OrderCuisineItemVO.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单菜品项VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "OrderCuisineItemVO对象", description = "订单菜品项VO")
+public class OrderCuisineItemVO {
+
+    @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;
+}

+ 77 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderDetailWithChangeLogVO.java

@@ -0,0 +1,77 @@
+package shop.alien.entity.store.vo;
+
+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;
+
+/**
+ * 订单详情VO(包含订单基本信息和按批次分组的变更记录)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderDetailWithChangeLogVO对象", description = "订单详情(包含变更记录)")
+public class OrderDetailWithChangeLogVO {
+
+    @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<OrderChangeLogBatchVO> changeLogBatches;
+}

Some files were not shown because too many files changed in this diff