Bladeren bron

前端及后端功能

wangwenju 2 weken geleden
bovenliggende
commit
5eded6c07c
46 gewijzigde bestanden met toevoegingen van 3227 en 11 verwijderingen
  1. 5 0
      .gitignore
  2. 91 0
      oederFoodVue/HOW_TO_CHANGE_QRCODE.md
  3. 153 0
      oederFoodVue/PAYMENT_GUIDE.md
  4. 32 0
      oederFoodVue/create-order-tables.sql
  5. 756 0
      oederFoodVue/css/style.css
  6. 20 0
      oederFoodVue/images/README.md
  7. 98 0
      oederFoodVue/images/generate-placeholder.html
  8. BIN
      oederFoodVue/images/payment-qrcode.jpg
  9. 122 0
      oederFoodVue/index.html
  10. 598 0
      oederFoodVue/js/app.js
  11. 238 0
      oederFoodVue/payment-simulator.html
  12. 107 2
      pom.xml
  13. 2 0
      src/main/java/com/we/OrderFoodApplication.java
  14. 133 0
      src/main/java/com/we/controller/DishController.java
  15. 54 0
      src/main/java/com/we/controller/OrderController.java
  16. 58 0
      src/main/java/com/we/controller/PaymentController.java
  17. 103 0
      src/main/java/com/we/domain/Dish.java
  18. 51 0
      src/main/java/com/we/domain/DishCategory.java
  19. 47 0
      src/main/java/com/we/domain/Flaver.java
  20. 28 0
      src/main/java/com/we/domain/Order.java
  21. 25 0
      src/main/java/com/we/domain/OrderDetail.java
  22. 20 0
      src/main/java/com/we/dto/OrderDTO.java
  23. 32 0
      src/main/java/com/we/dto/Result.java
  24. 18 0
      src/main/java/com/we/mapper/DishCategoryMapper.java
  25. 18 0
      src/main/java/com/we/mapper/DishMapper.java
  26. 18 0
      src/main/java/com/we/mapper/FlaverMapper.java
  27. 9 0
      src/main/java/com/we/mapper/OrderDetailMapper.java
  28. 9 0
      src/main/java/com/we/mapper/OrderMapper.java
  29. 13 0
      src/main/java/com/we/service/DishCategoryService.java
  30. 13 0
      src/main/java/com/we/service/DishService.java
  31. 13 0
      src/main/java/com/we/service/FlaverService.java
  32. 7 0
      src/main/java/com/we/service/OrderDetailService.java
  33. 9 0
      src/main/java/com/we/service/OrderService.java
  34. 22 0
      src/main/java/com/we/service/impl/DishCategoryServiceImpl.java
  35. 22 0
      src/main/java/com/we/service/impl/DishServiceImpl.java
  36. 22 0
      src/main/java/com/we/service/impl/FlaverServiceImpl.java
  37. 11 0
      src/main/java/com/we/service/impl/OrderDetailServiceImpl.java
  38. 90 0
      src/main/java/com/we/service/impl/OrderServiceImpl.java
  39. 40 0
      src/main/java/com/we/vo/CategoryVo.java
  40. 32 0
      src/main/java/com/we/vo/DishVO.java
  41. 0 3
      src/main/resources/application.properties
  42. 17 0
      src/main/resources/application.yml
  43. 19 0
      src/main/resources/com/we/mapper/DishCategoryMapper.xml
  44. 33 0
      src/main/resources/com/we/mapper/DishMapper.xml
  45. 19 0
      src/main/resources/com/we/mapper/FlaverMapper.xml
  46. 0 6
      src/main/resources/static/index.html

+ 5 - 0
.gitignore

@@ -31,3 +31,8 @@ build/
 
 ### VS Code ###
 .vscode/
+
+
+# 忽略IDE临时文件
+.idea/
+target/

+ 91 - 0
oederFoodVue/HOW_TO_CHANGE_QRCODE.md

@@ -0,0 +1,91 @@
+# 如何更换支付二维码图片
+
+## 方法一:直接替换图片文件(推荐)
+
+1. 准备你的支付二维码图片(微信/支付宝收款码)
+2. 将图片重命名为 `payment-qrcode.png`
+3. 放入 `oederFoodVue/images/` 文件夹
+4. 刷新页面即可看到新的二维码
+
+## 方法二:修改配置文件
+
+如果你的图片名称不同,可以修改配置:
+
+1. 打开 `oederFoodVue/js/app.js`
+2. 找到第5行的配置:
+   ```javascript
+   const PAYMENT_QRCODE_IMAGE = 'images/payment-qrcode.png';
+   ```
+3. 修改为你的图片路径:
+   ```javascript
+   const PAYMENT_QRCODE_IMAGE = 'images/你的图片名称.jpg';
+   ```
+4. 保存文件并刷新页面
+
+## 图片要求
+
+- **格式**:PNG、JPG、JPEG、GIF 均可
+- **尺寸**:建议 200x200 像素或更大(会自动缩放到200x200)
+- **位置**:必须放在 `oederFoodVue/images/` 文件夹中
+
+## 生成占位图片
+
+如果暂时没有真实二维码,可以使用占位图:
+
+1. 打开 `oederFoodVue/images/generate-placeholder.html`
+2. 点击"下载为 payment-qrcode.png"按钮
+3. 将下载的图片放入 `images` 文件夹
+
+## 使用在线二维码
+
+如果图片在网上,可以直接使用URL:
+
+```javascript
+const PAYMENT_QRCODE_IMAGE = 'https://example.com/your-qrcode.png';
+```
+
+## 故障排除
+
+### 图片不显示
+- 检查图片路径是否正确
+- 检查图片文件是否存在
+- 检查浏览器控制台是否有错误
+- 如果图片加载失败,会显示绿色占位图
+
+### 图片变形
+- 确保图片是正方形(如200x200)
+- CSS已设置 `object-fit: contain` 保持比例
+
+### 更换后不生效
+- 清除浏览器缓存(Ctrl+F5 强制刷新)
+- 检查图片名称是否正确
+- 检查配置文件是否保存
+
+## 示例
+
+### 使用微信收款码
+```javascript
+const PAYMENT_QRCODE_IMAGE = 'images/wechat-pay.png';
+```
+
+### 使用支付宝收款码
+```javascript
+const PAYMENT_QRCODE_IMAGE = 'images/alipay.png';
+```
+
+### 使用多个二维码(根据金额切换)
+可以在 `showPaymentQRCode()` 函数中添加逻辑:
+```javascript
+function showPaymentQRCode(orderData) {
+    // ...
+    let qrcodePath = PAYMENT_QRCODE_IMAGE;
+    
+    // 根据金额选择不同的二维码
+    if (orderData.finalAmount > 100) {
+        qrcodePath = 'images/payment-large.png';
+    }
+    
+    qrcodeImage.src = qrcodePath;
+    // ...
+}
+```

+ 153 - 0
oederFoodVue/PAYMENT_GUIDE.md

@@ -0,0 +1,153 @@
+# 支付功能使用指南
+
+## 功能说明
+
+实现了真实的支付验证流程:
+1. 用户点击"去结算"按钮
+2. 显示支付二维码
+3. 用户扫描二维码进行支付
+4. 前端轮询后端验证支付状态
+5. 支付成功后创建订单
+6. 支付失败则提示用户
+
+## 支付流程
+
+```
+用户下单 → 显示二维码 → 用户扫码支付 → 轮询验证支付状态 → 支付成功 → 创建订单 → 清空购物车
+                                                    ↓
+                                                支付失败 → 提示用户
+```
+
+## 测试方法
+
+### 方法一:使用支付模拟器(推荐)
+
+1. 启动后端服务:
+   ```bash
+   mvn spring-boot:run
+   ```
+
+2. 打开主页面:
+   ```
+   oederFoodVue/index.html
+   ```
+
+3. 添加菜品到购物车(满¥20起送)
+
+4. 点击"去结算"按钮,会显示支付二维码
+
+5. 复制二维码中的支付链接,在新标签页打开 `payment-simulator.html`,并在URL中添加支付信息:
+   ```
+   payment-simulator.html?paymentId=PAY1234567890&amount=25.00
+   ```
+
+6. 在模拟器页面点击"支付成功"或"支付失败"按钮
+
+7. 主页面会自动检测到支付状态并创建订单
+
+### 方法二:使用API直接模拟
+
+使用Postman或curl调用模拟支付接口:
+
+```bash
+# 模拟支付成功
+curl -X POST "http://localhost:8086/api/payment/simulate?paymentId=PAY1234567890&status=success"
+
+# 模拟支付失败
+curl -X POST "http://localhost:8086/api/payment/simulate?paymentId=PAY1234567890&status=failed"
+```
+
+## API接口
+
+### 1. 验证支付状态
+- **接口**: `GET /api/payment/verify`
+- **参数**: `paymentId` - 支付单号
+- **返回**:
+  ```json
+  {
+    "code": 200,
+    "data": {
+      "paymentId": "PAY1234567890",
+      "status": "success",
+      "paid": true,
+      "failed": false
+    }
+  }
+  ```
+
+### 2. 模拟支付
+- **接口**: `POST /api/payment/simulate`
+- **参数**: 
+  - `paymentId` - 支付单号
+  - `status` - 支付状态(success/failed)
+- **返回**:
+  ```json
+  {
+    "code": 200,
+    "data": "支付模拟成功"
+  }
+  ```
+
+### 3. 创建订单
+- **接口**: `POST /api/orders`
+- **请求体**:
+  ```json
+  {
+    "items": [
+      {"dishId": "1", "quantity": 2}
+    ],
+    "totalAmount": 20.00,
+    "deliveryFee": 5.00,
+    "finalAmount": 25.00,
+    "remark": ""
+  }
+  ```
+
+## 前端实现细节
+
+### 轮询机制
+- 每2秒轮询一次支付状态
+- 最多轮询60次(2分钟)
+- 超时后自动停止并提示用户
+
+### 支付状态
+- `pending`: 待支付(继续轮询)
+- `success`: 支付成功(创建订单)
+- `failed`: 支付失败(提示用户)
+
+### 关键函数
+- `checkout()`: 准备订单数据,显示二维码
+- `showPaymentQRCode()`: 生成二维码,开始轮询
+- `startPaymentPolling()`: 轮询支付状态
+- `createOrderAfterPayment()`: 支付成功后创建订单
+- `showPaymentSuccess()`: 显示支付成功
+- `showPaymentFailure()`: 显示支付失败
+
+## 生产环境集成
+
+在实际生产环境中,需要:
+
+1. **集成真实支付平台**(微信支付/支付宝)
+   - 替换二维码生成逻辑
+   - 使用支付平台提供的支付链接
+
+2. **实现支付回调接口**
+   - 接收支付平台的异步通知
+   - 验证签名确保安全性
+   - 更新支付状态
+
+3. **使用Redis存储支付状态**
+   - 替换内存存储(ConcurrentHashMap)
+   - 设置过期时间(如30分钟)
+
+4. **添加安全措施**
+   - 验证支付金额
+   - 防止重复支付
+   - 记录支付日志
+
+## 注意事项
+
+1. 当前实现使用内存存储支付状态,重启后端会丢失
+2. 支付单号使用时间戳生成,实际应使用更安全的算法
+3. 二维码链接指向本地,实际应指向支付平台
+4. 未实现支付超时自动取消订单功能

+ 32 - 0
oederFoodVue/create-order-tables.sql

@@ -0,0 +1,32 @@
+-- 创建订单相关表
+
+USE orderFood;
+
+-- 订单表
+CREATE TABLE IF NOT EXISTS `orders` (
+  `id` BIGINT NOT NULL PRIMARY KEY COMMENT '订单ID',
+  `order_number` VARCHAR(50) NOT NULL UNIQUE COMMENT '订单号',
+  `total_amount` DECIMAL(10,2) NOT NULL COMMENT '商品总价',
+  `delivery_fee` DECIMAL(10,2) NOT NULL COMMENT '配送费',
+  `final_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总额',
+  `status` INT NOT NULL DEFAULT 0 COMMENT '订单状态 0-待支付 1-已支付 2-已取消',
+  `remark` VARCHAR(500) COMMENT '备注',
+  `create_time` DATETIME NOT NULL COMMENT '创建时间',
+  `update_time` DATETIME NOT NULL COMMENT '更新时间',
+  INDEX `idx_order_number` (`order_number`),
+  INDEX `idx_status` (`status`),
+  INDEX `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
+
+-- 订单详情表
+CREATE TABLE IF NOT EXISTS `order_detail` (
+  `id` BIGINT NOT NULL PRIMARY KEY COMMENT '详情ID',
+  `order_id` BIGINT NOT NULL COMMENT '订单ID',
+  `dish_id` BIGINT NOT NULL COMMENT '菜品ID',
+  `dish_name` VARCHAR(100) NOT NULL COMMENT '菜品名称',
+  `price` DECIMAL(10,2) NOT NULL COMMENT '单价',
+  `quantity` INT NOT NULL COMMENT '数量',
+  `subtotal` DECIMAL(10,2) NOT NULL COMMENT '小计',
+  INDEX `idx_order_id` (`order_id`),
+  INDEX `idx_dish_id` (`dish_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情表';

+ 756 - 0
oederFoodVue/css/style.css

@@ -0,0 +1,756 @@
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+    background-color: #f5f5f5;
+    padding-bottom: 65px;
+}
+
+/* 顶部商家信息 */
+.header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 15px;
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.shop-name {
+    font-size: 20px;
+    margin-bottom: 8px;
+}
+
+.shop-tags {
+    display: flex;
+    gap: 10px;
+}
+
+.tag {
+    background: rgba(255,255,255,0.2);
+    padding: 4px 10px;
+    border-radius: 12px;
+    font-size: 12px;
+}
+
+/* 主体布局 */
+.main-container {
+    display: flex;
+    height: calc(100vh - 165px);
+    overflow: hidden;
+}
+
+/* 左侧分类 */
+.category-sidebar {
+    width: 90px;
+    background: #f8f8f8;
+    overflow-y: auto;
+    border-right: 1px solid #e5e5e5;
+}
+
+.category-item {
+    padding: 20px 10px;
+    text-align: center;
+    font-size: 14px;
+    color: #666;
+    cursor: pointer;
+    transition: all 0.3s;
+    border-left: 3px solid transparent;
+}
+
+.category-item:hover {
+    background: #fff;
+}
+
+.category-item.active {
+    background: #fff;
+    color: #667eea;
+    font-weight: bold;
+    border-left-color: #667eea;
+}
+
+/* 右侧菜品列表 */
+.dish-content {
+    flex: 1;
+    overflow-y: auto;
+    overflow-x: hidden;
+    scroll-behavior: smooth;
+    position: relative;
+
+    padding: 15px;
+}
+
+.category-section {
+    margin-bottom: 20px;
+}
+
+.category-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 15px;
+    padding-left: 10px;
+    border-left: 4px solid #667eea;
+    transition: all 0.3s;
+    position: sticky;
+    top: 0;
+    background: #f5f5f5;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    z-index: 10;
+}
+
+.dish-list {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.dish-item {
+    display: flex;
+    background: white;
+    border-radius: 12px;
+    padding: 12px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+    transition: transform 0.2s;
+    cursor: pointer;
+}
+
+.dish-item:active {
+    transform: scale(0.98);
+}
+
+.dish-img {
+    width: 100px;
+    height: 100px;
+    border-radius: 8px;
+    object-fit: cover;
+    margin-right: 12px;
+}
+
+.dish-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+}
+
+.dish-name {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 5px;
+}
+
+.dish-desc {
+    font-size: 12px;
+    color: #999;
+    margin-bottom: 5px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+}
+
+.dish-tags {
+    display: flex;
+    gap: 5px;
+    margin-bottom: 8px;
+}
+
+.dish-tag {
+    background: #fff3e0;
+    color: #ff9800;
+    padding: 2px 8px;
+    border-radius: 4px;
+    font-size: 11px;
+}
+
+.dish-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.dish-price {
+    color: #ff6b6b;
+    font-size: 18px;
+    font-weight: bold;
+}
+
+.dish-sales {
+    font-size: 12px;
+    color: #999;
+    margin-left: 10px;
+}
+
+.dish-controls {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.control-btn {
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    border: none;
+    background: #667eea;
+    color: white;
+    font-size: 18px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+.control-btn:active {
+    transform: scale(0.9);
+}
+
+.control-btn.minus {
+    background: #e0e0e0;
+    color: #666;
+}
+
+.dish-count {
+    min-width: 20px;
+    text-align: center;
+    font-weight: bold;
+}
+
+/* 底部购物车 */
+.cart-bar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 65px;
+    background: #2c2c2c;
+    display: flex;
+    align-items: center;
+    padding: 0 15px;
+    box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
+    z-index: 200;
+    gap: 10px;
+}
+
+.cart-icon-wrapper {
+    position: relative;
+    flex-shrink: 0;
+}
+
+.cart-icon {
+    width: 45px;
+    height: 45px;
+    background: #667eea;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 20px;
+    cursor: pointer;
+    transition: transform 0.2s;
+    position: relative;
+}
+
+.cart-icon:active {
+    transform: scale(0.95);
+}
+
+.cart-count {
+    position: absolute;
+    top: -3px;
+    right: -3px;
+    background: #ff6b6b;
+    color: white;
+    border-radius: 9px;
+    padding: 2px 5px;
+    font-size: 11px;
+    min-width: 18px;
+    text-align: center;
+    font-weight: bold;
+}
+
+.cart-info {
+    flex: 1;
+    color: white;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+.cart-price {
+    font-size: 20px;
+    font-weight: bold;
+    color: #fff;
+    line-height: 1.2;
+    margin-bottom: 2px;
+}
+
+.price-label {
+    font-size: 14px;
+}
+
+.delivery-fee {
+    font-size: 11px;
+    color: #999;
+    line-height: 1.2;
+}
+
+.checkout-btn {
+    background: #4caf50;
+    color: white;
+    border: none;
+    padding: 10px 18px;
+    border-radius: 18px;
+    font-size: 13px;
+    font-weight: bold;
+    cursor: pointer;
+    transition: all 0.3s;
+    white-space: nowrap;
+    flex-shrink: 0;
+}
+
+.checkout-btn:disabled {
+    background: #555;
+    cursor: not-allowed;
+}
+
+.checkout-btn:not(:disabled):active {
+    transform: scale(0.95);
+}
+
+/* 购物车详情弹窗 */
+.cart-detail-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 65px;
+    background: rgba(0,0,0,0.5);
+    z-index: 198;
+    display: none;
+    backdrop-filter: blur(2px);
+}
+
+.cart-detail-mask.show {
+    display: block;
+    animation: fadeIn 0.3s ease;
+}
+
+.cart-detail {
+    position: fixed;
+    bottom: 65px;
+    left: 0;
+    right: 0;
+    max-height: 50vh;
+    background: white;
+    border-radius: 20px 20px 0 0;
+    z-index: 199;
+    transform: translateY(0);
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
+    opacity: 0;
+    visibility: hidden;
+    pointer-events: none;
+}
+
+.cart-detail.show {
+    opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
+}
+
+.cart-detail-header {
+    display: flex;
+    justify-content: space-between;
+    padding: 15px 20px;
+    border-bottom: 1px solid #f0f0f0;
+    font-weight: bold;
+}
+
+.clear-cart {
+    color: #999;
+    cursor: pointer;
+}
+
+.cart-detail-list {
+    max-height: calc(50vh - 60px);
+    overflow-y: auto;
+    padding: 10px 20px;
+}
+
+.cart-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 15px 0;
+    border-bottom: 1px solid #f5f5f5;
+}
+
+.cart-item-name {
+    flex: 1;
+    font-size: 14px;
+}
+
+.cart-item-price {
+    color: #ff6b6b;
+    margin: 0 15px;
+}
+
+/* 菜品详情弹窗 */
+.dish-detail-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0,0,0,0.7);
+    z-index: 400;
+    display: none;
+}
+
+.dish-detail-mask.show {
+    display: block;
+}
+
+.dish-detail {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) scale(0);
+    width: 90%;
+    max-width: 500px;
+    background: white;
+    border-radius: 16px;
+    z-index: 401;
+    transition: transform 0.3s;
+}
+
+.dish-detail.show {
+    transform: translate(-50%, -50%) scale(1);
+}
+
+.dish-detail-content {
+    padding: 20px;
+}
+
+.dish-detail-img {
+    width: 100%;
+    height: 200px;
+    object-fit: cover;
+    border-radius: 12px;
+    margin-bottom: 15px;
+}
+
+.dish-detail-name {
+    font-size: 20px;
+    margin-bottom: 10px;
+}
+
+.dish-detail-desc {
+    color: #666;
+    font-size: 14px;
+    line-height: 1.6;
+    margin-bottom: 15px;
+}
+
+.dish-detail-tags {
+    display: flex;
+    gap: 8px;
+    margin-bottom: 20px;
+}
+
+.dish-detail-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.dish-detail-price {
+    color: #ff6b6b;
+    font-size: 24px;
+    font-weight: bold;
+}
+
+.add-to-cart-btn {
+    background: #667eea;
+    color: white;
+    border: none;
+    padding: 12px 30px;
+    border-radius: 25px;
+    font-size: 16px;
+    cursor: pointer;
+}
+
+.close-detail {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    width: 30px;
+    height: 30px;
+    background: rgba(0,0,0,0.5);
+    color: white;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 24px;
+    cursor: pointer;
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+    width: 6px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #ccc;
+    border-radius: 3px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f5f5f5;
+}
+
+/* 支付二维码弹窗 */
+.payment-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0,0,0,0.7);
+    z-index: 500;
+    display: none;
+    backdrop-filter: blur(4px);
+}
+
+.payment-mask.show {
+    display: block;
+    animation: fadeIn 0.3s ease;
+}
+
+.payment-modal {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) scale(0);
+    width: 90%;
+    max-width: 400px;
+    background: white;
+    border-radius: 16px;
+    z-index: 501;
+    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+}
+
+.payment-modal.show {
+    transform: translate(-50%, -50%) scale(1);
+}
+
+.payment-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 20px;
+    border-bottom: 1px solid #f0f0f0;
+}
+
+.payment-header h3 {
+    font-size: 20px;
+    color: #333;
+    margin: 0;
+}
+
+.close-payment {
+    width: 32px;
+    height: 32px;
+    background: #f5f5f5;
+    color: #666;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 24px;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.close-payment:hover {
+    background: #e0e0e0;
+    color: #333;
+}
+
+.payment-content {
+    padding: 20px;
+}
+
+.payment-info {
+    margin-bottom: 20px;
+}
+
+.info-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 0;
+    font-size: 14px;
+}
+
+.info-row .label {
+    color: #666;
+}
+
+.info-row .value {
+    color: #333;
+    font-weight: 500;
+}
+
+.info-row .amount {
+    color: #ff6b6b;
+    font-size: 20px;
+    font-weight: bold;
+}
+
+.qrcode-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 20px;
+    background: #f8f8f8;
+    border-radius: 12px;
+    margin-bottom: 20px;
+}
+
+.qrcode-image {
+    width: 200px;
+    height: 200px;
+    padding: 15px;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+    object-fit: contain;
+}
+
+.qrcode-tip {
+    margin-top: 15px;
+    font-size: 13px;
+    color: #666;
+    text-align: center;
+}
+
+.payment-status {
+    text-align: center;
+}
+
+.status-checking {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+}
+
+.loading-spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #f0f0f0;
+    border-top-color: #667eea;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    to { transform: rotate(360deg); }
+}
+
+.status-checking p {
+    color: #666;
+    font-size: 14px;
+    margin: 0;
+}
+
+.status-success {
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+}
+
+.status-success.show {
+    display: flex;
+}
+
+.success-icon {
+    width: 60px;
+    height: 60px;
+    background: #4caf50;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32px;
+    color: white;
+    animation: scaleIn 0.5s ease;
+}
+
+@keyframes scaleIn {
+    0% { transform: scale(0); }
+    50% { transform: scale(1.1); }
+    100% { transform: scale(1); }
+}
+
+.status-success p {
+    color: #4caf50;
+    font-size: 16px;
+    font-weight: bold;
+    margin: 0;
+}
+
+/* 支付失败样式 */
+.status-failure {
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+}
+
+.status-failure.show {
+    display: flex;
+}
+
+.failure-icon {
+    width: 60px;
+    height: 60px;
+    background: #f44336;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32px;
+    color: white;
+    animation: shakeIn 0.5s ease;
+}
+
+@keyframes shakeIn {
+    0%, 100% { transform: translateX(0); }
+    25% { transform: translateX(-10px); }
+    75% { transform: translateX(10px); }
+}
+
+.status-failure p {
+    color: #f44336;
+    font-size: 16px;
+    font-weight: bold;
+    margin: 0;
+    text-align: center;
+}

+ 20 - 0
oederFoodVue/images/README.md

@@ -0,0 +1,20 @@
+# 支付二维码图片说明
+
+## 图片位置
+将你的支付二维码图片放在此文件夹中,命名为:`payment-qrcode.png`
+
+## 图片要求
+- **格式**:PNG、JPG、JPEG 均可
+- **尺寸**:建议 200x200 像素或更大(会自动缩放)
+- **内容**:微信支付或支付宝收款码
+
+## 更换步骤
+1. 准备好你的支付二维码图片
+2. 将图片重命名为 `payment-qrcode.png`
+3. 替换此文件夹中的图片
+4. 刷新页面即可看到新的二维码
+
+## 备注
+- 如果图片加载失败,会显示占位图
+- 支持微信支付、支付宝等任何二维码图片
+- 图片会自动居中显示,保持比例

+ 98 - 0
oederFoodVue/images/generate-placeholder.html

@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>生成占位二维码</title>
+    <style>
+        body {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            background: #f5f5f5;
+            font-family: Arial, sans-serif;
+        }
+        .container {
+            text-align: center;
+            background: white;
+            padding: 40px;
+            border-radius: 10px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }
+        canvas {
+            border: 1px solid #ddd;
+            margin: 20px 0;
+        }
+        button {
+            padding: 10px 20px;
+            background: #4caf50;
+            color: white;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 16px;
+        }
+        button:hover {
+            background: #45a049;
+        }
+        .note {
+            margin-top: 20px;
+            color: #666;
+            font-size: 14px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h2>生成占位二维码图片</h2>
+        <canvas id="canvas" width="200" height="200"></canvas>
+        <br>
+        <button onclick="downloadImage()">下载为 payment-qrcode.png</button>
+        <div class="note">
+            <p>这是一个占位图片,请替换为你的真实支付二维码</p>
+            <p>下载后放在 images 文件夹中即可</p>
+        </div>
+    </div>
+
+    <script>
+        // 绘制占位二维码
+        const canvas = document.getElementById('canvas');
+        const ctx = canvas.getContext('2d');
+        
+        // 背景
+        ctx.fillStyle = '#ffffff';
+        ctx.fillRect(0, 0, 200, 200);
+        
+        // 边框
+        ctx.strokeStyle = '#333';
+        ctx.lineWidth = 2;
+        ctx.strokeRect(10, 10, 180, 180);
+        
+        // 文字
+        ctx.fillStyle = '#333';
+        ctx.font = 'bold 16px Arial';
+        ctx.textAlign = 'center';
+        ctx.fillText('支付二维码', 100, 90);
+        ctx.font = '12px Arial';
+        ctx.fillText('请替换为真实二维码', 100, 110);
+        
+        // 简单的二维码图案
+        ctx.fillStyle = '#333';
+        for (let i = 0; i < 8; i++) {
+            for (let j = 0; j < 8; j++) {
+                if ((i + j) % 2 === 0) {
+                    ctx.fillRect(50 + i * 12, 120 + j * 8, 10, 6);
+                }
+            }
+        }
+        
+        function downloadImage() {
+            const link = document.createElement('a');
+            link.download = 'payment-qrcode.png';
+            link.href = canvas.toDataURL('image/png');
+            link.click();
+        }
+    </script>
+</body>
+</html>

BIN
oederFoodVue/images/payment-qrcode.jpg


+ 122 - 0
oederFoodVue/index.html

@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>商家餐品</title>
+    <link rel="stylesheet" href="css/style.css">
+</head>
+<body>
+    <!-- 顶部商家信息 -->
+    <header class="header">
+        <div class="shop-info">
+            <h1 class="shop-name">美味餐厅</h1>
+            <div class="shop-tags">
+                <span class="tag">月售1000+</span>
+                <span class="tag">配送费¥5</span>
+            </div>
+        </div>
+    </header>
+
+    <!-- 主体内容区 -->
+    <div class="main-container">
+        <!-- 左侧分类导航 -->
+        <aside class="category-sidebar">
+            <div class="category-list" id="categoryList">
+                <!-- 动态加载分类 -->
+            </div>
+        </aside>
+
+        <!-- 右侧菜品列表 -->
+        <main class="dish-content">
+            <div class="dish-container" id="dishContainer">
+                <!-- 动态加载菜品 -->
+            </div>
+        </main>
+    </div>
+
+    <!-- 底部购物车 -->
+    <footer class="cart-bar">
+        <div class="cart-icon-wrapper" id="cartIcon">
+            <div class="cart-icon">
+                🛒
+                <span class="cart-count" id="cartCount">0</span>
+            </div>
+            <div class="cart-tip" id="cartTip">点击查看已选商品</div>
+        </div>
+        <div class="cart-info">
+            <div class="cart-price">
+                <span class="price-label">¥</span>
+                <span class="price-value" id="totalPrice">0</span>
+            </div>
+            <div class="delivery-fee">配送费¥5</div>
+        </div>
+        <button class="checkout-btn" id="checkoutBtn" disabled>
+            还差¥20起送
+        </button>
+    </footer>
+
+    <!-- 购物车详情弹窗 -->
+    <div class="cart-detail-mask" id="cartMask"></div>
+    <div class="cart-detail" id="cartDetail">
+        <div class="cart-detail-header">
+            <span>已选商品</span>
+            <span class="clear-cart" id="clearCart">清空</span>
+        </div>
+        <div class="cart-detail-list" id="cartDetailList">
+            <!-- 动态加载购物车商品 -->
+        </div>
+    </div>
+
+    <!-- 菜品详情弹窗 -->
+    <div class="dish-detail-mask" id="dishDetailMask"></div>
+    <div class="dish-detail" id="dishDetail">
+        <div class="dish-detail-content">
+            <img class="dish-detail-img" id="dishDetailImg" src="" alt="">
+            <div class="dish-detail-info">
+                <h3 class="dish-detail-name" id="dishDetailName"></h3>
+                <p class="dish-detail-desc" id="dishDetailDesc"></p>
+                <div class="dish-detail-tags" id="dishDetailTags"></div>
+                <div class="dish-detail-footer">
+                    <span class="dish-detail-price">¥<span id="dishDetailPrice"></span></span>
+                    <button class="add-to-cart-btn" id="addToCartBtn">加入购物车</button>
+                </div>
+            </div>
+        </div>
+        <div class="close-detail" id="closeDetail">×</div>
+    </div>
+
+    <!-- 支付二维码弹窗 -->
+    <div class="payment-mask" id="paymentMask"></div>
+    <div class="payment-modal" id="paymentModal">
+        <div class="payment-header">
+            <h3>扫码支付</h3>
+            <div class="close-payment" id="closePayment">×</div>
+        </div>
+        <div class="payment-content">
+            <div class="payment-info">
+                <div class="info-row">
+                    <span class="label">订单号:</span>
+                    <span class="value" id="payOrderNumber">-</span>
+                </div>
+                <div class="info-row">
+                    <span class="label">支付金额:</span>
+                    <span class="value amount" id="payAmount">¥0.00</span>
+                </div>
+            </div>
+            <div class="qrcode-container">
+                <img id="qrcodeImage" src="" alt="支付二维码" class="qrcode-image">
+                <p class="qrcode-tip">请使用微信或支付宝扫码支付</p>
+            </div>
+            <div class="payment-status" id="paymentStatus">
+                <div class="status-checking">
+                    <div class="loading-spinner"></div>
+                    <p>等待支付中...</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script src="js/app.js"></script>
+</body>
+</html>

+ 598 - 0
oederFoodVue/js/app.js

@@ -0,0 +1,598 @@
+// API 配置
+const API_BASE_URL = 'http://localhost:8086/api';
+
+// 支付二维码图片配置(方便更换)
+// 请将图片复制到 oederFoodVue/images/ 文件夹,然后使用相对路径
+const PAYMENT_QRCODE_IMAGE = 'images/payment-qrcode.jpg';
+
+// 应用状态
+const state = {
+    cart: {}, // { dishId: count } 雪花ID(字符串/数字)唯一键,彻底解决选品关联问题
+    currentCategory: null,
+    selectedDish: null,
+    categories: [],
+    dishes: []
+};
+
+// 工具函数:价格格式化(非数字兜底为0,避免NaN)
+function formatPrice(price) {
+    const num = parseFloat(price);
+    return isNaN(num) ? 0 : num;
+}
+
+// API 调用函数 - 获取分类
+async function fetchCategories() {
+    try {
+        const response = await fetch(`${API_BASE_URL}/categories`);
+        const result = await response.json();
+        if (result.code === 200) {
+            state.categories = result.data || [];
+            if (state.categories.length > 0 && !state.currentCategory) {
+                state.currentCategory = parseInt(state.categories[0].id);
+            }
+        } else {
+            console.error('获取分类失败:', result.message);
+            alert('获取分类失败: ' + result.message);
+        }
+    } catch (error) {
+        console.error('获取分类出错:', error);
+        alert('获取分类出错,请检查后端服务是否启动');
+    }
+}
+
+// API 调用函数 - 获取菜品
+async function fetchDishes() {
+    try {
+        const response = await fetch(`${API_BASE_URL}/dishes`);
+        const result = await response.json();
+        if (result.code === 200) {
+            state.dishes = result.data || [];
+        } else {
+            console.error('获取菜品失败:', result.message);
+            alert('获取菜品失败: ' + result.message);
+        }
+    } catch (error) {
+        console.error('获取菜品出错:', error);
+        alert('获取菜品出错,请检查后端服务是否启动');
+    }
+}
+
+// 初始化
+async function init() {
+    // 加载提示
+    document.getElementById('dishContainer').innerHTML = '<div style="text-align:center;padding:40px;color:#999;">加载中...</div>';
+    // 并行请求数据,提升速度
+    await Promise.all([fetchCategories(), fetchDishes()]);
+    // 渲染页面
+    renderCategories();
+    renderDishes();
+    bindEvents();
+}
+
+
+// 渲染分类列表
+function renderCategories() {
+    const categoryList = document.getElementById('categoryList');
+    if (state.categories.length === 0) {
+        categoryList.innerHTML = '<div style="padding:20px;text-align:center;color:#999;">暂无分类</div>';
+        return;
+    }
+    categoryList.innerHTML = state.categories.map(cat => {
+        const catId = cat.id; // 后端返回的字符串ID
+        const currentId = state.currentCategory; // 直接用字符串ID,不转数字
+        return `
+        <div class="category-item ${catId === currentId ? 'active' : ''}" data-id="${catId}">
+            ${cat.categoryName}
+        </div>
+    `}).join('');
+}
+
+// 渲染菜品列表 - 全程字符串ID,无任何parseInt,彻底解决精度丢失/ID不匹配
+function renderDishes() {
+    const dishContainer = document.getElementById('dishContainer');
+    if (state.dishes.length === 0) {
+        dishContainer.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">暂无菜品</div>';
+        return;
+    }
+    // 过滤有菜品的分类:字符串ID严格相等对比,无任何转换
+    const categoriesWithDishes = state.categories.filter(cat =>
+        state.dishes.some(dish => dish.categoryId === cat.id)
+    );
+    if (categoriesWithDishes.length === 0) {
+        dishContainer.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">暂无菜品</div>';
+        return;
+    }
+    // 按分类渲染菜品,全程字符串ID,data-id唯一绑定
+    dishContainer.innerHTML = categoriesWithDishes.map(category => {
+        const categoryId = category.id; // 纯字符串分类ID,不做任何转换
+        // 过滤当前分类的菜品:字符串categoryId严格相等对比
+        const dishes = state.dishes.filter(dish => dish.categoryId === categoryId);
+        return `
+            <div class="category-section" id="category-${categoryId}"> <!-- 字符串ID拼接,和点击的ID完全一致 -->
+                <h3 class="category-title">${category.categoryName}</h3>
+                <div class="dish-list">${dishes.map(dish => {
+            const imgUrl = dish.coverImg || 'https://picsum.photos/100/100?text=菜品图片';
+            const description = dish.description || '暂无描述';
+            const tasteFeature = dish.tasteFeature || '美味';
+            const adaptScene = dish.adaptScene || '推荐';
+            const sales = dish.sales || 0;
+            const dishId = dish.id; // 保留雪花ID原始字符串值,不做任何转换
+            const inCart = state.cart[dishId]; // 仅匹配当前dishId的购物车状态
+
+            return `
+                        <div class="dish-item ${inCart ? 'in-cart' : ''}" data-id="${dishId}">
+                            <img class="dish-img" src="${imgUrl}" alt="${dish.dishName}" onerror="this.src='https://picsum.photos/100/100?text=菜品图片'">
+                            <div class="dish-info">
+                                <div class="dish-name">${dish.dishName}</div>
+                                <div class="dish-desc">${description}</div>
+                                <div class="dish-tags">
+                                    <span class="dish-tag">${tasteFeature}</span>
+                                    <span class="dish-tag">${adaptScene}</span>
+                                </div>
+                                <div class="dish-footer">
+                                    <div>
+                                        <span class="dish-price">¥${formatPrice(dish.price).toFixed(2)}</span>
+                                        <span class="dish-sales">月售${sales}</span>
+                                    </div>
+                                    <div class="dish-controls">
+                                        ${inCart ? `
+                                            <button class="control-btn minus" data-action="minus" data-id="${dishId}">-</button>
+                                            <span class="dish-count">${inCart}</span>
+                                        ` : ''}
+                                        <button class="control-btn plus" data-action="plus" data-id="${dishId}">+</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    `}).join('')}
+                </div>
+            </div>
+        `;
+    }).join('');
+}
+
+// 更新购物车 - 核心:按dishId唯一计算,不混淆
+function updateCart() {
+    // 计算总数量(仅累加当前dishId的数量)
+    const totalCount = Object.values(state.cart).reduce((sum, count) => sum + (Number(count) || 0), 0);
+    // 计算总价(按dishId匹配菜品,精准计算)
+    const totalPrice = Object.entries(state.cart).reduce((sum, [dishId, count]) => {
+        const dish = state.dishes.find(d => d.id === dishId); // 精准匹配dishId
+        return sum + (dish ? formatPrice(dish.price) * (Number(count) || 0) : 0);
+    }, 0);
+
+    // 更新购物车角标
+    const cartCountEl = document.getElementById('cartCount');
+    const cartIconEl = document.querySelector('.cart-icon');
+    cartCountEl.textContent = totalCount;
+    totalCount > 0 ? (cartCountEl.classList.add('show'), cartIconEl.classList.add('has-items')) : (cartCountEl.classList.remove('show'), cartIconEl.classList.remove('has-items'));
+
+    // 更新总价和结算按钮
+    document.getElementById('totalPrice').textContent = `¥${totalPrice.toFixed(2)}`;
+    const checkoutBtn = document.getElementById('checkoutBtn');
+    
+    // 取消起送门槛,只要有商品就可以结算
+    if (totalCount > 0) {
+        checkoutBtn.disabled = false;
+        checkoutBtn.textContent = `去结算(总计¥${totalPrice.toFixed(2)})`;
+    } else {
+        checkoutBtn.disabled = true;
+        checkoutBtn.textContent = `去结算`;
+    }
+
+    // 局部更新,不重渲染整个列表(性能优化+避免选品关联)
+    renderCartDetail();
+    updateDishCartStatus();
+}
+
+// 局部更新菜品购物车状态 - 核心:按dishId精准更新单个菜品,不影响其他
+function updateDishCartStatus() {
+    document.querySelectorAll('.dish-item').forEach(dishItem => {
+        const dishId = dishItem.dataset.id; // 单独获取每个菜品的dishId
+        const inCart = state.cart[dishId];   // 仅判断当前dishId的购物车状态
+        const controlsDiv = dishItem.querySelector('.dish-controls');
+
+        // 单独更新当前菜品的选中样式
+        inCart ? dishItem.classList.add('in-cart') : dishItem.classList.remove('in-cart');
+        // 单独更新当前菜品的加减按钮
+        if (inCart) {
+            controlsDiv.innerHTML = `
+                <button class="control-btn minus" data-action="minus" data-id="${dishId}">-</button>
+                <span class="dish-count">${inCart}</span>
+                <button class="control-btn plus" data-action="plus" data-id="${dishId}">+</button>
+            `;
+        } else {
+            controlsDiv.innerHTML = `<button class="control-btn plus" data-action="plus" data-id="${dishId}">+</button>`;
+        }
+    });
+}
+
+// 渲染购物车详情 - 按dishId精准展示
+function renderCartDetail() {
+    const cartDetailList = document.getElementById('cartDetailList');
+    const cartItems = Object.entries(state.cart).map(([dishId, count]) => {
+        const dish = state.dishes.find(d => d.id === dishId);
+        return dish ? { ...dish, count } : null;
+    }).filter(Boolean);
+
+    if (cartItems.length === 0) {
+        cartDetailList.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">购物车是空的,快来添加菜品吧~</div>';
+        return;
+    }
+
+    cartDetailList.innerHTML = cartItems.map(item => `
+        <div class="cart-item">
+            <span class="cart-item-name">${item.dishName}</span>
+            <span class="cart-item-price">¥${formatPrice(item.price).toFixed(2)}</span>
+            <div class="dish-controls">
+                <button class="control-btn minus" data-action="minus" data-id="${item.id}">-</button>
+                <span class="dish-count">${item.count}</span>
+                <button class="control-btn plus" data-action="plus" data-id="${item.id}">+</button>
+            </div>
+        </div>
+    `).join('');
+}
+
+// 更新菜品数量 - 核心:dishId全程唯一,操作仅影响当前菜品
+function updateDishCount(dishId, action) {
+    console.log('=== updateDishCount ===');
+    console.log('dishId:', dishId, 'type:', typeof dishId);
+    console.log('action:', action);
+    console.log('购物车 before:', JSON.stringify(state.cart));
+    
+    if (action === 'plus') {
+        state.cart[dishId] = (state.cart[dishId] || 0) + 1;
+        // 首次加购提示
+        if (Object.keys(state.cart).length === 1 && state.cart[dishId] === 1) showCartTip();
+    } else if (action === 'minus') {
+        state.cart[dishId] > 1 ? state.cart[dishId]-- : delete state.cart[dishId];
+    }
+    
+    console.log('购物车 after:', JSON.stringify(state.cart));
+    console.log('购物车键:', Object.keys(state.cart));
+    console.log('购物车值:', Object.values(state.cart));
+    
+    updateCart(); // 仅更新当前菜品相关状态
+}
+
+// 购物车加购提示
+function showCartTip() {
+    const tip = document.getElementById('cartTip');
+    if (tip) {
+        tip.classList.add('show');
+        setTimeout(() => tip.classList.remove('show'), 3000);
+    }
+}
+
+// 显示菜品详情 - 按dishId精准匹配
+function showDishDetail(dishId) {
+    const dish = state.dishes.find(d => d.id === dishId);
+    if (!dish) return;
+    state.selectedDish = dish;
+
+    const imgUrl = dish.coverImg || 'https://picsum.photos/400/400?text=菜品详情';
+    document.getElementById('dishDetailImg').src = imgUrl;
+    document.getElementById('dishDetailImg').onerror = () => this.src = 'https://picsum.photos/400/400?text=菜品详情';
+    document.getElementById('dishDetailName').textContent = dish.dishName;
+    document.getElementById('dishDetailDesc').textContent = dish.description || '暂无描述';
+    document.getElementById('dishDetailPrice').textContent = formatPrice(dish.price).toFixed(2);
+    document.getElementById('dishDetailTags').innerHTML = `
+        <span class="dish-tag">${dish.tasteFeature || '美味'}</span>
+        <span class="dish-tag">${dish.adaptScene || '推荐'}</span>
+        <span class="dish-tag">月售${dish.sales || 0}</span>
+    `;
+
+    document.getElementById('dishDetailMask').classList.add('show');
+    document.getElementById('dishDetail').classList.add('show');
+}
+
+// 隐藏菜品详情
+function hideDishDetail() {
+    document.getElementById('dishDetailMask').classList.remove('show');
+    document.getElementById('dishDetail').classList.remove('show');
+}
+
+// 切换购物车详情
+function toggleCartDetail() {
+    console.log('=== toggleCartDetail ===');
+    console.log('state.cart:', state.cart);
+    console.log('购物车键:', Object.keys(state.cart));
+    console.log('购物车值:', Object.values(state.cart));
+    
+    const totalCount = Object.values(state.cart).reduce((sum, count) => sum + (Number(count) || 0), 0);
+    console.log('总数量:', totalCount);
+    
+    if (totalCount === 0) {
+        alert('购物车为空,无法打开~');
+        return;
+    }
+    const mask = document.getElementById('cartMask');
+    const detail = document.getElementById('cartDetail');
+    mask.classList.contains('show') ? (mask.classList.remove('show'), detail.classList.remove('show')) : (mask.classList.add('show'), detail.classList.add('show'));
+}
+
+// 清空购物车
+function clearCart() {
+    if (confirm('确定要清空购物车吗?清空后将恢复为空~')) {
+        state.cart = {};
+        updateCart();
+        toggleCartDetail();
+    }
+}
+
+// 结算功能 - 先显示二维码,支付成功后创建订单
+async function checkout() {
+    const totalPrice = Object.entries(state.cart).reduce((sum, [dishId, count]) => {
+        const dish = state.dishes.find(d => d.id === dishId);
+        return sum + (dish ? formatPrice(dish.price) * (Number(count) || 0) : 0);
+    }, 0);
+
+    // 取消起送门槛检查,只要有商品就可以结算
+    if (totalPrice <= 0) {
+        alert('购物车是空的,请先添加商品~');
+        return;
+    }
+
+    // 准备订单数据(暂存,支付成功后再提交)
+    const orderData = {
+        items: Object.entries(state.cart).map(([dishId, quantity]) => ({
+            dishId: dishId,
+            quantity: quantity
+        })),
+        totalAmount: totalPrice,
+        deliveryFee: 0,
+        finalAmount: totalPrice,
+        remark: ''
+    };
+
+    // 直接显示支付二维码
+    showPaymentQRCode(orderData);
+}
+
+// 显示支付二维码
+function showPaymentQRCode(orderData) {
+    // 生成临时支付单号用于验证
+    const paymentId = 'PAY' + Date.now();
+    
+    // 显示订单信息
+    document.getElementById('payOrderNumber').textContent = paymentId;
+    document.getElementById('payAmount').textContent = `¥${orderData.finalAmount.toFixed(2)}`;
+
+    // 设置二维码图片
+    const qrcodeImage = document.getElementById('qrcodeImage');
+    qrcodeImage.src = PAYMENT_QRCODE_IMAGE;
+    qrcodeImage.onerror = function() {
+        this.src = 'https://via.placeholder.com/200x200/4caf50/ffffff?text=请添加支付二维码';
+    };
+
+    // 显示支付弹窗
+    document.getElementById('paymentMask').classList.add('show');
+    document.getElementById('paymentModal').classList.add('show');
+
+    // 开始轮询支付状态(传入支付单号和订单数据)
+    startPaymentPolling(paymentId, orderData);
+}
+
+// 轮询支付状态
+let paymentPollingTimer = null;
+let pollingCount = 0;
+const MAX_POLLING_COUNT = 60; // 最多轮询60次(2分钟)
+
+function startPaymentPolling(paymentId, orderData) {
+    if (paymentPollingTimer) clearInterval(paymentPollingTimer);
+    
+    pollingCount = 0;
+    
+    // 每2秒轮询一次支付状态
+    paymentPollingTimer = setInterval(async () => {
+        pollingCount++;
+        
+        // 超时处理
+        if (pollingCount > MAX_POLLING_COUNT) {
+            stopPaymentPolling();
+            showPaymentFailure('支付超时,请重新下单');
+            return;
+        }
+        
+        try {
+            // 调用后端验证支付状态
+            const response = await fetch(`${API_BASE_URL}/payment/verify?paymentId=${paymentId}`);
+            const result = await response.json();
+            
+            if (result.code === 200 && result.data.paid) {
+                // 支付成功,停止轮询
+                stopPaymentPolling();
+                
+                // 创建订单
+                await createOrderAfterPayment(orderData);
+            } else if (result.code === 200 && result.data.failed) {
+                // 支付失败
+                stopPaymentPolling();
+                showPaymentFailure('支付失败,请重试');
+            }
+            // 如果是待支付状态,继续轮询
+        } catch (error) {
+            console.error('验证支付状态失败:', error);
+        }
+    }, 2000);
+}
+
+// 支付成功后创建订单
+async function createOrderAfterPayment(orderData) {
+    try {
+        const response = await fetch(`${API_BASE_URL}/orders`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(orderData)
+        });
+        const result = await response.json();
+        
+        if (result.code === 200) {
+            showPaymentSuccess();
+        } else {
+            showPaymentFailure('订单创建失败: ' + result.message);
+        }
+    } catch (error) {
+        console.error('创建订单失败:', error);
+        showPaymentFailure('订单创建失败,请联系客服');
+    }
+}
+
+function stopPaymentPolling() {
+    if (paymentPollingTimer) {
+        clearInterval(paymentPollingTimer);
+        paymentPollingTimer = null;
+    }
+    pollingCount = 0;
+}
+
+// 显示支付失败
+function showPaymentFailure(message) {
+    document.getElementById('paymentStatus').innerHTML = `
+        <div class="status-failure show">
+            <div class="failure-icon">✗</div>
+            <p>${message}</p>
+        </div>
+    `;
+    
+    setTimeout(() => {
+        closePaymentModal();
+    }, 3000);
+}
+
+function showPaymentSuccess() {
+    document.getElementById('paymentStatus').innerHTML = `
+        <div class="status-success show">
+            <div class="success-icon">✓</div>
+            <p>支付成功!</p>
+        </div>
+    `;
+
+    setTimeout(() => {
+        closePaymentModal();
+        state.cart = {};
+        updateCart();
+        
+        const mask = document.getElementById('cartMask');
+        const detail = document.getElementById('cartDetail');
+        mask.classList.remove('show');
+        detail.classList.remove('show');
+
+        alert('支付成功!我们将尽快为您配送~');
+    }, 2000);
+}
+
+function closePaymentModal() {
+    stopPaymentPolling();
+    document.getElementById('paymentMask').classList.remove('show');
+    document.getElementById('paymentModal').classList.remove('show');
+    
+    document.getElementById('paymentStatus').innerHTML = `
+        <div class="status-checking">
+            <div class="loading-spinner"></div>
+            <p>等待支付中...</p>
+        </div>
+    `;
+}
+
+// 事件绑定 - 核心:事件委托精准获取当前dishId,不混淆
+function bindEvents() {
+    // 分类切换
+    document.getElementById('categoryList').addEventListener('click', (e) => {
+        const item = e.target.closest('.category-item');
+        if (item) {
+            const categoryId = item.dataset.id;
+            state.currentCategory = categoryId;
+            console.log('=== 分类切换 ===');
+            console.log('目标分类 ID:', categoryId);
+
+            renderCategories();
+            // 【补充】分类切换时建议重新渲染菜品(确保菜品区域和分类同步)
+            renderDishes();
+
+            // 使用 requestAnimationFrame 确保 DOM 完全更新(双重帧足够等待渲染)
+            requestAnimationFrame(() => {
+                requestAnimationFrame(() => {
+                    // 错误1修复:拼接正确的分类区域ID → category-xxx
+                    const section = document.getElementById(`category-${categoryId}`);
+                    // 错误2修复:修正滚动容器类名 → .dish-container(和渲染一致)
+                    const container = document.querySelector('.dish-container');
+
+                    console.log('分类区域元素:', section);
+                    console.log('滚动容器:', container);
+
+                    if (section && container) {
+                        section.scrollIntoView({
+                            behavior: 'smooth',
+                            block: 'start' // 贴顶显示,体验更好
+                        });
+                        console.log('滚动成功:已定位到分类 →', `category-${categoryId}`);
+                    } else {
+                        // 分情况提示错误,方便排查
+                        if (!section) {
+                            console.warn('滚动失败:未找到分类区域元素,可能该分类无菜品', `ID: category-${categoryId}`);
+                        }
+                        if (!container) {
+                            console.error('滚动失败:未找到菜品滚动容器,请检查类名是否为 .dish-container');
+                        }
+                    }
+                });
+            });
+        }
+    });
+
+    // 以下是你原有的其他事件逻辑(菜品操作、购物车、详情等),保留不变即可
+    // 菜品操作 - 精准获取当前点击按钮的dishId
+    document.getElementById('dishContainer').addEventListener('click', (e) => {
+        const btn = e.target.closest('.control-btn');
+        if (btn) {
+            e.stopPropagation();
+            const dishId = btn.dataset.id;
+            const action = btn.dataset.action;
+            updateDishCount(dishId, action);
+            return;
+        }
+        // 菜品详情
+        const dishItem = e.target.closest('.dish-item');
+        dishItem && showDishDetail(dishItem.dataset.id);
+    });
+
+    // 购物车图标
+    document.getElementById('cartIcon').addEventListener('click', toggleCartDetail);
+    // 购物车遮罩
+    document.getElementById('cartMask').addEventListener('click', toggleCartDetail);
+    // 购物车内部操作
+    document.getElementById('cartDetailList').addEventListener('click', (e) => {
+        const btn = e.target.closest('.control-btn');
+        btn && updateDishCount(btn.dataset.id, btn.dataset.action);
+    });
+
+    // 清空购物车
+    document.getElementById('clearCart').addEventListener('click', (e) => {
+        e.stopPropagation();
+        clearCart();
+    });
+    // 结算按钮
+    document.getElementById('checkoutBtn').addEventListener('click', (e) => {
+        e.stopPropagation();
+        checkout();
+    });
+
+    // 菜品详情操作
+    document.getElementById('addToCartBtn').addEventListener('click', () => {
+        state.selectedDish && (updateDishCount(state.selectedDish.id, 'plus'), hideDishDetail());
+    });
+    document.getElementById('closeDetail').addEventListener('click', hideDishDetail);
+    document.getElementById('dishDetailMask').addEventListener('click', hideDishDetail);
+    document.getElementById('dishDetail').addEventListener('click', (e) => e.stopPropagation());
+    
+    // 支付弹窗事件
+    document.getElementById('closePayment').addEventListener('click', closePaymentModal);
+    document.getElementById('paymentMask').addEventListener('click', closePaymentModal);
+    document.getElementById('paymentModal').addEventListener('click', (e) => e.stopPropagation());
+}
+
+// 页面加载完成初始化
+document.addEventListener('DOMContentLoaded', init);

+ 238 - 0
oederFoodVue/payment-simulator.html

@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>支付模拟器</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 20px;
+        }
+        
+        .container {
+            background: white;
+            border-radius: 20px;
+            padding: 40px;
+            max-width: 500px;
+            width: 100%;
+            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
+        }
+        
+        h1 {
+            text-align: center;
+            color: #333;
+            margin-bottom: 30px;
+            font-size: 28px;
+        }
+        
+        .info {
+            background: #f5f5f5;
+            padding: 20px;
+            border-radius: 10px;
+            margin-bottom: 30px;
+        }
+        
+        .info-item {
+            display: flex;
+            justify-content: space-between;
+            margin-bottom: 10px;
+            font-size: 16px;
+        }
+        
+        .info-item:last-child {
+            margin-bottom: 0;
+        }
+        
+        .label {
+            color: #666;
+        }
+        
+        .value {
+            color: #333;
+            font-weight: bold;
+        }
+        
+        .amount {
+            color: #ff6b6b;
+            font-size: 24px;
+        }
+        
+        .buttons {
+            display: flex;
+            gap: 15px;
+        }
+        
+        button {
+            flex: 1;
+            padding: 15px;
+            border: none;
+            border-radius: 10px;
+            font-size: 16px;
+            font-weight: bold;
+            cursor: pointer;
+            transition: all 0.3s;
+        }
+        
+        .btn-success {
+            background: #4caf50;
+            color: white;
+        }
+        
+        .btn-success:hover {
+            background: #45a049;
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
+        }
+        
+        .btn-fail {
+            background: #f44336;
+            color: white;
+        }
+        
+        .btn-fail:hover {
+            background: #da190b;
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(244, 67, 54, 0.3);
+        }
+        
+        .message {
+            margin-top: 20px;
+            padding: 15px;
+            border-radius: 10px;
+            text-align: center;
+            font-weight: bold;
+            display: none;
+        }
+        
+        .message.success {
+            background: #d4edda;
+            color: #155724;
+            display: block;
+        }
+        
+        .message.error {
+            background: #f8d7da;
+            color: #721c24;
+            display: block;
+        }
+        
+        .note {
+            margin-top: 20px;
+            padding: 15px;
+            background: #fff3cd;
+            border-radius: 10px;
+            font-size: 14px;
+            color: #856404;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>💳 支付模拟器</h1>
+        
+        <div class="info">
+            <div class="info-item">
+                <span class="label">支付单号:</span>
+                <span class="value" id="paymentId">-</span>
+            </div>
+            <div class="info-item">
+                <span class="label">支付金额:</span>
+                <span class="value amount" id="amount">¥0.00</span>
+            </div>
+        </div>
+        
+        <div class="buttons">
+            <button class="btn-success" onclick="simulatePayment('success')">
+                ✓ 支付成功
+            </button>
+            <button class="btn-fail" onclick="simulatePayment('failed')">
+                ✗ 支付失败
+            </button>
+        </div>
+        
+        <div class="message" id="message"></div>
+        
+        <div class="note">
+            <strong>使用说明:</strong><br>
+            1. 在主页面点击"去结算"后会显示二维码<br>
+            2. 从URL中获取支付单号(paymentId)<br>
+            3. 在此页面输入支付单号并点击按钮模拟支付结果<br>
+            4. 主页面会自动检测到支付状态并创建订单
+        </div>
+    </div>
+    
+    <script>
+        const API_BASE_URL = 'http://localhost:8086/api';
+        
+        // 从URL获取支付信息
+        function getPaymentInfo() {
+            const urlParams = new URLSearchParams(window.location.search);
+            const paymentId = urlParams.get('paymentId');
+            const amount = urlParams.get('amount');
+            
+            if (paymentId) {
+                document.getElementById('paymentId').textContent = paymentId;
+            }
+            if (amount) {
+                document.getElementById('amount').textContent = '¥' + parseFloat(amount).toFixed(2);
+            }
+            
+            return { paymentId, amount };
+        }
+        
+        // 模拟支付
+        async function simulatePayment(status) {
+            const paymentId = document.getElementById('paymentId').textContent;
+            
+            if (paymentId === '-') {
+                showMessage('请先从主页面扫描二维码!', 'error');
+                return;
+            }
+            
+            try {
+                const response = await fetch(`${API_BASE_URL}/payment/simulate?paymentId=${paymentId}&status=${status}`, {
+                    method: 'POST'
+                });
+                const result = await response.json();
+                
+                if (result.code === 200) {
+                    const message = status === 'success' ? '支付成功!订单正在创建中...' : '支付失败!';
+                    showMessage(message, status === 'success' ? 'success' : 'error');
+                    
+                    if (status === 'success') {
+                        setTimeout(() => {
+                            window.close();
+                        }, 2000);
+                    }
+                } else {
+                    showMessage('操作失败:' + result.message, 'error');
+                }
+            } catch (error) {
+                console.error('模拟支付失败:', error);
+                showMessage('网络错误,请检查后端服务', 'error');
+            }
+        }
+        
+        function showMessage(text, type) {
+            const messageEl = document.getElementById('message');
+            messageEl.textContent = text;
+            messageEl.className = 'message ' + type;
+        }
+        
+        // 页面加载时获取支付信息
+        window.onload = getPaymentInfo;
+    </script>
+</body>
+</html>

+ 107 - 2
pom.xml

@@ -12,6 +12,10 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <spring-boot.version>3.0.2</spring-boot.version>
+        <mybatisplus.version>3.5.3</mybatisplus.version>
+        <druid.version>1.1.21</druid.version>
+        <fastjson.version>1.2.80</fastjson.version>
+        <swagger.version>3.0.0</swagger.version>
     </properties>
     <dependencies>
         <dependency>
@@ -24,10 +28,111 @@
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
+        <!-- Spring 集成支持(若使用 Spring Boot,可省略,已自动集成) -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-test</artifactId>
-            <scope>test</scope>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatisplus.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.baomidou</groupId>
+                    <artifactId>mybatis-plus-generator</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.7.15</version>
+        </dependency>
+        <!-- apache commons end -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>${fastjson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.20.0</version> <!-- 最新版本可到官网查询 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.5</version>
+        </dependency>
+        <!--        <dependency>-->
+        <!--            <groupId>io.springfox</groupId>-->
+        <!--            <artifactId>springfox-boot-starter</artifactId>-->
+        <!--            <version>${swagger.version}</version>-->
+        <!--        </dependency>-->
+
+        <!--        <dependency>-->
+        <!--            <groupId>io.springfox</groupId>-->
+        <!--            <artifactId>springfox-swagger-ui</artifactId>-->
+        <!--            <version>${swagger.version}</version>-->
+        <!--        </dependency>-->
+        <!-- SpringDoc OpenAPI 3 核心依赖(适配 Spring Boot 3.x) -->
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.xml.bind</groupId>
+            <artifactId>jakarta.xml.bind-api</artifactId>
+            <version>4.0.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <version>4.0.1</version>
+            <scope>runtime</scope>
+        </dependency>
+        <!-- MapStruct核心依赖 -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>1.5.5.Final</version>
+        </dependency>
+        <!-- 编译时生成映射代码,必须加 -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <version>1.5.5.Final</version>
+            <scope>provided</scope>
         </dependency>
     </dependencies>
     <dependencyManagement>

+ 2 - 0
src/main/java/com/we/OrderFoodApplication.java

@@ -1,9 +1,11 @@
 package com.we;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
+@MapperScan("com.we.mapper")
 public class OrderFoodApplication {
 
     public static void main(String[] args) {

+ 133 - 0
src/main/java/com/we/controller/DishController.java

@@ -0,0 +1,133 @@
+package com.we.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.we.domain.Dish;
+import com.we.domain.DishCategory;
+import com.we.domain.Flaver;
+import com.we.dto.Result;
+import com.we.service.DishCategoryService;
+import com.we.service.DishService;
+import com.we.service.FlaverService;
+import com.we.vo.CategoryVo;
+import com.we.vo.DishVO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 菜品控制器
+ */
+@RestController
+@RequestMapping("/api")
+@CrossOrigin(origins = "*")
+public class DishController {
+
+    @Autowired
+    private DishService dishService;
+
+    @Autowired
+    private DishCategoryService dishCategoryService;
+
+    @Autowired
+    private FlaverService flaverService;
+
+    /**
+     * 获取所有分类
+     */
+    @GetMapping("/categories")
+    public Result<List<DishCategory>> getCategories() {
+        LambdaQueryWrapper<DishCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(DishCategory::getStatus, 0)
+                .orderByAsc(DishCategory::getSort);
+        List<DishCategory> categories = dishCategoryService.list(wrapper);
+        // 转换为List<CategoryVo>
+        List<CategoryVo> categoryVoList = new ArrayList<>();
+        for (DishCategory dishCategory : categories) {
+            CategoryVo vo = new CategoryVo();
+            // 逐个映射属性(关键:属性名对应,雪花ID转String更推荐,避免前端精度丢失)
+            vo.setId(dishCategory.getId().toString()); // Long转String,核心修复精度问题
+            vo.setCategoryName(dishCategory.getCategoryName());
+            vo.setSort(dishCategory.getSort());
+            vo.setStatus(dishCategory.getStatus());
+            // 其他属性按需映射...
+            categoryVoList.add(vo);
+        }
+        return Result.success(categories);
+    }
+
+    /**
+     * 获取所有菜品(带分类和口味信息)
+     */
+    @GetMapping("/dishes")
+    public Result<List<DishVO>> getDishes(@RequestParam(required = false) Long categoryId) {
+        // 查询菜品
+        LambdaQueryWrapper<Dish> dishWrapper = new LambdaQueryWrapper<>();
+        dishWrapper.eq(Dish::getStatus, 1)
+                .eq(Dish::getIsDelete, 0);
+        if (categoryId != null) {
+            dishWrapper.eq(Dish::getCategoryId, categoryId);
+        }
+        List<Dish> dishes = dishService.list(dishWrapper);
+
+        // 查询所有分类
+        Map<Long, String> categoryMap = dishCategoryService.list()
+                .stream()
+                .collect(Collectors.toMap(DishCategory::getId, DishCategory::getCategoryName));
+
+        // 查询所有口味
+        Map<Integer, String> flavorMap = flaverService.list()
+                .stream()
+                .collect(Collectors.toMap(
+                        f -> f.getId().intValue(),
+                        Flaver::getFlavorName
+                ));
+
+        // 组装VO
+        List<DishVO> dishVOList = dishes.stream().map(dish -> {
+            DishVO vo = new DishVO();
+            BeanUtils.copyProperties(dish, vo);
+            vo.setCategoryName(categoryMap.get(dish.getCategoryId()));
+            if (dish.getFlavorId() != null) {
+                vo.setFlavorName(flavorMap.get(dish.getFlavorId()));
+            }
+            return vo;
+        }).collect(Collectors.toList());
+
+        return Result.success(dishVOList);
+    }
+
+    /**
+     * 获取菜品详情
+     */
+    @GetMapping("/dishes/{id}")
+    public Result<DishVO> getDishDetail(@PathVariable Long id) {
+        Dish dish = dishService.getById(id);
+        if (dish == null) {
+            return Result.error("菜品不存在");
+        }
+
+        DishVO vo = new DishVO();
+        BeanUtils.copyProperties(dish, vo);
+
+        // 获取分类名称
+        DishCategory category = dishCategoryService.getById(dish.getCategoryId());
+        if (category != null) {
+            vo.setCategoryName(category.getCategoryName());
+        }
+
+        // 获取口味名称
+        if (dish.getFlavorId() != null) {
+            Flaver flavor = flaverService.getById(dish.getFlavorId());
+            if (flavor != null) {
+                vo.setFlavorName(flavor.getFlavorName());
+            }
+        }
+
+        return Result.success(vo);
+    }
+}

+ 54 - 0
src/main/java/com/we/controller/OrderController.java

@@ -0,0 +1,54 @@
+package com.we.controller;
+
+import com.we.domain.Order;
+import com.we.dto.OrderDTO;
+import com.we.dto.Result;
+import com.we.service.OrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 订单控制器
+ */
+@RestController
+@RequestMapping("/api/orders")
+@CrossOrigin(origins = "*")
+public class OrderController {
+    
+    @Autowired
+    private OrderService orderService;
+    
+    /**
+     * 创建订单
+     */
+    @PostMapping
+    public Result<Order> createOrder(@RequestBody OrderDTO orderDTO) {
+        try {
+            Order order = orderService.createOrder(orderDTO);
+            return Result.success(order);
+        } catch (Exception e) {
+            return Result.error("创建订单失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 支付订单
+     */
+    @PostMapping("/{orderId}/pay")
+    public Result<String> payOrder(@PathVariable Long orderId) {
+        try {
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error("订单不存在");
+            }
+            
+            // 更新订单状态为已支付
+            order.setStatus(1);
+            orderService.updateById(order);
+            
+            return Result.success("支付成功");
+        } catch (Exception e) {
+            return Result.error("支付失败: " + e.getMessage());
+        }
+    }
+}

+ 58 - 0
src/main/java/com/we/controller/PaymentController.java

@@ -0,0 +1,58 @@
+package com.we.controller;
+
+import com.we.dto.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 支付控制器
+ */
+@RestController
+@RequestMapping("/api/payment")
+@CrossOrigin(origins = "*")
+public class PaymentController {
+    
+    // 模拟支付状态存储(实际应该用Redis或数据库)
+    private static final Map<String, String> paymentStatus = new ConcurrentHashMap<>();
+    
+    /**
+     * 验证支付状态
+     */
+    @GetMapping("/verify")
+    public Result<Map<String, Object>> verifyPayment(@RequestParam String paymentId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        String status = paymentStatus.getOrDefault(paymentId, "pending");
+        
+        result.put("paymentId", paymentId);
+        result.put("status", status);
+        result.put("paid", "success".equals(status));
+        result.put("failed", "failed".equals(status));
+        
+        return Result.success(result);
+    }
+    
+    /**
+     * 模拟支付回调(用于测试)
+     * 实际项目中这个接口应该由支付平台(如微信、支付宝)调用
+     */
+    @PostMapping("/callback")
+    public Result<String> paymentCallback(@RequestParam String paymentId, 
+                                          @RequestParam String status) {
+        paymentStatus.put(paymentId, status);
+        return Result.success("回调成功");
+    }
+    
+    /**
+     * 模拟用户支付(用于测试)
+     */
+    @PostMapping("/simulate")
+    public Result<String> simulatePayment(@RequestParam String paymentId,
+                                          @RequestParam(defaultValue = "success") String status) {
+        paymentStatus.put(paymentId, status);
+        return Result.success("支付模拟成功");
+    }
+}

+ 103 - 0
src/main/java/com/we/domain/Dish.java

@@ -0,0 +1,103 @@
+package com.we.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.math.BigDecimal;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 菜品核心表
+ * @TableName dish
+ */
+@TableName(value ="dish")
+@Data
+public class Dish {
+    /**
+     * 菜品ID(主键)
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 菜品名称(唯一)
+     */
+    private String dishName;
+
+    /**
+     * 所属分类ID,关联dish_category.id
+     */
+    private Long categoryId;
+
+    /**
+     * 口味ID,关联flavor.id
+     */
+    private Long flavorId;
+
+    /**
+     * 核心食材(如黄瓜、蒜末;番茄、鸡蛋)
+     */
+    private String coreMaterial;
+
+    /**
+     * 菜品单价,保留2位小数
+     */
+    private BigDecimal price;
+
+    /**
+     * 菜品成本价,用于核算利润
+     */
+    private BigDecimal costPrice;
+
+    /**
+     * 库存数量(0为无库存,-1为无限库存)
+     */
+    private Integer stock;
+
+    /**
+     * 销量,自动累计
+     */
+    private Integer sales;
+
+    /**
+     * 口味特点(补充,如酸辣爽口、脆嫩入味)
+     */
+    private String tasteFeature;
+
+    /**
+     * 适配场景(如日常佐餐、家庭小聚、下酒)
+     */
+    private String adaptScene;
+
+    /**
+     * 菜品封面图URL,用于前端展示
+     */
+    private String coverImg;
+
+    /**
+     * 菜品描述(如做法贴士、食材亮点)
+     */
+    private String description;
+
+    /**
+     * 状态:1-上架,0-下架
+     */
+    private Integer status;
+
+    /**
+     * 软删除:0-未删,1-已删
+     */
+    private Integer isDelete;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+}

+ 51 - 0
src/main/java/com/we/domain/DishCategory.java

@@ -0,0 +1,51 @@
+package com.we.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.util.Date;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+/**
+ * 
+ * @TableName dish_category
+ */
+@TableName(value ="dish_category")
+@Data
+public class DishCategory {
+    /**
+     * 雪花id
+     */
+    @TableId
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态 0:启用 1:禁用
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 修改时间
+     */
+    private Date updateTime;
+}

+ 47 - 0
src/main/java/com/we/domain/Flaver.java

@@ -0,0 +1,47 @@
+package com.we.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 
+ * @TableName flaver
+ */
+@TableName(value ="flaver")
+@Data
+public class Flaver {
+    /**
+     * 口味id
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 口味名称
+     */
+    private String flavorName;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态 0:启用 1:禁用
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 修改时间
+     */
+    private Date updateTime;
+}

+ 28 - 0
src/main/java/com/we/domain/Order.java

@@ -0,0 +1,28 @@
+package com.we.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单表
+ */
+@TableName(value = "orders")
+@Data
+public class Order {
+    @TableId(type = IdType.INPUT)
+    private Long id;
+    
+    private String orderNumber;
+    private BigDecimal totalAmount;
+    private BigDecimal deliveryFee;
+    private BigDecimal finalAmount;
+    private Integer status; // 0-待支付 1-已支付 2-已取消
+    private String remark;
+    private Date createTime;
+    private Date updateTime;
+}

+ 25 - 0
src/main/java/com/we/domain/OrderDetail.java

@@ -0,0 +1,25 @@
+package com.we.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单详情表
+ */
+@TableName(value = "order_detail")
+@Data
+public class OrderDetail {
+    @TableId(type = IdType.INPUT)
+    private Long id;
+    
+    private Long orderId;
+    private Long dishId;
+    private String dishName;
+    private BigDecimal price;
+    private Integer quantity;
+    private BigDecimal subtotal;
+}

+ 20 - 0
src/main/java/com/we/dto/OrderDTO.java

@@ -0,0 +1,20 @@
+package com.we.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 订单提交 DTO
+ */
+@Data
+public class OrderDTO {
+    private List<OrderItemDTO> items;
+    private String remark;
+    
+    @Data
+    public static class OrderItemDTO {
+        private Long dishId;
+        private Integer quantity;
+    }
+}

+ 32 - 0
src/main/java/com/we/dto/Result.java

@@ -0,0 +1,32 @@
+package com.we.dto;
+
+import lombok.Data;
+
+/**
+ * 统一返回结果
+ */
+@Data
+public class Result<T> {
+    private Integer code;
+    private String message;
+    private T data;
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success() {
+        return success(null);
+    }
+
+    public static <T> Result<T> error(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(500);
+        result.setMessage(message);
+        return result;
+    }
+}

+ 18 - 0
src/main/java/com/we/mapper/DishCategoryMapper.java

@@ -0,0 +1,18 @@
+package com.we.mapper;
+
+import com.we.domain.DishCategory;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author Lenovo
+* @description 针对表【dish_category】的数据库操作Mapper
+* @createDate 2026-02-04 18:01:02
+* @Entity com.we.domain.DishCategory
+*/
+public interface DishCategoryMapper extends BaseMapper<DishCategory> {
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/we/mapper/DishMapper.java

@@ -0,0 +1,18 @@
+package com.we.mapper;
+
+import com.we.domain.Dish;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author Lenovo
+* @description 针对表【dish(菜品核心表)】的数据库操作Mapper
+* @createDate 2026-02-04 18:01:02
+* @Entity com.we.domain.Dish
+*/
+public interface DishMapper extends BaseMapper<Dish> {
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/we/mapper/FlaverMapper.java

@@ -0,0 +1,18 @@
+package com.we.mapper;
+
+import com.we.domain.Flaver;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author Lenovo
+* @description 针对表【flaver】的数据库操作Mapper
+* @createDate 2026-02-04 18:01:02
+* @Entity com.we.domain.Flaver
+*/
+public interface FlaverMapper extends BaseMapper<Flaver> {
+
+}
+
+
+
+

+ 9 - 0
src/main/java/com/we/mapper/OrderDetailMapper.java

@@ -0,0 +1,9 @@
+package com.we.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.we.domain.OrderDetail;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
+}

+ 9 - 0
src/main/java/com/we/mapper/OrderMapper.java

@@ -0,0 +1,9 @@
+package com.we.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.we.domain.Order;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderMapper extends BaseMapper<Order> {
+}

+ 13 - 0
src/main/java/com/we/service/DishCategoryService.java

@@ -0,0 +1,13 @@
+package com.we.service;
+
+import com.we.domain.DishCategory;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author Lenovo
+* @description 针对表【dish_category】的数据库操作Service
+* @createDate 2026-02-04 18:01:02
+*/
+public interface DishCategoryService extends IService<DishCategory> {
+
+}

+ 13 - 0
src/main/java/com/we/service/DishService.java

@@ -0,0 +1,13 @@
+package com.we.service;
+
+import com.we.domain.Dish;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author Lenovo
+* @description 针对表【dish(菜品核心表)】的数据库操作Service
+* @createDate 2026-02-04 18:01:02
+*/
+public interface DishService extends IService<Dish> {
+
+}

+ 13 - 0
src/main/java/com/we/service/FlaverService.java

@@ -0,0 +1,13 @@
+package com.we.service;
+
+import com.we.domain.Flaver;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author Lenovo
+* @description 针对表【flaver】的数据库操作Service
+* @createDate 2026-02-04 18:01:02
+*/
+public interface FlaverService extends IService<Flaver> {
+
+}

+ 7 - 0
src/main/java/com/we/service/OrderDetailService.java

@@ -0,0 +1,7 @@
+package com.we.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.we.domain.OrderDetail;
+
+public interface OrderDetailService extends IService<OrderDetail> {
+}

+ 9 - 0
src/main/java/com/we/service/OrderService.java

@@ -0,0 +1,9 @@
+package com.we.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.we.domain.Order;
+import com.we.dto.OrderDTO;
+
+public interface OrderService extends IService<Order> {
+    Order createOrder(OrderDTO orderDTO);
+}

+ 22 - 0
src/main/java/com/we/service/impl/DishCategoryServiceImpl.java

@@ -0,0 +1,22 @@
+package com.we.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.we.domain.DishCategory;
+import com.we.service.DishCategoryService;
+import com.we.mapper.DishCategoryMapper;
+import org.springframework.stereotype.Service;
+
+/**
+* @author Lenovo
+* @description 针对表【dish_category】的数据库操作Service实现
+* @createDate 2026-02-04 18:01:02
+*/
+@Service
+public class DishCategoryServiceImpl extends ServiceImpl<DishCategoryMapper, DishCategory>
+    implements DishCategoryService{
+
+}
+
+
+
+

+ 22 - 0
src/main/java/com/we/service/impl/DishServiceImpl.java

@@ -0,0 +1,22 @@
+package com.we.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.we.domain.Dish;
+import com.we.service.DishService;
+import com.we.mapper.DishMapper;
+import org.springframework.stereotype.Service;
+
+/**
+* @author Lenovo
+* @description 针对表【dish(菜品核心表)】的数据库操作Service实现
+* @createDate 2026-02-04 18:01:02
+*/
+@Service
+public class DishServiceImpl extends ServiceImpl<DishMapper, Dish>
+    implements DishService{
+
+}
+
+
+
+

+ 22 - 0
src/main/java/com/we/service/impl/FlaverServiceImpl.java

@@ -0,0 +1,22 @@
+package com.we.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.we.domain.Flaver;
+import com.we.service.FlaverService;
+import com.we.mapper.FlaverMapper;
+import org.springframework.stereotype.Service;
+
+/**
+* @author Lenovo
+* @description 针对表【flaver】的数据库操作Service实现
+* @createDate 2026-02-04 18:01:02
+*/
+@Service
+public class FlaverServiceImpl extends ServiceImpl<FlaverMapper, Flaver>
+    implements FlaverService{
+
+}
+
+
+
+

+ 11 - 0
src/main/java/com/we/service/impl/OrderDetailServiceImpl.java

@@ -0,0 +1,11 @@
+package com.we.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.we.domain.OrderDetail;
+import com.we.mapper.OrderDetailMapper;
+import com.we.service.OrderDetailService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {
+}

+ 90 - 0
src/main/java/com/we/service/impl/OrderServiceImpl.java

@@ -0,0 +1,90 @@
+package com.we.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.we.domain.Dish;
+import com.we.domain.Order;
+import com.we.domain.OrderDetail;
+import com.we.dto.OrderDTO;
+import com.we.mapper.OrderMapper;
+import com.we.service.DishService;
+import com.we.service.OrderDetailService;
+import com.we.service.OrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+@Service
+public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
+    
+    @Autowired
+    private DishService dishService;
+    
+    @Autowired
+    private OrderDetailService orderDetailService;
+    
+    @Override
+    @Transactional
+    public Order createOrder(OrderDTO orderDTO) {
+        // 生成订单号
+        String orderNumber = generateOrderNumber();
+        
+        // 计算订单金额
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        
+        for (OrderDTO.OrderItemDTO item : orderDTO.getItems()) {
+            Dish dish = dishService.getById(item.getDishId());
+            if (dish == null) {
+                throw new RuntimeException("菜品不存在: " + item.getDishId());
+            }
+            BigDecimal subtotal = dish.getPrice().multiply(new BigDecimal(item.getQuantity()));
+            totalAmount = totalAmount.add(subtotal);
+        }
+        
+        // 取消配送费
+        BigDecimal deliveryFee = BigDecimal.ZERO;
+        BigDecimal finalAmount = totalAmount;
+        
+        // 创建订单
+        Order order = new Order();
+        order.setId(System.currentTimeMillis());
+        order.setOrderNumber(orderNumber);
+        order.setTotalAmount(totalAmount);
+        order.setDeliveryFee(deliveryFee);
+        order.setFinalAmount(finalAmount);
+        order.setStatus(0); // 待支付
+        order.setRemark(orderDTO.getRemark());
+        order.setCreateTime(new Date());
+        order.setUpdateTime(new Date());
+        
+        save(order);
+        
+        // 创建订单详情
+        for (OrderDTO.OrderItemDTO item : orderDTO.getItems()) {
+            Dish dish = dishService.getById(item.getDishId());
+            BigDecimal subtotal = dish.getPrice().multiply(new BigDecimal(item.getQuantity()));
+            
+            OrderDetail detail = new OrderDetail();
+            detail.setId(System.currentTimeMillis() + item.getDishId());
+            detail.setOrderId(order.getId());
+            detail.setDishId(dish.getId());
+            detail.setDishName(dish.getDishName());
+            detail.setPrice(dish.getPrice());
+            detail.setQuantity(item.getQuantity());
+            detail.setSubtotal(subtotal);
+            
+            orderDetailService.save(detail);
+        }
+        
+        return order;
+    }
+    
+    private String generateOrderNumber() {
+        return "ORD" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) 
+                + String.format("%04d", (int)(Math.random() * 10000));
+    }
+}

+ 40 - 0
src/main/java/com/we/vo/CategoryVo.java

@@ -0,0 +1,40 @@
+package com.we.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.util.Date;
+
+
+@Data
+public class CategoryVo {
+
+
+    private String id;
+
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态 0:启用 1:禁用
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 修改时间
+     */
+    private Date updateTime;
+}

+ 32 - 0
src/main/java/com/we/vo/DishVO.java

@@ -0,0 +1,32 @@
+package com.we.vo;
+
+// 方式2:用注解指定序列化为字符串(推荐,不影响数据库操作)
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.math.BigDecimal;
+
+/**
+ * 菜品展示对象
+ */
+@Data
+public class DishVO {
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+    private String dishName;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long categoryId;
+    private String categoryName;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long flavorId;
+    private String flavorName;
+    private String coreMaterial;
+    private BigDecimal price;
+    private Integer stock;
+    private Integer sales;
+    private String tasteFeature;
+    private String adaptScene;
+    private String coverImg;
+    private String description;
+    private Integer status;
+}

+ 0 - 3
src/main/resources/application.properties

@@ -1,3 +0,0 @@
-# 应用服务 WEB 访问端口
-server.port=8080
-

+ 17 - 0
src/main/resources/application.yml

@@ -0,0 +1,17 @@
+server:
+  port: 8086
+
+spring:
+  datasource:
+    url: jdbc:mysql://120.26.175.13:3306/orderFood?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true
+    username: root
+    password: TUhuyangche!
+    driver-class-name: com.mysql.cj.jdbc.Driver
+  # MyBatis-Plus配置(缩进修正,属于spring子节点)
+mybatis-plus:
+  mapper-locations: classpath:mapper/*.xml
+  type-aliases-package: com.zhentao.moudel.domain  # 若实际包名是module,建议修正
+  type-handlers-package: com.zhentao.utils
+  configuration:
+    map-underscore-to-camel-case: true  # 下划线转驼峰
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL日志

+ 19 - 0
src/main/resources/com/we/mapper/DishCategoryMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.we.mapper.DishCategoryMapper">
+
+    <resultMap id="BaseResultMap" type="com.we.domain.DishCategory">
+            <id property="id" column="id" />
+            <result property="categoryName" column="category_name" />
+            <result property="sort" column="sort" />
+            <result property="status" column="status" />
+            <result property="createTime" column="create_time" />
+            <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,category_name,sort,status,create_time,update_time
+    </sql>
+</mapper>

+ 33 - 0
src/main/resources/com/we/mapper/DishMapper.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.we.mapper.DishMapper">
+
+    <resultMap id="BaseResultMap" type="com.we.domain.Dish">
+            <id property="id" column="id" />
+            <result property="dishName" column="dish_name" />
+            <result property="categoryId" column="category_id" />
+            <result property="flavorId" column="flavor_id" />
+            <result property="coreMaterial" column="core_material" />
+            <result property="price" column="price" />
+            <result property="costPrice" column="cost_price" />
+            <result property="stock" column="stock" />
+            <result property="sales" column="sales" />
+            <result property="tasteFeature" column="taste_feature" />
+            <result property="adaptScene" column="adapt_scene" />
+            <result property="coverImg" column="cover_img" />
+            <result property="description" column="description" />
+            <result property="status" column="status" />
+            <result property="isDelete" column="is_delete" />
+            <result property="createTime" column="create_time" />
+            <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,dish_name,category_id,flavor_id,core_material,price,
+        cost_price,stock,sales,taste_feature,adapt_scene,
+        cover_img,description,status,is_delete,create_time,
+        update_time
+    </sql>
+</mapper>

+ 19 - 0
src/main/resources/com/we/mapper/FlaverMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.we.mapper.FlaverMapper">
+
+    <resultMap id="BaseResultMap" type="com.we.domain.Flaver">
+            <id property="id" column="id" />
+            <result property="flavorName" column="flavor_name" />
+            <result property="sort" column="sort" />
+            <result property="status" column="status" />
+            <result property="createTime" column="create_time" />
+            <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,flavor_name,sort,status,create_time,update_time
+    </sql>
+</mapper>

+ 0 - 6
src/main/resources/static/index.html

@@ -1,6 +0,0 @@
-<html>
-<body>
-<h1>hello word!!!</h1>
-<p>this is a html page</p>
-</body>
-</html>