Преглед изворни кода

餐饮模块部分功能开发(菜品分类、菜品库、包房管理、菜单管理)

徐滕 пре 2 недеља
родитељ
комит
abc3154828
55 измењених фајлова са 9153 додато и 0 уклоњено
  1. 415 0
      docs/构建与部署指南.md
  2. 159 0
      forge-admin-ui/src/api/restaurant.js
  3. 400 0
      forge-admin-ui/src/views/restaurant/dishLibrary.vue
  4. 993 0
      forge-admin-ui/src/views/restaurant/dishMenu.vue
  5. 557 0
      forge-admin-ui/src/views/restaurant/dishRoom.vue
  6. 173 0
      forge-admin-ui/src/views/restaurant/dishType.vue
  7. 117 0
      forge-h5-ui/HTTPS_SETUP.md
  8. 14 0
      forge-server/db/migration/V1.0.124__add_hotel_qrcode_scan_permission.sql
  9. 144 0
      forge-server/db/migration/V1.0.125__add_restaurant_dish_type_and_library.sql
  10. 3 0
      forge-server/db/migration/V1.0.126__fix_dish_library_sale_price_nullable.sql
  11. 85 0
      forge-server/db/migration/V1.0.127__add_restaurant_dish_room.sql
  12. 72 0
      forge-server/db/migration/V1.0.128__add_restaurant_dish_menu.sql
  13. 6 0
      forge-server/forge-admin-server/pom.xml
  14. 80 0
      forge-server/forge-business/forge-restaurant/pom.xml
  15. 859 0
      forge-server/forge-business/forge-restaurant/psi_credit_customer.sql
  16. 90 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishLibraryController.java
  17. 154 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishMenuController.java
  18. 81 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishRoomController.java
  19. 99 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishTypeController.java
  20. 71 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishLibrary.java
  21. 44 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishMenu.java
  22. 103 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishMenuDish.java
  23. 59 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishRoom.java
  24. 55 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishType.java
  25. 59 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishLibraryDTO.java
  26. 31 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishMenuAddDishDTO.java
  27. 25 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishMenuPriceDTO.java
  28. 34 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishRoomDTO.java
  29. 37 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishTypeDTO.java
  30. 78 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishLibraryMapper.java
  31. 120 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishMenuDishMapper.java
  32. 32 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishMenuMapper.java
  33. 42 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishRoomMapper.java
  34. 58 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishTypeMapper.java
  35. 69 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishLibraryService.java
  36. 81 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishMenuService.java
  37. 37 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishRoomService.java
  38. 75 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishTypeService.java
  39. 258 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishLibraryServiceImpl.java
  40. 356 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishMenuServiceImpl.java
  41. 173 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishRoomServiceImpl.java
  42. 275 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishTypeServiceImpl.java
  43. 75 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishLibraryVO.java
  44. 46 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuAvailableDishVO.java
  45. 76 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuDishVO.java
  46. 31 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuHistoryVO.java
  47. 38 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishRoomVO.java
  48. 44 0
      forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishTypeTreeVO.java
  49. 103 0
      forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishLibraryMapper.xml
  50. 162 0
      forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishMenuDishMapper.xml
  51. 28 0
      forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishMenuMapper.xml
  52. 67 0
      forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishRoomMapper.xml
  53. 51 0
      forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishTypeMapper.xml
  54. 1758 0
      forge-server/forge-business/forge-restaurant/餐饮模块迁移分析文档.md
  55. 1 0
      forge-server/forge-business/pom.xml

+ 415 - 0
docs/构建与部署指南.md

@@ -0,0 +1,415 @@
+# Forge Admin 构建与部署指南
+
+> 本文档涵盖 PC 前端、H5 移动端、后端三部分的构建与部署步骤。
+> 照着复制命令即可执行。
+
+---
+
+## 一、环境要求
+
+| 依赖 | 版本要求 | 用途 |
+|------|---------|------|
+| JDK | 17+ | 后端编译运行 |
+| Maven | 3.9+ | 后端构建 |
+| Node.js | 20+ | 前端构建 |
+| pnpm | 8+ | 前端包管理 |
+| MySQL | 8.0+ | 数据库 |
+| Redis | 6.0+ | 缓存 |
+| Nginx | 1.25+ | 前端静态资源 + API 反向代理 |
+
+---
+
+## 二、PC 前端(forge-admin-ui)
+
+### 2.1 开发模式
+
+```powershell
+cd D:\java_project\forge-admin\forge-admin-ui
+
+# 安装依赖(首次或依赖变更后执行)
+pnpm install
+
+# 启动开发服务器(默认 http://localhost:3000)
+pnpm dev
+```
+
+### 2.2 生产构建
+
+```powershell
+cd D:\java_project\forge-admin\forge-admin-ui
+
+# 生产构建(产物输出到 dist/ 目录)
+pnpm build
+```
+
+**构建产物:** `forge-admin-ui/dist/`
+
+**生产环境配置**(`.env.production`):
+
+| 配置项 | 值 | 含义 |
+|--------|-----|------|
+| `VITE_PUBLIC_PATH` | `/forge` | 静态资源部署路径 |
+| `VITE_REQUEST_PREFIX` | `/forge-api` | API 请求前缀 |
+
+### 2.3 部署到服务器
+
+```bash
+# 1. 将 dist/ 目录上传到服务器
+scp -r dist/ root@你的服务器:/usr/share/nginx/html/forge/
+
+# 2. 确认 Nginx 配置中有以下 location(见第五节 Nginx 完整配置)
+#    location /forge/ { ... }
+#    location /forge-api/ { ... }
+
+# 3. 重载 Nginx
+nginx -s reload
+```
+
+**访问地址:** `http://服务器IP/forge`
+**默认账号:** `admin` / `123456`
+
+---
+
+## 三、H5 移动端(forge-h5-ui)
+
+### 3.1 开发模式
+
+```powershell
+cd D:\java_project\forge-admin\forge-h5-ui
+
+# 安装依赖(首次或依赖变更后执行)
+pnpm install
+
+# 启动开发服务器(默认 http://localhost:3001)
+pnpm dev:h5
+```
+
+### 3.2 生产构建
+
+```powershell
+cd D:\java_project\forge-admin\forge-h5-ui
+
+# H5 生产构建(产物输出到 dist/build/h5/ 目录)
+pnpm build:h5
+```
+
+**构建产物:** `forge-h5-ui/dist/build/h5/`
+
+产物结构:
+```
+dist/build/h5/
+├── index.html      # 入口文件
+├── assets/         # JS/CSS 打包产物
+└── static/         # 静态资源
+```
+
+**生产环境配置**(`.env.production`):
+
+| 配置项 | 值 | 含义 |
+|--------|-----|------|
+| `VITE_PUBLIC_PATH` | `/forge-h5` | 静态资源部署路径 |
+| `VITE_REQUEST_PREFIX` | `/forge-h5-api` | API 请求前缀 |
+
+### 3.3 部署到服务器
+
+```bash
+# 1. 将 dist/build/h5/ 目录内容上传到服务器
+scp -r dist/build/h5/* root@你的服务器:/usr/share/nginx/forge-h5/
+
+# 2. 确认 Nginx 配置中有以下 location(见第五节 Nginx 完整配置)
+#    location /forge-h5/ { ... }
+#    location /forge-h5-api/ { ... }
+
+# 3. 重载 Nginx
+nginx -s reload
+```
+
+**访问地址:** `http://服务器IP/forge-h5`
+
+> **注意:** H5 移动端连接的是 `forge-app-server`(端口 8583),不是 admin-server(8580)。
+> 确保服务器上 app-server 已启动。
+
+---
+
+## 四、后端打 JAR 包
+
+### 4.1 一键打全部 JAR(推荐)
+
+```powershell
+cd D:\java_project\forge-admin\forge-server
+
+mvn clean package -DskipTests -Pprod
+```
+
+执行完后,4 个 JAR 包分别在这里:
+
+| 服务 | JAR 包路径 | 端口 |
+|------|-----------|------|
+| admin 主服务(PC 管理端 API) | `forge-admin-server/target/forge-admin-server.jar` | 8580 |
+| flow 流程服务(工作流) | `forge-flow/forge-flow-server/target/forge-flow-server.jar` | 8081 |
+| app 服务(H5 移动端 API) | `forge-app-server/target/forge-app-server.jar` | 8583 |
+| report 报表服务(数据大屏) | `forge-report-server/target/forge-report-server.jar` | 8084 |
+
+### 4.2 单独打某个 JAR
+
+```powershell
+cd D:\java_project\forge-admin\forge-server
+
+# 只打 admin 主服务(自动编译依赖的 framework、plugin 模块)
+mvn clean package -pl forge-admin-server -am -DskipTests -Pprod
+
+# 只打 flow 流程服务
+mvn clean package -pl forge-flow/forge-flow-server -am -DskipTests -Pprod
+
+# 只打 app 服务(H5 需要)
+mvn clean package -pl forge-app-server -am -DskipTests -Pprod
+
+# 只打 report 报表服务
+mvn clean package -pl forge-report-server -am -DskipTests -Pprod
+```
+
+> **参数说明:**
+> - `-pl` 指定要打包的模块
+> - `-am` 自动编译该模块依赖的其他模块(framework、plugin 等)
+> - `-DskipTests` 跳过单元测试,加快速度
+> - `-Pprod` 使用生产环境配置(日志级别 warn 等)
+
+### 4.3 用 JAR 包启动服务
+
+```powershell
+# 启动顺序:flow → admin → app(app 依赖 admin 的认证模块)
+
+# 1. flow 流程服务
+cd D:\java_project\forge-admin\forge-server
+java -jar forge-flow/forge-flow-server/target/forge-flow-server.jar --spring.profiles.active=dev
+
+# 2. admin 主服务
+java -jar forge-admin-server/target/forge-admin-server.jar --spring.profiles.active=dev
+
+# 3. app 服务(H5 移动端需要)
+java -jar forge-app-server/target/forge-app-server.jar --spring.profiles.active=dev
+
+# 4. report 报表服务(可选)
+java -jar forge-report-server/target/forge-report-server.jar --spring.profiles.active=dev
+```
+
+> 本地开发用 `--spring.profiles.active=dev`,服务器部署用 `--spring.profiles.active=prod`
+
+### 4.4 部署到服务器
+
+```bash
+# 1. 把 JAR 包上传到服务器
+scp forge-admin-server/target/forge-admin-server.jar root@服务器:/opt/forge/
+scp forge-flow/forge-flow-server/target/forge-flow-server.jar root@服务器:/opt/forge/
+scp forge-app-server/target/forge-app-server.jar root@服务器:/opt/forge/
+
+# 2. 在服务器上按顺序启动
+# 先启动 flow
+nohup java -Xms256m -Xmx512m \
+  -Dspring.profiles.active=prod \
+  -jar /opt/forge/forge-flow-server.jar \
+  > /opt/forge/logs/flow.log 2>&1 &
+
+# 再启动 admin
+nohup java -Xms256m -Xmx512m \
+  -Dspring.profiles.active=prod \
+  -jar /opt/forge/forge-admin-server.jar \
+  > /opt/forge/logs/admin.log 2>&1 &
+
+# 最后启动 app(H5 需要)
+nohup java -Xms256m -Xmx512m \
+  -Dspring.profiles.active=prod \
+  -jar /opt/forge/forge-app-server.jar \
+  > /opt/forge/logs/app.log 2>&1 &
+```
+
+**服务器配置文件:** 在 jar 同目录放 `application-prod.yml`,配置数据库和 Redis 连接信息。
+
+---
+
+## 五、Nginx 完整配置
+
+将以下内容写入 `/etc/nginx/conf.d/forge.conf`(或合并到 `nginx.conf`):
+
+```nginx
+server {
+    listen 80;
+    server_name localhost;
+
+    # ==================== PC 前端 ====================
+    location /forge/ {
+        alias   /usr/share/nginx/html/forge/;
+        index   index.html index.htm;
+        try_files $uri $uri/ /forge/index.html;
+    }
+
+    # ==================== H5 移动端前端 ====================
+    location /forge-h5/ {
+        alias   /usr/share/nginx/forge-h5/;
+        index   index.html;
+        try_files $uri $uri/ /forge-h5/index.html;
+    }
+
+    # ==================== H5 API 代理 → app-server:8583 ====================
+    location /forge-h5-api/ {
+        proxy_pass http://127.0.0.1:8583/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+
+    # ==================== 流程服务 API(路径更长,优先匹配) ====================
+    location /forge-api/api/flow/ {
+        proxy_send_timeout 3000;
+        proxy_read_timeout 3000;
+        proxy_connect_timeout 3000;
+        proxy_pass http://127.0.0.1:8081/api/flow/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+    # ==================== PC API 代理 → admin-server:8580 ====================
+    location /forge-api/ {
+        proxy_send_timeout 3000;
+        proxy_read_timeout 3000;
+        proxy_connect_timeout 3000;
+        proxy_pass http://127.0.0.1:8580/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+    # ==================== 通用配置 ====================
+    client_max_body_size 20m;
+
+    gzip on;
+    gzip_types text/plain application/json application/javascript text/css;
+    gzip_min_length 1000;
+}
+```
+
+### 请求链路图
+
+```
+PC 浏览器
+  http://服务器/forge/          → Nginx → /usr/share/nginx/html/forge/(静态文件)
+  http://服务器/forge-api/xxx   → Nginx → admin-server:8580/xxx
+
+H5 手机浏览器
+  http://服务器/forge-h5/        → Nginx → /usr/share/nginx/forge-h5/(静态文件)
+  http://服务器/forge-h5-api/xxx → Nginx → app-server:8583/xxx
+
+流程服务(PC 和 H5 共用)
+  http://服务器/forge-api/api/flow/xxx → Nginx → flow-server:8081/api/flow/xxx
+```
+
+---
+
+## 六、Docker 一键部署(可选)
+
+如果服务器上有 Docker,可以用 docker-compose 一键部署:
+
+```bash
+cd /mnt/project/forge_admin/docker
+
+# 复制环境变量模板
+cp .env.example .env
+
+# 编辑 .env 修改数据库密码等配置
+vim .env
+
+# 一键构建并启动(首次约 15-20 分钟)
+docker compose up -d --build
+
+# 查看状态
+docker compose ps
+
+# 查看日志
+docker compose logs -f
+```
+
+> **注意:** 当前 Docker 配置只包含 PC 前端和 admin/flow 服务,H5 前端和 app-server 需要手动添加(见下方扩展说明)。
+
+### Docker 扩展:加入 H5 前端
+
+修改 `docker/Dockerfile.ui`:
+
+```dockerfile
+FROM node:20-alpine AS builder
+WORKDIR /app
+
+# PC 前端
+COPY forge-admin-ui/package.json forge-admin-ui/pnpm-lock.yaml ./pc/
+RUN npm install -g pnpm@8 && cd pc && pnpm install
+COPY forge-admin-ui/ ./pc/
+RUN cd pc && pnpm build
+
+# H5 移动端
+COPY forge-h5-ui/package.json forge-h5-ui/pnpm-lock.yaml ./h5/
+RUN cd h5 && pnpm install
+COPY forge-h5-ui/ ./h5/
+RUN cd h5 && pnpm build:h5
+
+FROM nginx:1.25-alpine
+COPY --from=builder /app/pc/dist /usr/share/nginx/html/forge
+COPY --from=builder /app/h5/dist/build/h5 /usr/share/nginx/html/forge-h5
+COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
+```
+
+---
+
+## 七、快速命令速查表
+
+| 操作 | 命令 |
+|------|------|
+| **PC 前端开发** | `cd forge-admin-ui && pnpm dev` |
+| **PC 前端构建** | `cd forge-admin-ui && pnpm build` |
+| **H5 前端开发** | `cd forge-h5-ui && pnpm dev:h5` |
+| **H5 前端构建** | `cd forge-h5-ui && pnpm build:h5` |
+| **后端全量构建** | `cd forge-server && mvn clean install -DskipTests` |
+| **后端单独构建 admin** | `cd forge-server && mvn clean package -pl forge-admin-server -am -DskipTests -Pprod` |
+| **后端单独构建 flow** | `cd forge-server && mvn clean package -pl forge-flow/forge-flow-server -am -DskipTests -Pprod` |
+| **后端单独构建 app** | `cd forge-server && mvn clean package -pl forge-app-server -am -DskipTests -Pprod` |
+| **启动 admin 服务** | `cd forge-server/forge-admin-server && mvn spring-boot:run` |
+| **启动 flow 服务** | `cd forge-server/forge-flow && mvn spring-boot:run` |
+| **启动 app 服务** | `cd forge-server/forge-app-server && mvn spring-boot:run` |
+| **Docker 一键部署** | `cd docker && docker compose up -d --build` |
+| **Nginx 重载配置** | `nginx -s reload` |
+| **检查 Nginx 配置** | `nginx -t` |
+
+---
+
+## 八、常见问题
+
+### Q1: 前端构建报内存不足
+```powershell
+# PC 前端构建时增大 Node 内存
+$env:NODE_OPTIONS="--max-old-space-size=4096"
+pnpm build
+```
+
+### Q2: H5 构建后页面白屏
+- 检查 Nginx 的 `alias` 路径是否指向 `dist/build/h5/` 目录
+- 检查浏览器控制台是否有 404 错误(静态资源路径不对)
+- 确认 `.env.production` 中 `VITE_PUBLIC_PATH=/forge-h5` 与 Nginx location 一致
+
+### Q3: API 请求 404/502
+- 确认后端服务已启动(`curl http://127.0.0.1:8580/` 测试)
+- 确认 Nginx `proxy_pass` 地址和端口正确
+- 检查防火墙是否放行了后端端口
+
+### Q4: H5 页面能打开但接口不通
+- H5 连接的是 app-server(8583),不是 admin-server(8580)
+- 确认 app-server 已启动
+- 确认 Nginx `/forge-h5-api/` 代理到 `127.0.0.1:8583`
+
+### Q5: 后端启动报数据库连接失败
+- 检查 `application-dev.yml` 或 `application-prod.yml` 中的数据库配置
+- 确认 MySQL 已启动且可访问
+- 确认数据库 `forge_admin` 已创建

+ 159 - 0
forge-admin-ui/src/api/restaurant.js

@@ -0,0 +1,159 @@
+import { request } from '@/utils'
+
+// ==================== 菜品分类管理 ====================
+
+/** 查询分类树(仅启用,用于左侧导航和下拉) */
+export function getDishTypeTree() {
+  return request.get('/restaurant/dishType/tree')
+}
+
+/** 查询分类列表(平铺,支持名称和状态筛选) */
+export function getDishTypeList(params) {
+  return request.get('/restaurant/dishType/list', { params })
+}
+
+/** 查询分类详情 */
+export function getDishTypeDetail(id) {
+  return request.get('/restaurant/dishType/detail', { params: { id } })
+}
+
+/** 新增分类 */
+export function createDishType(data) {
+  return request.post('/restaurant/dishType', data)
+}
+
+/** 修改分类 */
+export function updateDishType(data) {
+  return request.put('/restaurant/dishType', data)
+}
+
+/** 删除分类 */
+export function deleteDishType(id) {
+  return request.post('/restaurant/dishType/remove', null, { params: { id } })
+}
+
+/** 切换分类状态 */
+export function toggleDishTypeStatus(id) {
+  return request.post('/restaurant/dishType/toggleStatus', null, { params: { id } })
+}
+
+// ==================== 菜品库管理 ====================
+
+/** 分页查询菜品 */
+export function getDishPage(params) {
+  return request.get('/restaurant/dish/page', { params })
+}
+
+/** 查询菜品详情 */
+export function getDishDetail(id) {
+  return request.get('/restaurant/dish/detail', { params: { id } })
+}
+
+/** 新增菜品 */
+export function createDish(data) {
+  return request.post('/restaurant/dish', data)
+}
+
+/** 修改菜品 */
+export function updateDish(data) {
+  return request.put('/restaurant/dish', data)
+}
+
+/** 删除菜品 */
+export function deleteDish(id) {
+  return request.post('/restaurant/dish/remove', null, { params: { id } })
+}
+
+/** 切换菜品状态 */
+export function toggleDishStatus(id) {
+  return request.post('/restaurant/dish/toggleStatus', null, { params: { id } })
+}
+
+// ==================== 包房管理 ====================
+
+/** 查询包房列表(不分页) */
+export function getDishRoomList(params) {
+  return request.get('/restaurant/dishRoom/list', { params })
+}
+
+/** 查询包房详情 */
+export function getDishRoomDetail(id) {
+  return request.get('/restaurant/dishRoom/detail', { params: { id } })
+}
+
+/** 保存包房(新增/修改) */
+export function saveDishRoom(data) {
+  return request.post('/restaurant/dishRoom/save', data)
+}
+
+/** 删除包房 */
+export function deleteDishRoom(id) {
+  return request.post('/restaurant/dishRoom/remove', null, { params: { id } })
+}
+
+/** 切换包房状态 */
+export function toggleDishRoomStatus(id) {
+  return request.post('/restaurant/dishRoom/toggleStatus', null, { params: { id } })
+}
+
+// ==================== 菜单管理 ====================
+
+/** 查询当前菜单全部菜品(不分页,树形表格用) */
+export function getMenuAllDishList() {
+  return request.get('/restaurant/dishMenu/allDishList')
+}
+
+/** 查询当前菜单菜品列表(分页) */
+export function getMenuDishList(params) {
+  return request.get('/restaurant/dishMenu/list', { params })
+}
+
+/** 查询历史菜单列表(分页) */
+export function getMenuHistoryList(params) {
+  return request.get('/restaurant/dishMenu/historyList', { params })
+}
+
+/** 查询历史菜单菜品详情(分页) */
+export function getMenuHistoryDetailList(params) {
+  return request.get('/restaurant/dishMenu/historyDetailList', { params })
+}
+
+/** 查询可添加菜品列表 */
+export function getMenuAvailableDishList(dishName) {
+  return request.get('/restaurant/dishMenu/availableDishList', { params: { dishName } })
+}
+
+/** 查询点单菜品列表 */
+export function getMenuOrderDishList(params) {
+  return request.get('/restaurant/dishMenu/orderDishList', { params })
+}
+
+/** 查询点单分类列表 */
+export function getMenuOrderTypeList() {
+  return request.get('/restaurant/dishMenu/orderTypeList')
+}
+
+/** 添加菜品到菜单 */
+export function addMenuDishes(data) {
+  return request.post('/restaurant/dishMenu/addDishes', data)
+}
+
+/** 全量保存菜单菜品 */
+export function saveMenuDishes(data) {
+  return request.post('/restaurant/dishMenu/saveDishes', data)
+}
+
+/** 修改菜单菜品价格 */
+export function updateMenuPrice(data) {
+  return request.post('/restaurant/dishMenu/updatePrice', data)
+}
+
+/** 删除菜单菜品 */
+export function deleteMenuDish(id) {
+  return request.post('/restaurant/dishMenu/remove', null, { params: { id } })
+}
+
+/** 更新菜单(归档旧版本) */
+export function updateMenu(remark) {
+  return request.post('/restaurant/dishMenu/updateMenu', null, { params: { remark } })
+}

+ 400 - 0
forge-admin-ui/src/views/restaurant/dishLibrary.vue

@@ -0,0 +1,400 @@
+<template>
+  <div class="restaurant-dish-library-page">
+    <!-- 左侧分类树 -->
+    <div class="dish-tree-panel">
+      <div class="dish-tree-header">
+        <span>菜品分类</span>
+        <n-button text size="tiny" @click="clearTreeSelect">
+          全部
+        </n-button>
+      </div>
+      <n-spin :show="treeLoading">
+        <n-tree
+          :data="treeData"
+          key-field="id"
+          label-field="name"
+          children-field="children"
+          block-line
+          :selected-keys="selectedKeys"
+          default-expand-all
+          @update:selected-keys="handleTreeSelect"
+        />
+      </n-spin>
+    </div>
+
+    <!-- 右侧菜品列表 -->
+    <div class="dish-crud-panel">
+      <AiCrudPage
+        ref="crudRef"
+        api="/restaurant/dish"
+        :api-config="{
+          list: 'get@/restaurant/dish/page',
+          detail: 'get@/restaurant/dish/detail?id=:id',
+          add: 'post@/restaurant/dish',
+          update: 'put@/restaurant/dish',
+          delete: 'post@/restaurant/dish/remove?id=:id',
+        }"
+        :load-detail-on-edit="true"
+        :public-params="publicParams"
+        :search-schema="searchSchema"
+        :columns="tableColumns"
+        :edit-schema="editSchema"
+        :before-submit="beforeSubmit"
+        :before-render-detail="beforeRenderDetail"
+        row-key="id"
+        add-button-text="新增菜品"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { NTag } from 'naive-ui'
+import { computed, h, onMounted, ref } from 'vue'
+import { getDishTypeTree, toggleDishStatus } from '@/api/restaurant'
+import { AiCrudPage } from '@/components/ai-form'
+import AuthImage from '@/components/common/AuthImage.vue'
+
+defineOptions({ name: 'RestaurantDishLibrary' })
+
+const crudRef = ref(null)
+
+// ==================== 左侧分类树 ====================
+
+const treeLoading = ref(false)
+const treeData = ref([])
+const selectedKeys = ref([])
+const selectedTypeId = ref(null)
+
+async function loadTreeData() {
+  treeLoading.value = true
+  try {
+    const res = await getDishTypeTree()
+    if (res.code === 200) {
+      treeData.value = res.data || []
+    }
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    treeLoading.value = false
+  }
+}
+
+function handleTreeSelect(keys) {
+  selectedKeys.value = keys
+  selectedTypeId.value = keys.length > 0 ? keys[0] : null
+}
+
+function clearTreeSelect() {
+  selectedKeys.value = []
+  selectedTypeId.value = null
+}
+
+onMounted(() => {
+  loadTreeData()
+})
+
+// 动态公共参数:当选中分类变化时自动触发列表刷新
+const publicParams = computed(() => {
+  const params = {}
+  if (selectedTypeId.value !== null) {
+    params.typeId = selectedTypeId.value
+  }
+  return params
+})
+
+// ==================== 状态与分类选项 ====================
+
+const statusColorMap = {
+  ENABLED: 'success',
+  DISABLED: 'default',
+}
+const statusLabelMap = {
+  ENABLED: '启用',
+  DISABLED: '禁用',
+}
+
+// 分类选项(从树形数据扁平化,用于搜索和编辑表单)
+const typeOptions = computed(() => {
+  const options = []
+  function flatten(nodes, level) {
+    for (let i = 0; i < nodes.length; i++) {
+      const node = nodes[i]
+      const prefix = level > 0 ? `${' '.repeat(level)}└ ` : ''
+      options.push({ label: `${prefix}${node.name}`, value: node.id })
+      if (node.children && node.children.length > 0) {
+        flatten(node.children, level + 1)
+      }
+    }
+  }
+  flatten(treeData.value, 0)
+  return options
+})
+
+const dishStatusOptions = [
+  { label: '启用', value: 'ENABLED' },
+  { label: '禁用', value: 'DISABLED' },
+]
+
+// ==================== 搜索表单配置 ====================
+
+const searchSchema = computed(() => [
+  {
+    field: 'dishName',
+    label: '菜品名称',
+    type: 'input',
+    props: { placeholder: '请输入菜品名称' },
+  },
+  {
+    field: 'dishCode',
+    label: '菜品编码',
+    type: 'input',
+    props: { placeholder: '请输入菜品编码' },
+  },
+  {
+    field: 'typeId',
+    label: '分类',
+    type: 'select',
+    props: { placeholder: '请选择分类', options: typeOptions.value, clearable: true },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    type: 'select',
+    props: { placeholder: '请选择状态', options: dishStatusOptions, clearable: true },
+  },
+])
+
+// ==================== 表格列配置 ====================
+
+/** 分转元(展示用) */
+function formatPrice(priceInCents) {
+  if (priceInCents === null || priceInCents === undefined)
+    return '-'
+  return (priceInCents / 100).toFixed(2)
+}
+
+const tableColumns = computed(() => [
+  { prop: 'dishCode', label: '菜品编码', width: 150 },
+  { prop: 'dishName', label: '菜品名称', minWidth: 160 },
+  {
+    prop: 'image',
+    label: '图片',
+    width: 80,
+    render: (row) => {
+      if (!row.image)
+        return '-'
+      return h(AuthImage, {
+        src: row.image,
+        style: 'width: 36px; height: 36px; border-radius: 4px; object-fit: cover;',
+      })
+    },
+  },
+  { prop: 'typeName', label: '分类', width: 120 },
+  { prop: 'unit', label: '单位', width: 70 },
+  { prop: 'spec', label: '规格', width: 80 },
+  {
+    prop: 'salePrice',
+    label: '销售价(元)',
+    width: 100,
+    render: row => formatPrice(row.salePrice),
+  },
+  {
+    prop: 'costPrice',
+    label: '成本价(元)',
+    width: 100,
+    render: row => formatPrice(row.costPrice),
+  },
+  { prop: 'taste', label: '口味', width: 80 },
+  {
+    prop: 'status',
+    label: '状态',
+    width: 90,
+    render: row => h(NTag, { type: statusColorMap[row.status] || 'default', size: 'small' }, { default: () => statusLabelMap[row.status] || row.status }),
+  },
+  { prop: 'sort', label: '排序', width: 70 },
+  { prop: 'createTime', label: '创建时间', width: 172 },
+  {
+    label: '操作',
+    width: 200,
+    render: (row) => {
+      const toggleText = row.status === 'ENABLED' ? '禁用' : '启用'
+      const toggleType = row.status === 'ENABLED' ? 'text-warning' : 'text-success'
+      return h('div', { class: 'flex gap-3' }, [
+        h('a', {
+          class: 'text-primary cursor-pointer hover:opacity-80',
+          onClick: () => crudRef.value?.handleEdit(row),
+        }, '编辑'),
+        h('a', {
+          class: `${toggleType} cursor-pointer hover:opacity-80`,
+          onClick: async () => {
+            await toggleDishStatus(row.id)
+            crudRef.value?.refresh()
+          },
+        }, toggleText),
+        h('a', {
+          class: 'text-error cursor-pointer hover:opacity-80',
+          onClick: () => crudRef.value?.handleDelete(row),
+        }, '删除'),
+      ])
+    },
+  },
+])
+
+// ==================== 编辑表单配置 ====================
+
+const editSchema = computed(() => [
+  {
+    field: 'dishName',
+    label: '菜品名称',
+    type: 'input',
+    rules: [{ required: true, message: '请输入菜品名称', trigger: 'blur' }],
+    props: { placeholder: '请输入菜品名称' },
+  },
+  {
+    field: 'typeId',
+    label: '菜品分类',
+    type: 'select',
+    rules: [{ required: true, message: '请选择菜品分类', trigger: 'change' }],
+    props: { placeholder: '请选择最子级分类', options: typeOptions.value },
+  },
+  {
+    field: 'image',
+    label: '菜品图片',
+    type: 'imageUpload',
+    businessType: 'restaurant-dish-image',
+    limit: 1,
+    fileSize: 5,
+    fileType: ['png', 'jpg', 'jpeg', 'webp'],
+    valueType: 'string',
+    props: { showTip: true },
+  },
+  {
+    field: 'unit',
+    label: '单位',
+    type: 'input',
+    props: { placeholder: '如:份、碗、杯' },
+  },
+  {
+    field: 'spec',
+    label: '规格',
+    type: 'input',
+    props: { placeholder: '如:大份、小份' },
+  },
+  {
+    field: 'salePrice',
+    label: '销售价(元)',
+    type: 'inputNumber',
+    props: { placeholder: '请输入销售价', min: 0, step: 1, precision: 2 },
+  },
+  {
+    field: 'costPrice',
+    label: '成本价(元)',
+    type: 'inputNumber',
+    props: { placeholder: '请输入成本价', min: 0, step: 1, precision: 2 },
+  },
+  {
+    field: 'taste',
+    label: '口味',
+    type: 'input',
+    props: { placeholder: '如:微辣、酸甜' },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    type: 'select',
+    defaultValue: 'ENABLED',
+    props: { placeholder: '请选择状态', options: dishStatusOptions },
+  },
+  {
+    field: 'sort',
+    label: '排序',
+    type: 'inputNumber',
+    defaultValue: 0,
+    props: { min: 0, step: 1, precision: 0 },
+  },
+  {
+    field: 'remark',
+    label: '备注',
+    type: 'textarea',
+    span: 2,
+    props: { placeholder: '请输入备注', rows: 3 },
+  },
+])
+
+// ==================== 提交前钩子:元转分 ====================
+
+function beforeSubmit(data) {
+  // 销售价:元 -> 分
+  if (data.salePrice !== null && data.salePrice !== undefined) {
+    data.salePrice = Math.round(data.salePrice * 100)
+  }
+  // 成本价:元 -> 分
+  if (data.costPrice !== null && data.costPrice !== undefined) {
+    data.costPrice = Math.round(data.costPrice * 100)
+  }
+  return data
+}
+
+// ==================== 详情渲染前钩子:分转元 ====================
+
+function beforeRenderDetail(data) {
+  if (!data)
+    return data
+  // 销售价:分 -> 元(0 视为未填写,保持空)
+  if (data.salePrice !== null && data.salePrice !== undefined && data.salePrice !== 0) {
+    data.salePrice = data.salePrice / 100
+  }
+  else {
+    data.salePrice = null
+  }
+  // 成本价:分 -> 元(0 视为未填写,保持空)
+  if (data.costPrice !== null && data.costPrice !== undefined && data.costPrice !== 0) {
+    data.costPrice = data.costPrice / 100
+  }
+  else {
+    data.costPrice = null
+  }
+  return data
+}
+</script>
+
+<style scoped>
+.restaurant-dish-library-page {
+  display: flex;
+  height: 100%;
+  gap: 0;
+}
+
+.dish-tree-panel {
+  width: 220px;
+  flex-shrink: 0;
+  border-right: 1px solid #e5e7eb;
+  padding: 12px 8px;
+  overflow-y: auto;
+  background: #fafafa;
+}
+
+.dish-tree-panel :deep(.n-tree-node-switcher) {
+  display: none;
+}
+
+.dish-tree-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  font-weight: 600;
+  color: #374151;
+  padding: 0 6px 10px;
+  border-bottom: 1px solid #f3f4f6;
+  margin-bottom: 8px;
+}
+
+.dish-crud-panel {
+  flex: 1;
+  overflow: hidden;
+}
+</style>

+ 993 - 0
forge-admin-ui/src/views/restaurant/dishMenu.vue

@@ -0,0 +1,993 @@
+<template>
+  <div class="dish-menu-page">
+    <div class="page-layout">
+      <!-- 左侧分类树 -->
+      <div class="menu-tree-panel">
+        <div class="menu-tree-header">
+          <span>菜品分类</span>
+          <NButton text size="tiny" @click="clearTreeSelect">
+            全部
+          </NButton>
+        </div>
+        <NSpin :show="treeLoading">
+          <NTree
+            :data="typeTreeData"
+            key-field="id"
+            label-field="name"
+            children-field="children"
+            block-line
+            :selected-keys="selectedKeys"
+            default-expand-all
+            @update:selected-keys="handleTreeSelect"
+          />
+        </NSpin>
+      </div>
+
+      <!-- 右侧内容区 -->
+      <div class="menu-content-panel">
+        <div class="tabs-wrapper">
+          <NTabs v-model:value="activeTab" type="line" @update:value="handleTabChange">
+            <!-- ==================== 当前菜单 ==================== -->
+            <NTabPane name="current" tab="当前菜单">
+              <AiCrudPage
+                ref="crudRef"
+                api="/restaurant/dishMenu"
+                :api-config="{
+                  list: 'get@/restaurant/dishMenu/list',
+                  delete: 'post@/restaurant/dishMenu/remove?id=:id',
+                }"
+                :search-schema="searchSchema"
+                :columns="tableColumns"
+                :public-params="publicParams"
+                :page-size="20"
+                hide-add
+                add-button-text="新增菜品"
+                @custom-action="handleCustomAction"
+              >
+                <template #toolbar>
+                  <div class="ai-crud-toolbar">
+                    <div class="toolbar-left" style="display: flex; gap: 12px;">
+                      <NButton type="primary" size="small" @click="openAddDishDialog">
+                        <template #icon>
+                          <NIcon><Add /></NIcon>
+                        </template>
+                        新增菜品
+                      </NButton>
+                      <NButton type="primary" size="small" @click="handleUpdateMenu">
+                        <template #icon>
+                          <NIcon><RefreshOutline /></NIcon>
+                        </template>
+                        更新
+                      </NButton>
+                      <NButton
+                        type="error"
+                        size="small"
+                        :disabled="checkedRowKeys.length === 0"
+                        @click="handleBatchDelete"
+                      >
+                        <template #icon>
+                          <NIcon><TrashOutline /></NIcon>
+                        </template>
+                        批量删除
+                      </NButton>
+                    </div>
+                  </div>
+                </template>
+              </AiCrudPage>
+            </NTabPane>
+
+            <!-- ==================== 历史菜单 ==================== -->
+            <NTabPane name="history" tab="历史菜单">
+              <NSpin :show="historyLoading">
+                <NDataTable
+                  :columns="historyColumns"
+                  :data="historyList"
+                  :row-key="row => row.id"
+                  :bordered="true"
+                  :single-line="false"
+                  :pagination="{ pageSize: 10 }"
+                />
+              </NSpin>
+            </NTabPane>
+          </NTabs>
+        </div>
+      </div>
+    </div>
+
+    <!-- ==================== 添加菜品弹窗(穿梭框) ==================== -->
+    <NModal
+      v-model:show="addDishVisible"
+      preset="card"
+      title="添加菜品"
+      style="width: 900px; max-width: 90vw;"
+      :segmented="{ content: true, footer: 'soft' }"
+    >
+      <div class="transfer-layout">
+        <!-- 左侧:可添加菜品 -->
+        <div class="transfer-panel">
+          <div class="transfer-header">
+            <span>可添加菜品</span>
+            <span class="transfer-count">
+              已选 {{ leftCheckedKeys.length }}/{{ leftTotalCount }} 项
+              <a class="cursor-pointer text-primary" @click="toggleLeftCheckAll">全选</a>
+            </span>
+          </div>
+          <NInput
+            v-model:value="leftSearchText"
+            placeholder=""
+            clearable
+            size="small"
+            class="transfer-search"
+            @clear="loadAvailableDishes"
+            @keyup.enter="loadAvailableDishes"
+          />
+          <div class="transfer-tree">
+            <NTree
+              :data="leftTreeData"
+              :checked-keys="leftCheckedKeys"
+              :default-expand-all="true"
+              checkable
+              cascade
+              key-field="id"
+              label-field="name"
+              children-field="children"
+              @update:checked-keys="onLeftCheckedChange"
+            />
+          </div>
+        </div>
+
+        <!-- 中间按钮 -->
+        <div class="transfer-buttons">
+          <NButton type="primary" :disabled="leftCheckedKeys.length === 0" @click="handleTransferRight">
+            &gt; 添加
+          </NButton>
+          <NButton type="primary" :disabled="rightCheckedKeys.length === 0" @click="handleTransferLeft">
+            &lt; 移出
+          </NButton>
+        </div>
+
+        <!-- 右侧:已选菜品 -->
+        <div class="transfer-panel">
+          <div class="transfer-header">
+            <span>已选菜品</span>
+            <span class="transfer-count">
+              已选 {{ rightCheckedKeys.length }}/{{ rightTotalCount }} 项
+              <a class="cursor-pointer text-primary" @click="toggleRightCheckAll">全选</a>
+            </span>
+          </div>
+          <NInput
+            v-model:value="rightSearchText"
+            placeholder=""
+            clearable
+            size="small"
+            class="transfer-search"
+          />
+          <div class="transfer-tree">
+            <NTree
+              :data="rightTreeData"
+              :checked-keys="rightCheckedKeys"
+              :default-expand-all="true"
+              checkable
+              cascade
+              key-field="id"
+              label-field="name"
+              children-field="children"
+              @update:checked-keys="onRightCheckedChange"
+            />
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div style="display: flex; justify-content: flex-end; gap: 8px;">
+          <NButton @click="addDishVisible = false">
+            取消
+          </NButton>
+          <NButton type="primary" @click="confirmAddDishes">
+            确定
+          </NButton>
+        </div>
+      </template>
+    </NModal>
+
+    <!-- ==================== 修改价格弹窗 ==================== -->
+    <NModal
+      v-model:show="priceVisible"
+      preset="dialog"
+      title="修改菜单价格"
+      :show-icon="false"
+      style="width: 400px;"
+    >
+      <NForm>
+        <NFormItem label="菜品名称">
+          <span>{{ priceEditRow.dishName }}</span>
+        </NFormItem>
+        <NFormItem label="菜品库价格">
+          <span>{{ formatPrice(priceEditRow.librarySalePrice) }} 元</span>
+        </NFormItem>
+        <NFormItem label="菜单价格(元)">
+          <NInputNumber
+            v-model:value="priceEditValue"
+            :precision="2"
+            :min="0"
+            placeholder="输入菜单价格"
+            style="width: 100%;"
+          />
+        </NFormItem>
+      </NForm>
+      <template #action>
+        <NButton @click="priceVisible = false">
+          取消
+        </NButton>
+        <NButton type="primary" @click="submitUpdatePrice">
+          确定
+        </NButton>
+      </template>
+    </NModal>
+
+    <!-- ==================== 历史详情弹窗 ==================== -->
+    <NModal
+      v-model:show="historyDetailVisible"
+      preset="card"
+      title="历史菜单详情"
+      style="width: 1200px; max-width: 95vw;"
+    >
+      <NSpin :show="historyDetailLoading">
+        <NDataTable
+          :columns="historyDetailColumns"
+          :data="historyDetailList"
+          :row-key="row => row.id"
+          :bordered="true"
+          :single-line="false"
+          :pagination="{ pageSize: 20 }"
+        />
+      </NSpin>
+    </NModal>
+  </div>
+</template>
+
+<script setup>
+import { Add, RefreshOutline, TrashOutline } from '@vicons/ionicons5'
+import { NButton, NDataTable, NForm, NFormItem, NIcon, NInput, NInputNumber, NModal, NSpin, NTabPane, NTabs, NTag, NTree, useDialog, useMessage } from 'naive-ui'
+import { computed, h, onMounted, ref } from 'vue'
+import {
+  deleteMenuDish,
+  getDishTypeTree,
+  getMenuAllDishList,
+  getMenuAvailableDishList,
+  getMenuHistoryDetailList,
+  getMenuHistoryList,
+  saveMenuDishes,
+  updateMenu,
+  updateMenuPrice,
+} from '@/api/restaurant'
+import { AiCrudPage } from '@/components/ai-form'
+import AuthImage from '@/components/common/AuthImage.vue'
+
+defineOptions({ name: 'RestaurantDishMenu' })
+
+const message = useMessage()
+const dialog = useDialog()
+const crudRef = ref(null)
+
+// ==================== 状态 ====================
+const activeTab = ref('current')
+const checkedRowKeys = ref([])
+
+// 分类树筛选
+const typeSearchText = ref('')
+const selectedKeys = ref([])
+const selectedTypeId = ref(null)
+const treeLoading = ref(false)
+const typeTreeData = ref([])
+
+// 搜索表单配置
+const statusOptions = [
+  { label: '启用', value: 'ENABLED' },
+  { label: '禁用', value: 'DISABLED' },
+]
+
+const searchSchema = computed(() => [
+  {
+    field: 'dishName',
+    label: '菜品名称',
+    type: 'input',
+    props: { placeholder: '请输入菜品名称' },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    type: 'select',
+    props: { placeholder: '请选择状态', options: statusOptions, clearable: true },
+  },
+])
+
+// 动态公共参数
+const publicParams = computed(() => {
+  const params = {}
+  if (selectedTypeId.value !== null) {
+    params.typeId = selectedTypeId.value
+  }
+  return params
+})
+
+// 历史菜单
+const historyLoading = ref(false)
+const historyList = ref([])
+
+// 添加菜品弹窗(穿梭框)
+const addDishVisible = ref(false)
+const leftSearchText = ref('')
+const rightSearchText = ref('')
+const leftTreeData = ref([])
+const rightTreeData = ref([])
+const leftCheckedKeys = ref([])
+const rightCheckedKeys = ref([])
+const allAvailableDishes = ref([])
+const currentMenuDishIds = ref(new Set())
+
+// 修改价格弹窗
+const priceVisible = ref(false)
+const priceEditRow = ref({})
+const priceEditValue = ref(0)
+
+// 历史详情弹窗
+const historyDetailVisible = ref(false)
+const historyDetailLoading = ref(false)
+const historyDetailList = ref([])
+
+// ==================== 工具方法 ====================
+function formatPrice(price) {
+  if (price == null)
+    return '-'
+  return (price / 100).toFixed(2)
+}
+
+// ==================== 分类树 ====================
+async function loadTypeTree() {
+  treeLoading.value = true
+  try {
+    const res = await getDishTypeTree()
+    if (res.code === 200) {
+      typeTreeData.value = res.data || []
+    }
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    treeLoading.value = false
+  }
+}
+
+function handleTreeSelect(keys) {
+  selectedKeys.value = keys
+  selectedTypeId.value = keys.length > 0 ? keys[0] : null
+  // publicParams 变化会自动触发 AiCrudPage 的 watch 刷新列表,无需手动 refresh
+}
+
+function handleTypeSearch() {
+  crudRef.value?.refresh()
+}
+
+function clearTreeSelect() {
+  selectedKeys.value = []
+  selectedTypeId.value = null
+  crudRef.value?.refresh()
+}
+
+// ==================== 表格列 ====================
+const statusColorMap = {
+  ENABLED: 'success',
+  DISABLED: 'default',
+}
+const statusLabelMap = {
+  ENABLED: '启用',
+  DISABLED: '禁用',
+}
+
+const tableColumns = computed(() => [
+  { prop: '_index', label: '序号', width: 60, render: (row, rowIndex) => rowIndex + 1 },
+  { prop: 'dishName', label: '菜品名称', width: 150, render: row => row.dishName || '-' },
+  {
+    prop: 'image',
+    label: '菜品图片',
+    width: 80,
+    render: (row) => {
+      if (!row.image)
+        return '-'
+      return h(AuthImage, {
+        src: row.image,
+        style: 'width: 40px; height: 40px; border-radius: 4px; object-fit: cover;',
+      })
+    },
+  },
+  { prop: 'dishCode', label: '菜品编码', width: 150, render: row => row.dishCode || '-' },
+  { prop: 'typeName', label: '菜品分类', width: 100, render: row => row.typeName || '-' },
+  { prop: 'unit', label: '单位', width: 60, render: row => row.unit || '-' },
+  {
+    prop: 'librarySalePrice',
+    label: '菜品库价格',
+    width: 100,
+    render: row => row.librarySalePrice != null ? formatPrice(row.librarySalePrice) : '-',
+  },
+  {
+    prop: 'salePrice',
+    label: '菜单价格',
+    width: 100,
+    render: row => row.salePrice != null ? formatPrice(row.salePrice) : '-',
+  },
+  {
+    prop: 'status',
+    label: '状态',
+    width: 80,
+    render: row => h(NTag, {
+      type: statusColorMap[row.status] || 'default',
+      size: 'small',
+    }, { default: () => statusLabelMap[row.status] || '-' }),
+  },
+  { prop: 'createTime', label: '创建时间', width: 170, render: row => row.createTime || '-' },
+  {
+    prop: 'action',
+    label: '操作',
+    width: 140,
+    fixed: 'right',
+    render: (row) => {
+      return h('div', { class: 'flex gap-3' }, [
+        h('a', {
+          class: 'text-primary cursor-pointer hover:opacity-80',
+          onClick: () => handleEditPrice(row),
+        }, '修改价格'),
+        h('a', {
+          class: 'text-error cursor-pointer hover:opacity-80',
+          onClick: () => handleDeleteDish(row),
+        }, '删除'),
+      ])
+    },
+  },
+])
+
+// ==================== 自定义操作处理 ====================
+function handleCustomAction({ action, row }) {
+  if (action.key === 'edit-price') {
+    handleEditPrice(row)
+  }
+}
+
+// ==================== 添加菜品(穿梭框) ====================
+async function openAddDishDialog() {
+  addDishVisible.value = true
+  leftSearchText.value = ''
+  rightSearchText.value = ''
+  leftCheckedKeys.value = []
+  rightCheckedKeys.value = []
+  try {
+    const res = await getMenuAllDishList()
+    if (res.code === 200) {
+      const dishes = res.data || []
+      currentMenuDishIds.value = new Set(dishes.map(d => d.dishId))
+      rightTreeData.value = buildTransferTree(dishes)
+    }
+  }
+  catch {
+    // ignore
+  }
+  await loadAvailableDishes()
+}
+
+async function loadAvailableDishes() {
+  try {
+    const res = await getMenuAvailableDishList(leftSearchText.value)
+    if (res.code === 200) {
+      allAvailableDishes.value = res.data || []
+      const filtered = allAvailableDishes.value.filter(d => !currentMenuDishIds.value.has(d.id))
+      leftTreeData.value = buildTransferTree(filtered)
+    }
+  }
+  catch {
+    // ignore
+  }
+}
+
+/** 构建穿梭框树结构 */
+function buildTransferTree(dishes) {
+  const typeMap = new Map()
+  for (const dish of dishes) {
+    const dishLibraryId = dish.dishId ?? dish.id
+    const typeId = dish.typeId
+    if (!typeId)
+      continue
+    if (!typeMap.has(typeId)) {
+      typeMap.set(typeId, {
+        id: `type_${typeId}`,
+        name: dish.typeName || '未分类',
+        children: [],
+      })
+    }
+    typeMap.get(typeId).children.push({
+      id: `dish_${dishLibraryId}`,
+      name: dish.dishName,
+      dishId: dishLibraryId,
+      dishData: dish,
+    })
+  }
+  return Array.from(typeMap.values())
+}
+
+const leftTotalCount = computed(() => {
+  let count = 0
+  for (const cat of leftTreeData.value) {
+    count += (cat.children || []).length
+  }
+  return count
+})
+
+const rightTotalCount = computed(() => {
+  let count = 0
+  for (const cat of rightTreeData.value) {
+    count += (cat.children || []).length
+  }
+  return count
+})
+
+function onLeftCheckedChange(keys) {
+  leftCheckedKeys.value = keys
+}
+
+function onRightCheckedChange(keys) {
+  rightCheckedKeys.value = keys
+}
+
+function toggleLeftCheckAll() {
+  const allKeys = []
+  for (const cat of leftTreeData.value) {
+    allKeys.push(cat.id)
+    for (const child of (cat.children || [])) {
+      allKeys.push(child.id)
+    }
+  }
+  leftCheckedKeys.value = leftCheckedKeys.value.length === allKeys.length ? [] : allKeys
+}
+
+function toggleRightCheckAll() {
+  const allKeys = []
+  for (const cat of rightTreeData.value) {
+    allKeys.push(cat.id)
+    for (const child of (cat.children || [])) {
+      allKeys.push(child.id)
+    }
+  }
+  rightCheckedKeys.value = rightCheckedKeys.value.length === allKeys.length ? [] : allKeys
+}
+
+/** 添加:左侧选中 → 右侧 */
+function handleTransferRight() {
+  const selectedDishes = []
+  for (const cat of leftTreeData.value) {
+    for (const child of (cat.children || [])) {
+      if (leftCheckedKeys.value.includes(child.id)) {
+        selectedDishes.push(child.dishData)
+      }
+    }
+  }
+  for (const dish of selectedDishes) {
+    currentMenuDishIds.value.add(dish.id)
+  }
+  const rightDishes = []
+  for (const cat of rightTreeData.value) {
+    for (const child of (cat.children || [])) {
+      rightDishes.push(child.dishData)
+    }
+  }
+  rightDishes.push(...selectedDishes)
+  rightTreeData.value = buildTransferTree(rightDishes)
+
+  allAvailableDishes.value = allAvailableDishes.value.filter(d => !selectedDishes.includes(d))
+  const filtered = allAvailableDishes.value.filter(d => !currentMenuDishIds.value.has(d.id))
+  leftTreeData.value = buildTransferTree(filtered)
+
+  leftCheckedKeys.value = []
+}
+
+/** 移出:右侧选中 → 左侧 */
+function handleTransferLeft() {
+  const removedDishes = []
+  for (const cat of rightTreeData.value) {
+    for (const child of (cat.children || [])) {
+      if (rightCheckedKeys.value.includes(child.id)) {
+        removedDishes.push(child.dishData)
+        currentMenuDishIds.value.delete(child.dishId)
+      }
+    }
+  }
+  const leftDishes = [...allAvailableDishes.value, ...removedDishes]
+  const filtered = leftDishes.filter(d => !currentMenuDishIds.value.has(d.id))
+  leftTreeData.value = buildTransferTree(filtered)
+
+  const rightDishes = []
+  for (const cat of rightTreeData.value) {
+    for (const child of (cat.children || [])) {
+      if (!removedDishes.find(r => (r.dishId ?? r.id) === child.dishId)) {
+        rightDishes.push(child.dishData)
+      }
+    }
+  }
+  rightTreeData.value = buildTransferTree(rightDishes)
+
+  rightCheckedKeys.value = []
+}
+
+/** 确定:将右侧所有菜品全量提交到后端 */
+async function confirmAddDishes() {
+  const dishes = []
+  let sort = 0
+  for (const cat of rightTreeData.value) {
+    for (const child of (cat.children || [])) {
+      dishes.push({
+        dishId: child.dishId,
+        salePrice: child.dishData.salePrice,
+        sort: sort++,
+      })
+    }
+  }
+  try {
+    const res = await saveMenuDishes(dishes)
+    if (res.code === 200) {
+      message.success('菜单菜品已保存')
+      addDishVisible.value = false
+      crudRef.value?.refresh()
+    }
+    else {
+      message.error(res.msg || '保存失败')
+    }
+  }
+  catch {
+    message.error('保存失败')
+  }
+}
+
+// ==================== 修改价格 ====================
+function handleEditPrice(row) {
+  priceEditRow.value = row
+  priceEditValue.value = row.salePrice != null ? +(row.salePrice / 100).toFixed(2) : 0
+  priceVisible.value = true
+}
+
+async function submitUpdatePrice() {
+  if (priceEditValue.value == null || priceEditValue.value < 0) {
+    message.warning('请输入有效价格')
+    return
+  }
+  try {
+    const res = await updateMenuPrice({
+      id: priceEditRow.value.id,
+      salePrice: Math.round(priceEditValue.value * 100),
+    })
+    if (res.code === 200) {
+      message.success('价格修改成功')
+      priceVisible.value = false
+      crudRef.value?.refresh()
+    }
+    else {
+      message.error(res.msg || '修改失败')
+    }
+  }
+  catch {
+    message.error('修改失败')
+  }
+}
+
+// ==================== 删除菜品 ====================
+function handleDeleteDish(row) {
+  dialog.warning({
+    title: '确认删除',
+    content: `确定从菜单中移除「${row.dishName}」?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      try {
+        const res = await deleteMenuDish(row.id)
+        if (res.code === 200) {
+          message.success('已移除')
+          crudRef.value?.refresh()
+        }
+        else {
+          message.error(res.msg || '删除失败')
+        }
+      }
+      catch {
+        message.error('删除失败')
+      }
+    },
+  })
+}
+
+function handleBatchDelete() {
+  if (checkedRowKeys.value.length === 0) {
+    message.warning('请选择要删除的菜品')
+    return
+  }
+  dialog.warning({
+    title: '批量删除',
+    content: `确定从菜单中移除选中的 ${checkedRowKeys.value.length} 个菜品?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      try {
+        for (const id of checkedRowKeys.value) {
+          await deleteMenuDish(id)
+        }
+        message.success('批量删除成功')
+        crudRef.value?.refresh()
+      }
+      catch {
+        message.error('删除失败')
+      }
+    },
+  })
+}
+
+// ==================== 更新菜单(归档) ====================
+function handleUpdateMenu() {
+  dialog.warning({
+    title: '更新菜单',
+    content: '将当前菜单归档为历史版本,并创建新菜单(复制所有菜品)。确定继续?',
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      try {
+        const res = await updateMenu()
+        if (res.code === 200) {
+          message.success('菜单已更新归档')
+          crudRef.value?.refresh()
+        }
+        else {
+          message.error(res.msg || '更新失败')
+        }
+      }
+      catch {
+        message.error('更新失败')
+      }
+    },
+  })
+}
+
+// ==================== 历史菜单 ====================
+async function loadHistoryList() {
+  historyLoading.value = true
+  try {
+    const res = await getMenuHistoryList({ pageNum: 1, pageSize: 100 })
+    if (res.code === 200) {
+      historyList.value = res.data?.records || res.data || []
+    }
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    historyLoading.value = false
+  }
+}
+
+const historyColumns = [
+  { title: '菜单ID', key: 'id', width: 180 },
+  { title: '归档时间', key: 'historyTime', width: 200, render(row) { return row.historyTime || '-' } },
+  { title: '菜品数量', key: 'dishCount', width: 100, render(row) { return `${row.dishCount || 0} 道` } },
+  { title: '备注', key: 'remark', width: 200, render(row) { return row.remark || '-' } },
+  {
+    title: '操作',
+    key: 'actions',
+    width: 100,
+    render(row) {
+      return h(NButton, {
+        size: 'small',
+        type: 'primary',
+        text: true,
+        onClick: () => openHistoryDetail(row),
+      }, { default: () => '查看详情' })
+    },
+  },
+]
+
+async function openHistoryDetail(row) {
+  historyDetailVisible.value = true
+  historyDetailLoading.value = true
+  historyDetailList.value = []
+  try {
+    const res = await getMenuHistoryDetailList({ menuId: row.id, pageNum: 1, pageSize: 200 })
+    if (res.code === 200) {
+      historyDetailList.value = res.data?.records || res.data || []
+    }
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    historyDetailLoading.value = false
+  }
+}
+
+const historyDetailColumns = [
+  { title: '菜品编码', key: 'dishCode', width: 130 },
+  {
+    title: '菜品图片',
+    key: 'image',
+    width: 80,
+    render(row) {
+      if (!row.image)
+        return '-'
+      return h(AuthImage, {
+        src: row.image,
+        style: 'width: 40px; height: 40px; border-radius: 4px; object-fit: cover;',
+      })
+    },
+  },
+  { title: '菜品名称', key: 'dishName', width: 150 },
+  { title: '分类', key: 'typeName', width: 100 },
+  { title: '单位', key: 'unit', width: 70 },
+  { title: '规格', key: 'spec', width: 80 },
+  { title: '口味', key: 'taste', width: 80 },
+  { title: '菜单价', key: 'salePrice', width: 100, render(row) { return row.salePrice != null ? `${formatPrice(row.salePrice)} 元` : '-' } },
+  { title: '菜品库价', key: 'librarySalePrice', width: 100, render(row) { return row.librarySalePrice != null ? `${formatPrice(row.librarySalePrice)} 元` : '-' } },
+]
+
+function handleTabChange(val) {
+  if (val === 'history' && historyList.value.length === 0) {
+    loadHistoryList()
+  }
+}
+
+// ==================== 初始化 ====================
+onMounted(() => {
+  loadTypeTree()
+})
+</script>
+
+<style scoped>
+.dish-menu-page {
+  height: 100%;
+}
+
+/* 当前菜单 Tab 内的左右布局 */
+.page-layout {
+  display: flex;
+  height: calc(100vh - 180px);
+  gap: 0;
+}
+
+/* 左侧分类树 */
+.menu-tree-panel {
+  width: 220px;
+  flex-shrink: 0;
+  border-right: 1px solid #e5e7eb;
+  padding: 12px 8px;
+  overflow-y: auto;
+  background: #fafafa;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.menu-tree-panel :deep(.n-tree-node-switcher) {
+  display: none;
+}
+
+.menu-tree-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  font-weight: 600;
+  color: #374151;
+  padding: 0 6px 10px;
+  border-bottom: 1px solid #f3f4f6;
+  margin-bottom: 8px;
+}
+
+/* 右侧内容区 */
+.menu-content-panel {
+  flex: 1;
+  overflow: hidden;
+}
+
+/* Tab 容器:往右偏移 */
+.tabs-wrapper {
+  padding-left: 16px;
+}
+
+/* Tab 样式:紧凑排列,选中带底色 */
+.menu-content-panel :deep(.n-tabs-rail) {
+  gap: 0 !important;
+}
+
+.menu-content-panel :deep(.n-tabs-tab-wrapper) {
+  margin-right: 0 !important;
+  padding-right: 0 !important;
+}
+
+.menu-content-panel :deep(.n-tabs-tab-pad) {
+  display: none !important;
+}
+
+.menu-content-panel :deep(.n-tab-pane) {
+  margin-right: 0 !important;
+}
+
+.menu-content-panel :deep(.n-tabs-tab) {
+  border: 1px solid #e5e7eb !important;
+  border-radius: 4px 0 0 4px !important;
+  margin-right: -1px !important;
+  padding: 6px 20px !important;
+  background: #f5f5f5;
+  color: #666;
+}
+
+.menu-content-panel :deep(.n-tabs-tab:last-child) {
+  border-radius: 0 4px 4px 0 !important;
+  margin-right: 0 !important;
+}
+
+.menu-content-panel :deep(.n-tabs-tab--active) {
+  background: #e8f4f8 !important;
+  border-color: #2080f0 !important;
+  color: #2080f0 !important;
+  font-weight: 500;
+  z-index: 1;
+}
+
+.menu-content-panel :deep(.n-tabs-bar) {
+  display: none;
+}
+
+/* 穿梭框布局 */
+.transfer-layout {
+  display: flex;
+  gap: 12px;
+  height: 450px;
+}
+
+.transfer-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid #e8e8e8;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.transfer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 12px;
+  background: #fafafa;
+  border-bottom: 1px solid #e8e8e8;
+  font-weight: 500;
+  font-size: 14px;
+}
+
+.transfer-count {
+  font-size: 12px;
+  color: #999;
+  font-weight: normal;
+}
+
+.transfer-search {
+  padding: 8px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.transfer-tree {
+  flex: 1;
+  overflow-y: auto;
+  padding: 8px;
+}
+
+.transfer-buttons {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  padding: 0 8px;
+}
+</style>

+ 557 - 0
forge-admin-ui/src/views/restaurant/dishRoom.vue

@@ -0,0 +1,557 @@
+<template>
+  <div class="restaurant-dish-room-page">
+    <!-- 搜索栏 -->
+    <AiSearch
+      ref="searchRef"
+      v-model="searchForm"
+      :schema="searchSchema"
+      :enable-collapse="false"
+      @search="loadRoomList"
+      @reset="handleReset"
+    />
+
+    <!-- 工具栏 -->
+    <div class="room-toolbar">
+      <n-button type="primary" @click="handleAdd">
+        新增包房
+      </n-button>
+    </div>
+
+    <!-- 卡片网格 -->
+    <n-spin :show="loading">
+      <div class="room-grid">
+        <div
+          v-for="item in dataList"
+          :key="item.id"
+          class="room-card"
+          :class="{ 'room-card-disabled': item.status === 'DISABLED' }"
+        >
+          <div class="room-card-top">
+            <div>
+              <div class="room-name">
+                {{ item.roomName }}
+              </div>
+              <div class="room-no">
+                包房号:{{ item.roomNo || '-' }}
+              </div>
+            </div>
+            <n-dropdown
+              trigger="click"
+              :options="cardMenuOptions"
+              @select="(key) => handleCardCommand(key, item)"
+            >
+              <span class="room-menu">•••</span>
+            </n-dropdown>
+          </div>
+
+          <div class="room-status-row">
+            <n-tag :type="item.status === 'ENABLED' ? 'success' : 'error'" size="small">
+              {{ item.status === 'ENABLED' ? '启用' : '停用' }}
+            </n-tag>
+            <n-tag
+              v-if="item.useStatus === 'IN_USE'"
+              type="warning"
+              size="small"
+              style="margin-left: 6px"
+            >
+              使用中
+            </n-tag>
+          </div>
+
+          <div class="room-meta">
+            <div>
+              <span class="meta-label">容纳人数</span>
+              <span class="meta-value">{{ item.capacity || 0 }} 人</span>
+            </div>
+            <div>
+              <span class="meta-label">最低消费</span>
+              <span class="meta-value">{{ formatMoney(item.minConsumption) }}</span>
+            </div>
+          </div>
+
+          <div class="room-remarks">
+            {{ item.remark || '暂无备注' }}
+          </div>
+        </div>
+
+        <!-- 新增卡片 -->
+        <div class="room-card room-add-card" @click="handleAdd">
+          <div class="add-icon">
+            +
+          </div>
+          <div class="add-text">
+            新增包房
+          </div>
+        </div>
+      </div>
+    </n-spin>
+
+    <!-- 编辑弹窗 -->
+    <n-modal
+      v-model:show="modalVisible"
+      preset="dialog"
+      :title="modalTitle"
+      style="width: 600px"
+      :show-icon="false"
+    >
+      <n-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-placement="left"
+        label-width="90"
+        style="margin-top: 16px"
+      >
+        <n-grid :cols="2" :x-gap="16">
+          <n-form-item-gi :span="1" label="包房名称" path="roomName">
+            <n-input v-model:value="formData.roomName" placeholder="请填写包房名称" />
+          </n-form-item-gi>
+          <n-form-item-gi :span="1" label="包房编号" path="roomNo">
+            <n-input v-model:value="formData.roomNo" placeholder="不填写则自动生成" />
+          </n-form-item-gi>
+          <n-form-item-gi :span="1" label="容纳人数" path="capacity">
+            <n-input-number v-model:value="formData.capacity" :min="0" :precision="0" style="width: 100%" placeholder="请输入" />
+          </n-form-item-gi>
+          <n-form-item-gi :span="1" label="最低消费" path="minConsumption">
+            <n-input-number v-model:value="formData.minConsumption" :min="0" :precision="2" style="width: 100%" placeholder="请输入" />
+          </n-form-item-gi>
+          <n-form-item-gi :span="1" label="排序" path="sort">
+            <n-input-number v-model:value="formData.sort" :min="0" :precision="0" style="width: 100%" />
+          </n-form-item-gi>
+          <n-form-item-gi :span="1" label="状态" path="status">
+            <n-radio-group v-model:value="formData.status">
+              <n-radio value="ENABLED">
+                启用
+              </n-radio>
+              <n-radio value="DISABLED">
+                停用
+              </n-radio>
+            </n-radio-group>
+          </n-form-item-gi>
+          <n-form-item-gi :span="2" label="备注" path="remark">
+            <n-input v-model:value="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" maxlength="500" show-count />
+          </n-form-item-gi>
+        </n-grid>
+      </n-form>
+      <template #action>
+        <n-button @click="modalVisible = false">
+          取消
+        </n-button>
+        <n-button type="primary" :loading="submitLoading" @click="handleSubmit">
+          确定
+        </n-button>
+      </template>
+    </n-modal>
+  </div>
+</template>
+
+<script setup>
+import { useDialog } from 'naive-ui'
+import { computed, onMounted, ref } from 'vue'
+import { deleteDishRoom, getDishRoomList, saveDishRoom, toggleDishRoomStatus } from '@/api/restaurant'
+import { AiSearch } from '@/components/ai-form'
+
+defineOptions({ name: 'RestaurantDishRoom' })
+
+const dialog = useDialog()
+
+// ==================== 搜索 ====================
+
+const searchRef = ref(null)
+
+const searchForm = ref({
+  roomName: '',
+  roomNo: '',
+  status: null,
+})
+
+const searchSchema = computed(() => [
+  {
+    field: 'roomName',
+    label: '包房名称',
+    type: 'input',
+    props: { placeholder: '请输入包房名称' },
+  },
+  {
+    field: 'roomNo',
+    label: '包房编号',
+    type: 'input',
+    props: { placeholder: '请输入包房编号' },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    type: 'select',
+    props: {
+      placeholder: '请选择状态',
+      clearable: true,
+      options: [
+        { label: '启用', value: 'ENABLED' },
+        { label: '停用', value: 'DISABLED' },
+      ],
+    },
+  },
+])
+
+function handleReset() {
+  searchForm.value = { roomName: '', roomNo: '', status: null }
+  loadRoomList()
+}
+
+// ==================== 列表 ====================
+
+const loading = ref(false)
+const dataList = ref([])
+
+async function loadRoomList(formData) {
+  loading.value = true
+  try {
+    const params = {}
+    const data = formData || searchForm.value
+    if (data.roomName)
+      params.roomName = data.roomName
+    if (data.roomNo)
+      params.roomNo = data.roomNo
+    if (data.status)
+      params.status = data.status
+    const res = await getDishRoomList(params)
+    if (res.code === 200) {
+      dataList.value = res.data || []
+    }
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  loadRoomList()
+})
+
+// ==================== 卡片菜单 ====================
+
+const cardMenuOptions = computed(() => [
+  { label: '修改', key: 'edit' },
+  { label: '切换状态', key: 'status' },
+  { label: '删除', key: 'delete', type: 'error' },
+])
+
+function handleCardCommand(command, row) {
+  if (command === 'edit') {
+    handleEdit(row)
+  }
+  else if (command === 'delete') {
+    handleDelete(row)
+  }
+  else if (command === 'status') {
+    handleToggleStatus(row)
+  }
+}
+
+// ==================== 新增/编辑弹窗 ====================
+
+const modalVisible = ref(false)
+const modalTitle = ref('新增包房')
+const submitLoading = ref(false)
+const formRef = ref(null)
+
+const formData = ref({
+  id: null,
+  roomNo: '',
+  roomName: '',
+  capacity: null,
+  minConsumption: null,
+  status: 'ENABLED',
+  sort: 0,
+  remark: '',
+})
+
+const formRules = {
+  roomName: [{ required: true, message: '包房名称不能为空', trigger: 'blur' }],
+}
+
+function resetForm() {
+  formData.value = {
+    id: null,
+    roomNo: '',
+    roomName: '',
+    capacity: null,
+    minConsumption: null,
+    status: 'ENABLED',
+    sort: 0,
+    remark: '',
+  }
+}
+
+function handleAdd() {
+  resetForm()
+  modalTitle.value = '新增包房'
+  modalVisible.value = true
+}
+
+function handleEdit(row) {
+  resetForm()
+  modalTitle.value = '修改包房'
+  formData.value = {
+    id: row.id,
+    roomNo: row.roomNo || '',
+    roomName: row.roomName || '',
+    capacity: row.capacity,
+    minConsumption: row.minConsumption !== null && row.minConsumption !== undefined ? row.minConsumption / 100 : null,
+    status: row.status || 'ENABLED',
+    sort: row.sort || 0,
+    remark: row.remark || '',
+  }
+  modalVisible.value = true
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value?.validate()
+  }
+  catch {
+    return
+  }
+
+  submitLoading.value = true
+  try {
+    const data = { ...formData.value }
+    // 最低消费:元 -> 分
+    if (data.minConsumption !== null && data.minConsumption !== undefined) {
+      data.minConsumption = Math.round(data.minConsumption * 100)
+    }
+    await saveDishRoom(data)
+    modalVisible.value = false
+    loadRoomList()
+  }
+  catch {
+    // ignore
+  }
+  finally {
+    submitLoading.value = false
+  }
+}
+
+// ==================== 删除 ====================
+
+function handleDelete(row) {
+  dialog.warning({
+    title: '提示',
+    content: `确定删除包房「${row.roomName}」吗?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      await deleteDishRoom(row.id)
+      loadRoomList()
+    },
+  })
+}
+
+// ==================== 切换状态 ====================
+
+function handleToggleStatus(row) {
+  if (row.useStatus === 'IN_USE') {
+    dialog.warning({
+      title: '提示',
+      content: '该包房正在使用中,无法修改状态',
+      positiveText: '知道了',
+    })
+    return
+  }
+  const action = row.status === 'ENABLED' ? '停用' : '启用'
+  dialog.warning({
+    title: '提示',
+    content: `确定${action}包房「${row.roomName}」吗?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      await toggleDishRoomStatus(row.id)
+      loadRoomList()
+    },
+  })
+}
+
+// ==================== 工具方法 ====================
+
+/** 分转元显示 */
+function formatMoney(cents) {
+  if (cents === null || cents === undefined)
+    return '0 元'
+  return `${(cents / 100).toFixed(0)} 元`
+}
+</script>
+
+<style scoped>
+.restaurant-dish-room-page {
+  padding: 16px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.room-toolbar {
+  display: flex;
+  align-items: center;
+  padding: 8px 12px 12px;
+  border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 16px;
+}
+
+.room-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.room-card {
+  position: relative;
+  min-height: 180px;
+  border: 3px solid #e5e7eb;
+  border-radius: 8px;
+  background: #fff;
+  padding: 18px;
+  transition:
+    border-color 0.2s,
+    box-shadow 0.2s,
+    transform 0.2s;
+}
+
+.room-card:hover {
+  border-color: var(--primary-color, #18a058);
+  box-shadow: 0 8px 22px rgba(31, 45, 61, 0.08);
+  transform: translateY(-1px);
+}
+
+.room-card-disabled {
+  background: #f7f8fa;
+}
+
+.room-card-top {
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.room-name {
+  font-size: 20px;
+  font-weight: 600;
+  line-height: 28px;
+  color: #1f2937;
+}
+
+.room-no {
+  margin-top: 4px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.room-menu {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border-radius: 4px;
+  cursor: pointer;
+  color: #606266;
+  font-size: 18px;
+  line-height: 1;
+}
+
+.room-menu:hover {
+  background: #f2f6fc;
+  color: var(--primary-color, #18a058);
+}
+
+.room-status-row {
+  margin-top: 12px;
+}
+
+.room-meta {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px;
+  margin-top: 18px;
+}
+
+.room-meta > div {
+  border-radius: 6px;
+  background: #f7f9fb;
+  padding: 10px;
+}
+
+.meta-label {
+  display: block;
+  font-size: 12px;
+  color: #909399;
+  line-height: 18px;
+}
+
+.meta-value {
+  display: block;
+  margin-top: 4px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 22px;
+}
+
+.room-remarks {
+  margin-top: 14px;
+  color: #606266;
+  font-size: 13px;
+  line-height: 20px;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.room-add-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  border-style: dashed;
+  color: var(--primary-color, #18a058);
+}
+
+.room-add-card:hover {
+  border-color: var(--primary-color, #18a058) !important;
+  transform: none !important;
+  box-shadow: none !important;
+}
+
+.add-icon {
+  width: 42px;
+  height: 42px;
+  border-radius: 50%;
+  border: 1px dashed var(--primary-color, #18a058);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28px;
+  line-height: 1;
+}
+
+.add-text {
+  margin-top: 12px;
+  font-size: 15px;
+}
+
+@media (max-width: 1200px) {
+  .room-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 760px) {
+  .room-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 173 - 0
forge-admin-ui/src/views/restaurant/dishType.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="restaurant-dish-type-page">
+    <AiCrudPage
+      ref="crudRef"
+      api="/restaurant/dishType"
+      :api-config="{
+        list: 'get@/restaurant/dishType/list',
+        detail: 'get@/restaurant/dishType/detail?id=:id',
+        add: 'post@/restaurant/dishType',
+        update: 'put@/restaurant/dishType',
+        delete: 'post@/restaurant/dishType/remove?id=:id',
+      }"
+      :load-detail-on-edit="true"
+      :columns="tableColumns"
+      :edit-schema="editSchema"
+      row-key="id"
+      add-button-text="新增分类"
+    />
+  </div>
+</template>
+
+<script setup>
+import { NTag } from 'naive-ui'
+import { computed, h, onMounted, ref } from 'vue'
+import { getDishTypeTree, toggleDishTypeStatus } from '@/api/restaurant'
+import { AiCrudPage } from '@/components/ai-form'
+
+defineOptions({ name: 'RestaurantDishType' })
+
+const crudRef = ref(null)
+
+// 状态标签颜色映射
+const statusColorMap = {
+  ENABLED: 'success',
+  DISABLED: 'default',
+}
+const statusLabelMap = {
+  ENABLED: '启用',
+  DISABLED: '禁用',
+}
+
+// 父级分类选项(从树形数据扁平化)
+const parentOptions = ref([])
+
+/** 将树形数据扁平化为带缩进的下拉选项 */
+function flattenTreeToOptions(nodes, level = 0) {
+  const options = []
+  for (let i = 0; i < nodes.length; i++) {
+    const node = nodes[i]
+    const prefix = level > 0 ? `${' '.repeat(level)}└ ` : ''
+    options.push({
+      label: `${prefix}${node.name}`,
+      value: node.id,
+    })
+    if (node.children && node.children.length > 0) {
+      const childOptions = flattenTreeToOptions(node.children, level + 1)
+      for (let j = 0; j < childOptions.length; j++) {
+        options.push(childOptions[j])
+      }
+    }
+  }
+  return options
+}
+
+async function loadParentOptions() {
+  try {
+    const res = await getDishTypeTree()
+    if (res.code === 200) {
+      parentOptions.value = flattenTreeToOptions(res.data || [])
+    }
+  }
+  catch {
+    // ignore
+  }
+}
+
+onMounted(() => {
+  loadParentOptions()
+})
+
+// 表格列配置
+const tableColumns = computed(() => [
+  { prop: 'name', label: '分类名称', minWidth: 180 },
+  { prop: 'sort', label: '排序', width: 80 },
+  {
+    prop: 'status',
+    label: '状态',
+    width: 100,
+    render: row => h(NTag, { type: statusColorMap[row.status] || 'default', size: 'small' }, { default: () => statusLabelMap[row.status] || row.status }),
+  },
+  { prop: 'remark', label: '备注', minWidth: 150 },
+  { prop: 'createTime', label: '创建时间', width: 172 },
+  {
+    label: '操作',
+    width: 200,
+    render: (row) => {
+      const toggleText = row.status === 'ENABLED' ? '禁用' : '启用'
+      const toggleType = row.status === 'ENABLED' ? 'text-warning' : 'text-success'
+      return h('div', { class: 'flex gap-3' }, [
+        h('a', {
+          class: 'text-primary cursor-pointer hover:opacity-80',
+          onClick: () => crudRef.value?.handleEdit(row),
+        }, '编辑'),
+        h('a', {
+          class: `${toggleType} cursor-pointer hover:opacity-80`,
+          onClick: async () => {
+            await toggleDishTypeStatus(row.id)
+            crudRef.value?.refresh()
+            loadParentOptions()
+          },
+        }, toggleText),
+        h('a', {
+          class: 'text-error cursor-pointer hover:opacity-80',
+          onClick: () => crudRef.value?.handleDelete(row),
+        }, '删除'),
+      ])
+    },
+  },
+])
+
+// 编辑表单配置
+const editSchema = computed(() => [
+  {
+    field: 'name',
+    label: '分类名称',
+    type: 'input',
+    rules: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
+    props: { placeholder: '请输入分类名称' },
+  },
+  {
+    field: 'parentId',
+    label: '上级分类',
+    type: 'select',
+    props: {
+      placeholder: '不填则为顶级分类',
+      options: parentOptions.value,
+      clearable: true,
+    },
+  },
+  {
+    field: 'sort',
+    label: '排序',
+    type: 'inputNumber',
+    defaultValue: 0,
+    props: { placeholder: '排序号', min: 0 },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    type: 'select',
+    defaultValue: 'ENABLED',
+    props: {
+      placeholder: '请选择状态',
+      options: [
+        { label: '启用', value: 'ENABLED' },
+        { label: '禁用', value: 'DISABLED' },
+      ],
+    },
+  },
+  {
+    field: 'remark',
+    label: '备注',
+    type: 'textarea',
+    props: { placeholder: '请输入备注', rows: 3 },
+  },
+])
+</script>
+
+<style scoped>
+.restaurant-dish-type-page {
+  height: 100%;
+}
+</style>

+ 117 - 0
forge-h5-ui/HTTPS_SETUP.md

@@ -0,0 +1,117 @@
+# H5 扫码功能 HTTPS 配置指南
+
+## 问题原因
+
+`html5-qrcode` 库要求必须在 **HTTPS** 或 **localhost** 环境下才能访问摄像头。
+
+你当前的环境是局域网 IP(`192.168.10.7`),不是 HTTPS,所以浏览器拒绝访问摄像头。
+
+## 解决方案:使用 ngrok 提供 HTTPS 隧道
+
+### 步骤1:下载并安装 ngrok
+
+1. 访问 https://ngrok.com/download
+2. 下载 Windows 版本(`ngrok-v3-stable-windows-amd64.zip`)
+3. 解压到任意目录,例如 `D:\tools\ngrok`
+4. (可选)将 ngrok.exe 所在目录添加到系统 PATH
+
+### 步骤2:注册 ngrok 账号(免费)
+
+1. 访问 https://dashboard.ngrok.com/signup
+2. 使用邮箱注册
+3. 获取你的 authtoken(在 Dashboard → Your Authtoken)
+
+### 步骤3:配置 ngrok
+
+```bash
+# 打开命令行,进入 ngrok 目录
+cd D:\tools\ngrok
+
+# 配置 authtoken(替换为你的 token)
+ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
+```
+
+### 步骤4:启动 H5 开发服务器
+
+```bash
+cd D:\java_project\forge-admin\forge-h5-ui
+pnpm dev:h5
+```
+
+假设前端运行在 **3001** 端口(查看控制台输出确认实际端口)。
+
+### 步骤5:启动 ngrok 隧道
+
+```bash
+# 方法1:直接启动(推荐)
+ngrok http 3001
+
+# 方法2:指定域名(如果你有付费计划)
+ngrok http --domain=your-domain.ngrok-free.app 3001
+```
+
+启动成功后,你会看到类似这样的输出:
+
+```
+Forwarding    https://abc123def456.ngrok-free.app -> http://localhost:3001
+```
+
+### 步骤6:使用 HTTPS URL 访问
+
+在手机浏览器中访问 ngrok 提供的 HTTPS URL,例如:
+
+```
+https://abc123def456.ngrok-free.app
+```
+
+**注意:**
+- 不要使用原来的 `http://192.168.10.7:3001`
+- 必须使用 ngrok 生成的 `https://xxx.ngrok-free.app`
+
+### 步骤7:测试扫码功能
+
+1. 打开 HTTPS URL
+2. 点击"扫描二维码"按钮
+3. 浏览器会请求摄像头权限,点击"允许"
+4. 应该能正常打开相机进行扫码了!
+
+## 常见问题
+
+### Q1: ngrok 免费版有限制吗?
+
+A: 是的,免费版有以下限制:
+- 每次重启 ngrok,URL 会变化
+- 每月有带宽限制(通常够用)
+- 连接数有限制
+
+如果需要固定域名,可以升级到付费计划。
+
+### Q2: 为什么花生壳不行?
+
+A: 花生壳免费版不支持 HTTPS 映射,而 `html5-qrcode` 必须要求 HTTPS。
+
+### Q3: 有没有其他替代方案?
+
+A: 有的,可以使用以下工具:
+- **Cloudflare Tunnel**(免费,推荐):https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/
+- **localtunnel**(免费):`npm install -g localtunnel && lt --port 3001`
+- **serveo**(免费,无需安装):`ssh -R 80:localhost:3001 serveo.net`
+
+### Q4: 支付宝沙盒扫码还是空白页怎么办?
+
+A: 支付宝内置 WebView 强制要求 HTTPS,即使使用 ngrok 也可能有问题。建议:
+1. 使用普通手机浏览器(Chrome/Safari)测试
+2. 或者部署到正式的 HTTPS 服务器(如阿里云、腾讯云)
+
+## 验证是否成功
+
+打开浏览器控制台(F12),如果看到以下日志,说明摄像头已正常启动:
+
+```
+[QR Scanner] Camera started successfully
+```
+
+如果看到错误信息,请检查:
+1. 是否使用了 HTTPS URL
+2. 是否允许了摄像头权限
+3. 是否有其他应用占用了摄像头

+ 14 - 0
forge-server/db/migration/V1.0.124__add_hotel_qrcode_scan_permission.sql

@@ -0,0 +1,14 @@
+-- V1.0.124: 添加 H5 扫码绑定房间权限(hotel:qrcode:scan)
+-- 用于控制移动端员工扫码绑定房间功能的访问权限,仅授权人员可使用
+
+-- 查找酒店管理目录
+SET @hotel_parent_id = (SELECT id FROM sys_resource WHERE resource_name = '酒店管理' AND tenant_id = 1 AND parent_id = 0 LIMIT 1);
+
+-- 查找二维码管理菜单(如果存在则挂在其下,否则挂到酒店管理目录)
+SET @qrcode_menu_id = (SELECT id FROM sys_resource WHERE resource_name = '二维码管理' AND tenant_id = 1 LIMIT 1);
+SET @qrcode_menu_id = IFNULL(@qrcode_menu_id, @hotel_parent_id);
+
+-- 扫码绑定权限(按钮级,resource_type=3)
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '扫码绑定', @qrcode_menu_id, 3, 20, 'hotel:qrcode:scan', 0, 0, 1, 1, 'H5移动端扫码绑定/解绑房间(员工专属)', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'hotel:qrcode:scan' AND tenant_id = 1);

+ 144 - 0
forge-server/db/migration/V1.0.125__add_restaurant_dish_type_and_library.sql

@@ -0,0 +1,144 @@
+-- ============================================================
+-- V1.0.125  新增餐饮模块:菜品分类表 + 菜品库表 + 字典 + 菜单权限
+-- ============================================================
+
+-- ==================== 1. 建表 ====================
+
+-- 菜品分类(树形结构,支持多级分类)
+CREATE TABLE IF NOT EXISTS `restaurant_dish_type` (
+  `id` bigint NOT NULL COMMENT '主键ID(雪花算法)',
+  `tenant_id` bigint NOT NULL COMMENT '租户ID',
+  `name` varchar(100) NOT NULL COMMENT '分类名称',
+  `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父级ID(0=顶级分类)',
+  `parent_ids` varchar(500) DEFAULT NULL COMMENT '父级链路(逗号分隔祖先ID,如 1,2,3)',
+  `level` int NOT NULL DEFAULT 1 COMMENT '层级(1=顶级)',
+  `sort` int NOT NULL DEFAULT 0 COMMENT '排序号',
+  `status` varchar(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `del_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记(0=未删除,删除时写入主键ID)',
+  `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_dept` bigint DEFAULT NULL COMMENT '创建部门ID',
+  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_tenant_name_del` (`tenant_id`, `name`, `parent_id`, `del_flag`),
+  KEY `idx_tenant_parent` (`tenant_id`, `parent_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品分类';
+
+-- 菜品库(所有菜品的基础信息)
+CREATE TABLE IF NOT EXISTS `restaurant_dish_library` (
+  `id` bigint NOT NULL COMMENT '主键ID(雪花算法)',
+  `tenant_id` bigint NOT NULL COMMENT '租户ID',
+  `dish_code` varchar(50) NOT NULL COMMENT '菜品编码(自动生成 CP+时间戳)',
+  `dish_name` varchar(100) NOT NULL COMMENT '菜品名称',
+  `type_id` bigint NOT NULL COMMENT '分类ID(必须为最子级分类)',
+  `image` varchar(200) DEFAULT NULL COMMENT '菜品图片(文件ID)',
+  `unit` varchar(20) DEFAULT NULL COMMENT '单位(如:份、碗、杯)',
+  `spec` varchar(50) DEFAULT NULL COMMENT '规格(如:大份、小份)',
+  `sale_price` bigint NOT NULL DEFAULT 0 COMMENT '销售价(单位:分)',
+  `cost_price` bigint DEFAULT NULL COMMENT '参考成本价(单位:分)',
+  `taste` varchar(50) DEFAULT NULL COMMENT '口味(如:微辣、酸甜)',
+  `status` varchar(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-禁用',
+  `sort` int NOT NULL DEFAULT 0 COMMENT '排序号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `del_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记(0=未删除,删除时写入主键ID)',
+  `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_dept` bigint DEFAULT NULL COMMENT '创建部门ID',
+  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_tenant_code_del` (`tenant_id`, `dish_code`, `del_flag`),
+  UNIQUE KEY `uk_tenant_type_name_del` (`tenant_id`, `type_id`, `dish_name`, `del_flag`),
+  KEY `idx_tenant_type` (`tenant_id`, `type_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品库';
+
+-- ==================== 2. 字典数据 ====================
+
+-- 菜品分类状态
+INSERT INTO sys_dict_type (tenant_id, dict_name, dict_type, dict_status, remark, create_time, update_time)
+SELECT 1, '菜品分类状态', 'restaurant_dish_type_status', 1, '餐厅菜品分类启用/禁用状态', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'restaurant_dish_type_status' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 1, '启用', 'ENABLED', 'restaurant_dish_type_status', NULL, 'success', 'N', 1, '菜品分类启用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_dish_type_status' AND dict_value = 'ENABLED' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 2, '禁用', 'DISABLED', 'restaurant_dish_type_status', NULL, 'default', 'N', 1, '菜品分类禁用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_dish_type_status' AND dict_value = 'DISABLED' AND tenant_id = 1);
+
+-- 菜品状态
+INSERT INTO sys_dict_type (tenant_id, dict_name, dict_type, dict_status, remark, create_time, update_time)
+SELECT 1, '菜品状态', 'restaurant_dish_status', 1, '餐厅菜品启用/禁用状态', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'restaurant_dish_status' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 1, '启用', 'ENABLED', 'restaurant_dish_status', NULL, 'success', 'N', 1, '菜品启用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_dish_status' AND dict_value = 'ENABLED' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 2, '禁用', 'DISABLED', 'restaurant_dish_status', NULL, 'default', 'N', 1, '菜品禁用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_dish_status' AND dict_value = 'DISABLED' AND tenant_id = 1);
+
+-- ==================== 3. 菜单权限 ====================
+
+-- 获取「餐厅管理」一级菜单ID(V1.0.117 已创建)
+SET @restaurant_parent_id = (SELECT id FROM sys_resource WHERE resource_name = '餐厅管理' AND tenant_id = 1 AND parent_id = 0 LIMIT 1);
+
+-- 菜品管理(目录菜单,挂在「餐厅管理」下)
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, path, component, is_external, is_public, menu_status, visible, icon, keep_alive, always_show, remark, create_time, update_time)
+SELECT 1, '菜品管理', @restaurant_parent_id, 1, 2, '/restaurant/dish', NULL, 0, 0, 1, 1, 'mdi:food-apple', 0, 0, '菜品分类与菜品库管理', NOW(), NOW()
+WHERE @restaurant_parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE resource_name = '菜品管理' AND parent_id = @restaurant_parent_id);
+
+SET @dish_parent_id = (SELECT id FROM sys_resource WHERE resource_name = '菜品管理' AND parent_id = @restaurant_parent_id LIMIT 1);
+
+-- ---- 菜品分类(菜单 + 按钮权限) ----
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, path, component, is_external, is_public, menu_status, visible, icon, keep_alive, always_show, remark, create_time, update_time)
+SELECT 1, '菜品分类', @dish_parent_id, 2, 1, '/restaurant/dishType', 'restaurant/dishType', 0, 0, 1, 1, 'mdi:format-list-bulleted', 0, 0, '菜品分类管理(树形)', NOW(), NOW()
+WHERE @dish_parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishType:query' AND tenant_id = 1);
+
+SET @dish_type_menu_id = (SELECT id FROM sys_resource WHERE resource_name = '菜品分类' AND parent_id = @dish_parent_id LIMIT 1);
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '分类查询', @dish_type_menu_id, 3, 1, 'restaurant:dishType:query', 0, 0, 1, 1, '查询菜品分类', NOW(), NOW()
+WHERE @dish_type_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishType:query');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '分类新增', @dish_type_menu_id, 3, 2, 'restaurant:dishType:add', 0, 0, 1, 1, '新增菜品分类', NOW(), NOW()
+WHERE @dish_type_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishType:add');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '分类编辑', @dish_type_menu_id, 3, 3, 'restaurant:dishType:edit', 0, 0, 1, 1, '编辑菜品分类', NOW(), NOW()
+WHERE @dish_type_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishType:edit');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '分类删除', @dish_type_menu_id, 3, 4, 'restaurant:dishType:delete', 0, 0, 1, 1, '删除菜品分类', NOW(), NOW()
+WHERE @dish_type_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishType:delete');
+
+-- ---- 菜品库(菜单 + 按钮权限) ----
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, path, component, is_external, is_public, menu_status, visible, icon, keep_alive, always_show, remark, create_time, update_time)
+SELECT 1, '菜品库', @dish_parent_id, 2, 2, '/restaurant/dishLibrary', 'restaurant/dishLibrary', 0, 0, 1, 1, 'mdi:book-open-variant', 0, 0, '菜品库管理(左树右表)', NOW(), NOW()
+WHERE @dish_parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dish:query' AND tenant_id = 1);
+
+SET @dish_lib_menu_id = (SELECT id FROM sys_resource WHERE resource_name = '菜品库' AND parent_id = @dish_parent_id LIMIT 1);
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜品查询', @dish_lib_menu_id, 3, 1, 'restaurant:dish:query', 0, 0, 1, 1, '查询菜品', NOW(), NOW()
+WHERE @dish_lib_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dish:query');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜品新增', @dish_lib_menu_id, 3, 2, 'restaurant:dish:add', 0, 0, 1, 1, '新增菜品', NOW(), NOW()
+WHERE @dish_lib_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dish:add');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜品编辑', @dish_lib_menu_id, 3, 3, 'restaurant:dish:edit', 0, 0, 1, 1, '编辑菜品', NOW(), NOW()
+WHERE @dish_lib_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dish:edit');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜品删除', @dish_lib_menu_id, 3, 4, 'restaurant:dish:delete', 0, 0, 1, 1, '删除菜品', NOW(), NOW()
+WHERE @dish_lib_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dish:delete');

+ 3 - 0
forge-server/db/migration/V1.0.126__fix_dish_library_sale_price_nullable.sql

@@ -0,0 +1,3 @@
+-- 修复菜品库销售价字段允许为空(之前误设为 NOT NULL DEFAULT 0)
+ALTER TABLE `restaurant_dish_library`
+  MODIFY COLUMN `sale_price` bigint DEFAULT NULL COMMENT '销售价(单位:分)';

+ 85 - 0
forge-server/db/migration/V1.0.127__add_restaurant_dish_room.sql

@@ -0,0 +1,85 @@
+-- ============================================================
+-- V1.0.127  新增餐饮模块:包房表 + 字典 + 菜单权限
+-- ============================================================
+
+-- ==================== 1. 建表 ====================
+
+-- 包房(卡片式管理,支持启用/停用、使用状态跟踪)
+CREATE TABLE IF NOT EXISTS `restaurant_dish_room` (
+  `id` bigint NOT NULL COMMENT '主键ID(雪花算法)',
+  `tenant_id` bigint NOT NULL COMMENT '租户ID',
+  `room_no` varchar(50) NOT NULL COMMENT '包房编号(自动生成 BF+时间戳,全局唯一)',
+  `room_name` varchar(100) NOT NULL COMMENT '包房名称',
+  `capacity` int DEFAULT NULL COMMENT '容纳人数',
+  `min_consumption` bigint DEFAULT NULL COMMENT '最低消费(单位:分)',
+  `status` varchar(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED-启用, DISABLED-停用',
+  `use_status` varchar(20) NOT NULL DEFAULT 'IDLE' COMMENT '使用状态:IDLE-空闲, IN_USE-使用中(由订单模块自动管理)',
+  `sort` int NOT NULL DEFAULT 0 COMMENT '排序号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `del_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记(0=未删除,删除时写入主键ID)',
+  `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_dept` bigint DEFAULT NULL COMMENT '创建部门ID',
+  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_tenant_room_no_del` (`tenant_id`, `room_no`, `del_flag`),
+  UNIQUE KEY `uk_tenant_room_name_del` (`tenant_id`, `room_name`, `del_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='包房管理';
+
+-- ==================== 2. 字典数据 ====================
+
+-- 包房状态
+INSERT INTO sys_dict_type (tenant_id, dict_name, dict_type, dict_status, remark, create_time, update_time)
+SELECT 1, '包房状态', 'restaurant_room_status', 1, '餐厅包房启用/停用状态', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'restaurant_room_status' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 1, '启用', 'ENABLED', 'restaurant_room_status', NULL, 'success', 'N', 1, '包房启用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_room_status' AND dict_value = 'ENABLED' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 2, '停用', 'DISABLED', 'restaurant_room_status', NULL, 'error', 'N', 1, '包房停用', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_room_status' AND dict_value = 'DISABLED' AND tenant_id = 1);
+
+-- 包房使用状态
+INSERT INTO sys_dict_type (tenant_id, dict_name, dict_type, dict_status, remark, create_time, update_time)
+SELECT 1, '包房使用状态', 'restaurant_room_use_status', 1, '餐厅包房使用状态(空闲/使用中)', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'restaurant_room_use_status' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 1, '空闲', 'IDLE', 'restaurant_room_use_status', NULL, 'success', 'N', 1, '包房空闲', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_room_use_status' AND dict_value = 'IDLE' AND tenant_id = 1);
+
+INSERT INTO sys_dict_data (tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, dict_status, remark, create_time, update_time)
+SELECT 1, 2, '使用中', 'IN_USE', 'restaurant_room_use_status', NULL, 'warning', 'N', 1, '包房使用中', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'restaurant_room_use_status' AND dict_value = 'IN_USE' AND tenant_id = 1);
+
+-- ==================== 3. 菜单权限 ====================
+
+-- 获取「餐厅管理」一级菜单ID
+SET @restaurant_parent_id = (SELECT id FROM sys_resource WHERE resource_name = '餐厅管理' AND tenant_id = 1 AND parent_id = 0 LIMIT 1);
+
+-- 包房管理(菜单,挂在「餐厅管理」下)
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, path, component, is_external, is_public, menu_status, visible, icon, keep_alive, always_show, remark, create_time, update_time)
+SELECT 1, '包房管理', @restaurant_parent_id, 2, 3, '/restaurant/dishRoom', 'restaurant/dishRoom', 0, 0, 1, 1, 'mdi:door-open', 0, 0, '包房管理(卡片列表)', NOW(), NOW()
+WHERE @restaurant_parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishRoom:query' AND tenant_id = 1);
+
+SET @room_menu_id = (SELECT id FROM sys_resource WHERE resource_name = '包房管理' AND parent_id = @restaurant_parent_id LIMIT 1);
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '包房查询', @room_menu_id, 3, 1, 'restaurant:dishRoom:query', 0, 0, 1, 1, '查询包房', NOW(), NOW()
+WHERE @room_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishRoom:query');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '包房新增', @room_menu_id, 3, 2, 'restaurant:dishRoom:add', 0, 0, 1, 1, '新增包房', NOW(), NOW()
+WHERE @room_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishRoom:add');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '包房编辑', @room_menu_id, 3, 3, 'restaurant:dishRoom:edit', 0, 0, 1, 1, '编辑包房', NOW(), NOW()
+WHERE @room_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishRoom:edit');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '包房删除', @room_menu_id, 3, 4, 'restaurant:dishRoom:delete', 0, 0, 1, 1, '删除包房', NOW(), NOW()
+WHERE @room_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:dishRoom:delete');

+ 72 - 0
forge-server/db/migration/V1.0.128__add_restaurant_dish_menu.sql

@@ -0,0 +1,72 @@
+-- ============================================================
+-- V1.0.128  新增餐饮模块:菜单管理表 + 菜单权限
+-- ============================================================
+
+-- ==================== 1. 建表 ====================
+
+-- 菜单主表(版本归档机制:系统中始终只有一份"当前菜单")
+CREATE TABLE IF NOT EXISTS `restaurant_dish_menu` (
+  `id` bigint NOT NULL COMMENT '主键ID(雪花算法)',
+  `tenant_id` bigint NOT NULL COMMENT '租户ID',
+  `is_history` varchar(1) NOT NULL DEFAULT '0' COMMENT '是否历史菜单:0-当前, 1-历史',
+  `history_time` datetime DEFAULT NULL COMMENT '成为历史菜单的时间',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `del_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记(0=未删除,删除时写入主键ID)',
+  `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_dept` bigint DEFAULT NULL COMMENT '创建部门ID',
+  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单主表';
+
+-- 菜单菜品关联表(菜单级独立定价,覆盖菜品库价格)
+CREATE TABLE IF NOT EXISTS `restaurant_dish_menu_dish` (
+  `id` bigint NOT NULL COMMENT '主键ID(雪花算法)',
+  `tenant_id` bigint NOT NULL COMMENT '租户ID',
+  `menu_id` bigint NOT NULL COMMENT '菜单ID',
+  `dish_id` bigint NOT NULL COMMENT '菜品ID',
+  `sale_price` bigint DEFAULT NULL COMMENT '菜单销售价(单位:分,覆盖菜品库价格)',
+  `sort` int NOT NULL DEFAULT 0 COMMENT '排序号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `is_history` varchar(1) NOT NULL DEFAULT '0' COMMENT '是否历史菜单:0-当前, 1-历史',
+  `history_time` datetime DEFAULT NULL COMMENT '成为历史菜单的时间',
+  `del_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记(0=未删除,删除时写入主键ID)',
+  `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_dept` bigint DEFAULT NULL COMMENT '创建部门ID',
+  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_menu_id` (`menu_id`),
+  KEY `idx_dish_id` (`dish_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单菜品关联表';
+
+-- ==================== 2. 菜单权限 ====================
+
+-- 获取「餐厅管理」一级菜单ID
+SET @restaurant_parent_id = (SELECT id FROM sys_resource WHERE resource_name = '餐厅管理' AND tenant_id = 1 AND parent_id = 0 LIMIT 1);
+
+-- 菜单管理(菜单,挂在「餐厅管理」下)
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, path, component, is_external, is_public, menu_status, visible, icon, keep_alive, always_show, remark, create_time, update_time)
+SELECT 1, '菜单管理', @restaurant_parent_id, 2, 4, '/restaurant/dishMenu', 'restaurant/dishMenu', 0, 0, 1, 1, 'mdi:book-open-variant', 0, 0, '菜单管理(版本归档+独立定价)', NOW(), NOW()
+WHERE @restaurant_parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:menu:query' AND tenant_id = 1);
+
+SET @menu_menu_id = (SELECT id FROM sys_resource WHERE resource_name = '菜单管理' AND parent_id = @restaurant_parent_id LIMIT 1);
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜单查询', @menu_menu_id, 3, 1, 'restaurant:menu:query', 0, 0, 1, 1, '查询菜单', NOW(), NOW()
+WHERE @menu_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:menu:query');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜单添加菜品', @menu_menu_id, 3, 2, 'restaurant:menu:add', 0, 0, 1, 1, '添加菜品到菜单', NOW(), NOW()
+WHERE @menu_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:menu:add');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜单编辑', @menu_menu_id, 3, 3, 'restaurant:menu:edit', 0, 0, 1, 1, '编辑菜单', NOW(), NOW()
+WHERE @menu_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:menu:edit');
+
+INSERT INTO sys_resource (tenant_id, resource_name, parent_id, resource_type, sort, perms, is_external, is_public, menu_status, visible, remark, create_time, update_time)
+SELECT 1, '菜单删除', @menu_menu_id, 3, 4, 'restaurant:menu:delete', 0, 0, 1, 1, '删除菜单菜品', NOW(), NOW()
+WHERE @menu_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_resource WHERE perms = 'restaurant:menu:delete');

+ 6 - 0
forge-server/forge-admin-server/pom.xml

@@ -85,6 +85,12 @@
             <artifactId>forge-hotel</artifactId>
             <version>${revision}</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-restaurant</artifactId>
+            <version>${revision}</version>
+        </dependency>
         
         <dependency>
             <groupId>com.mdframe.forge</groupId>

+ 80 - 0
forge-server/forge-business/forge-restaurant/pom.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>forge-business</artifactId>
+        <groupId>com.mdframe.forge</groupId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>forge-restaurant</artifactId>
+    <version>${revision}</version>
+    <name>forge-restaurant</name>
+    <description>餐饮业务模块(菜品管理、包房管理、点菜订单、挂账客户、物资管理、供应商管理)</description>
+    
+    <dependencies>
+        
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-web</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-auth</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-orm</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-tenant</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-config</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-id</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-log</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-excel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-starter-file</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mdframe.forge</groupId>
+            <artifactId>forge-flow-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    
+    </dependencies>
+
+</project>

+ 859 - 0
forge-server/forge-business/forge-restaurant/psi_credit_customer.sql

@@ -0,0 +1,859 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server         : 本地
+ Source Server Type    : MySQL
+ Source Server Version : 50737
+ Source Host           : localhost:3306
+ Source Schema         : xg_saas
+
+ Target Server Type    : MySQL
+ Target Server Version : 50737
+ File Encoding         : 65001
+
+ Date: 18/09/2026 15:07:43
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for psi_credit_customer
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_credit_customer`;
+CREATE TABLE `psi_credit_customer`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '录入人ID',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '录入时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` int(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `customer_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '姓名',
+  `mobile` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '手机号',
+  `birthday` date NULL DEFAULT NULL COMMENT '生日',
+  `id_card` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证号',
+  `age` int(3) NULL DEFAULT NULL COMMENT '年龄',
+  `occupation` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '职业',
+  `company_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '公司名称',
+  `address` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '住址',
+  `emergency_contact` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '紧急联系人',
+  `emergency_contact_phone` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '紧急联系人电话',
+  `invoice_title` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开票抬头',
+  `tax_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '税号',
+  `bank_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开户行',
+  `bank_account` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
+  `invoice_address_phone` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开票地址电话',
+  `credit_status` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '挂账状态:0正常挂账,1暂停挂账,2黑名单',
+  `total_arrears_amount` decimal(12, 2) NULL DEFAULT NULL COMMENT '累计欠款金额',
+  `unsettled_bill_count` int(11) NULL DEFAULT 0 COMMENT '未结账单数',
+  `first_credit_time` datetime(0) NULL DEFAULT NULL COMMENT '首次挂账时间',
+  `last_consume_time` datetime(0) NULL DEFAULT NULL COMMENT '最后消费时间',
+  `auditor_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '审核人ID',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '审核状态',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_credit_customer_mobile`(`mobile`) USING BTREE,
+  INDEX `idx_credit_customer_status`(`credit_status`) USING BTREE,
+  INDEX `idx_credit_customer_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '挂账人信息登记' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_library
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_library`;
+CREATE TABLE `psi_dish_library`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `dish_code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品编码',
+  `dish_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜品名称',
+  `type_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '分类ID',
+  `image_url` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品图片,多个用逗号分隔',
+  `unit` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `spec` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规格',
+  `sale_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '销售价',
+  `cost_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '参考成本价',
+  `taste` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '口味',
+  `status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '状态:0启用,1停用',
+  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_library_type_id`(`type_id`) USING BTREE,
+  INDEX `idx_psi_dish_library_code`(`dish_code`) USING BTREE,
+  INDEX `idx_psi_dish_library_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜品库' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_menu`;
+CREATE TABLE `psi_dish_menu`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `is_history` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否为历史菜单(0:否,1:是)',
+  `history_time` datetime(0) NULL DEFAULT NULL COMMENT '成为历史菜单的时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_menu_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜品菜单表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_menu_dish
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_menu_dish`;
+CREATE TABLE `psi_dish_menu_dish`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `sale_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '销售价',
+  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `is_history` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否为历史菜单(0:否,1:是)',
+  `history_time` datetime(0) NULL DEFAULT NULL COMMENT '成为历史菜单的时间',
+  `menu_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '关联菜单id',
+  `dish_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '关联菜品库菜品id',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_menu_dish_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单菜品关联表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_order
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_order`;
+CREATE TABLE `psi_dish_order`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` int(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `order_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '订单编号',
+  `room_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '包房ID',
+  `room_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包房编号',
+  `room_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包房名称',
+  `order_status` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '订单状态:0用餐中,1已结账',
+  `total_amount` decimal(12, 2) NULL DEFAULT NULL COMMENT '账单原金额',
+  `settle_type` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '结账方式:0结账,1折扣结账,2优惠结账',
+  `discount_rate` decimal(8, 2) NULL DEFAULT 100.00 COMMENT '折扣比例',
+  `discount_amount` decimal(12, 2) NULL DEFAULT NULL COMMENT '优惠金额',
+  `payable_amount` decimal(12, 2) NULL DEFAULT NULL COMMENT '实收金额',
+  `settle_time` datetime(0) NULL DEFAULT NULL COMMENT '结账时间',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `taste_remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `special_requirements` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '特殊要求',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_order_room`(`room_id`, `order_status`, `del_flag`) USING BTREE,
+  INDEX `idx_psi_dish_order_no`(`order_no`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '点菜订单表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_order_detail
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_order_detail`;
+CREATE TABLE `psi_dish_order_detail`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人ID',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人ID',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` int(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `order_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单ID',
+  `room_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '包房ID',
+  `dish_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜品ID',
+  `dish_code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品编码',
+  `dish_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品名称',
+  `type_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '分类ID',
+  `type_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '分类名称',
+  `unit` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `spec` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规格',
+  `sale_price` decimal(12, 2) NULL DEFAULT NULL COMMENT '单价',
+  `quantity` int(11) NULL DEFAULT 0 COMMENT '数量,减菜为负数',
+  `amount` decimal(12, 2) NULL DEFAULT NULL COMMENT '金额',
+  `is_add_dish` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '是否加菜:0否,1是',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `dish_sort` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '排序',
+  `reason` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '原因',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_order_detail_order`(`order_id`, `del_flag`) USING BTREE,
+  INDEX `idx_psi_dish_order_detail_dish`(`dish_id`) USING BTREE,
+  INDEX `idx_psi_dish_order_detail_room`(`room_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '点菜订单详情表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_room
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_room`;
+CREATE TABLE `psi_dish_room`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `room_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包房编号',
+  `room_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '包房名称',
+  `capacity` int(11) NULL DEFAULT NULL COMMENT '容纳人数',
+  `min_consumption` decimal(18, 2) NULL DEFAULT NULL COMMENT '最低消费',
+  `status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '状态:0启用,1停用',
+  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `use_status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '使用状态(0:空闲中,1:使用中)',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_room_no`(`room_no`) USING BTREE,
+  INDEX `idx_psi_dish_room_status`(`status`) USING BTREE,
+  INDEX `idx_psi_dish_room_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '包房管理' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_dish_type
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_dish_type`;
+CREATE TABLE `psi_dish_type`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '删除标记:0正常,1删除',
+  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '分类名称',
+  `parent_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '父级ID',
+  `parent_ids` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父级链路',
+  `level` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '层级',
+  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
+  `status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '状态:0启用,1停用',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `tenant_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_psi_dish_type_parent_id`(`parent_id`) USING BTREE,
+  INDEX `idx_psi_dish_type_del_flag`(`del_flag`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜品分类' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_collect_basics
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_collect_basics`;
+CREATE TABLE `psi_management_collect_basics`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `collect_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用编号',
+  `handled_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人',
+  `handled_by_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人部门',
+  `collect_date` datetime(0) NULL DEFAULT NULL COMMENT '领用时间',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用基础表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_collect_basics_return
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_collect_basics_return`;
+CREATE TABLE `psi_management_collect_basics_return`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '申请原因',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用id',
+  `request_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '退回申请人',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用退回' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_collect_basics_return_hi
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_collect_basics_return_hi`;
+CREATE TABLE `psi_management_collect_basics_return_hi`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `collect_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用id',
+  `json_data` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT 'json数据',
+  `return_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '退回申请人',
+  `return_goods` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '退回数据的物品名称',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用退回-历史' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_collect_detailed
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_collect_detailed`;
+CREATE TABLE `psi_management_collect_detailed`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用基础表id',
+  `recipient_agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用人',
+  `collect_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用类型',
+  `goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '物品名称',
+  `surplus_number` double(10, 2) NULL DEFAULT NULL COMMENT '剩余数量',
+  `collect_number` double(10, 2) NULL DEFAULT NULL COMMENT '领用数量',
+  `company` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `current_inventory` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前库存',
+  `is_return` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '0末退回 1已退回',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_collect_detailed_record
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_collect_detailed_record`;
+CREATE TABLE `psi_management_collect_detailed_record`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用基础表id',
+  `recipient_agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用人',
+  `collect_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用类型',
+  `goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '物品名称',
+  `surplus_number` double(10, 2) NULL DEFAULT NULL COMMENT '剩余数量',
+  `collect_number` double(10, 2) NULL DEFAULT NULL COMMENT '领用数量',
+  `company` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `current_inventory` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前库存',
+  `is_return` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '0末退回 1已退回',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  `detail_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '关联的领用详情id',
+  `inventory_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '预扣除库存数据id',
+  `surplus_stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '退回数量',
+  `not_surplus_stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '未退回数量',
+  `returned_num` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '已退回数量',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_contract
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_contract`;
+CREATE TABLE `psi_management_contract`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks1` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户名称',
+  `is_usc_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否有统一社会信用代码',
+  `usc_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '统一社会信用代码',
+  `company_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '注册地址',
+  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '详细地址',
+  `mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '公司电话',
+  `contract_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '合同名称',
+  `signing_date` datetime(0) NULL DEFAULT NULL COMMENT '签订日期',
+  `effective_date` datetime(0) NULL DEFAULT NULL COMMENT '合同生效日期',
+  `end_time` datetime(0) NULL DEFAULT NULL COMMENT '合同终止日期',
+  `remarks2` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2',
+  `contract_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '合同编号',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-合同表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_contract_user
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_contract_user`;
+CREATE TABLE `psi_management_contract_user`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
+  `position` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '职务',
+  `contact_first` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系方式1',
+  `contact_second` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系方式2',
+  `contract_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '物资管理-合同表id',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-合同-客户联系人表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_food_purchase_basic
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_food_purchase_basic`;
+CREATE TABLE `psi_management_food_purchase_basic`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `purchase_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购编号',
+  `handled_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购人',
+  `handled_by_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购人部门',
+  `purchase_time` datetime(0) NULL DEFAULT NULL COMMENT '采购时间',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `total_price` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前采购单的总价',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用基础表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_food_purchase_change
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_food_purchase_change`;
+CREATE TABLE `psi_management_food_purchase_change`  (
+  `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
+  `source_basic_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '对应正式采购单ID',
+  `purchase_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '正式采购单号',
+  `handled_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '采购人',
+  `handled_by_office` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '采购部门',
+  `purchase_time` datetime(0) NULL DEFAULT NULL COMMENT '采购时间',
+  `total_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '采购总金额',
+  `remarks` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `proc_ins_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流程实例ID',
+  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流程定义ID',
+  `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '状态:1暂存 2审批中 4驳回 5通过 等',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `create_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `del_flag` int(11) NULL DEFAULT 0 COMMENT '删除标记 0正常 1删除',
+  `tenant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '租户ID',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_food_purchase_change_source_basic_id`(`source_basic_id`) USING BTREE,
+  INDEX `idx_food_purchase_change_proc_ins_id`(`proc_ins_id`) USING BTREE,
+  INDEX `idx_food_purchase_change_status`(`status`) USING BTREE,
+  INDEX `idx_food_purchase_change_update_time`(`update_time`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '食材采购修改单主表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_food_purchase_change_detail
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_food_purchase_change_detail`;
+CREATE TABLE `psi_management_food_purchase_change_detail`  (
+  `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
+  `change_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '修改单主表ID',
+  `goods_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '采购食材名称',
+  `food_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '食材分类',
+  `purchase_number` decimal(18, 2) NULL DEFAULT NULL COMMENT '采购数量',
+  `unit` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `unit_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '单价',
+  `total_price` decimal(18, 2) NULL DEFAULT NULL COMMENT '总价',
+  `storage_area` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '存储区域',
+  `shelf_life` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `expiry_date` date NULL DEFAULT NULL COMMENT '到期日期',
+  `sort` int(11) NULL DEFAULT NULL COMMENT '排序',
+  `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+  `create_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
+  `update_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `del_flag` int(11) NULL DEFAULT 0 COMMENT '删除标记 0正常 1删除',
+  `tenant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '租户ID',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_food_purchase_change_detail_change_id`(`change_id`) USING BTREE,
+  INDEX `idx_food_purchase_change_detail_sort`(`sort`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '食材采购修改单明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_food_purchase_detail
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_food_purchase_detail`;
+CREATE TABLE `psi_management_food_purchase_detail`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购基础表id',
+  `recipient_agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购人',
+  `food_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '食材分类',
+  `goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购食材名称',
+  `purchase_number` double(10, 2) NULL DEFAULT NULL COMMENT '采购数量',
+  `unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  `storage_area` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '存储区域',
+  `unit_price` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单价',
+  `total_price` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '总价',
+  `expiry_date` date NULL DEFAULT NULL COMMENT '到期日期',
+  `sort` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_loss_basics
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_loss_basics`;
+CREATE TABLE `psi_management_loss_basics`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `collect_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '领用编号',
+  `handled_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人',
+  `handled_by_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人部门',
+  `collect_date` datetime(0) NULL DEFAULT NULL COMMENT '报损时间',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用基础表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_loss_detailed
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_loss_detailed`;
+CREATE TABLE `psi_management_loss_detailed`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报损基础表id',
+  `recipient_agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报损人',
+  `collect_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报损类型',
+  `goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '物品名称',
+  `loss_number` double(10, 2) NULL DEFAULT NULL COMMENT '报损数量',
+  `company` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-领用明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_pruchase_request_basics
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_pruchase_request_basics`;
+CREATE TABLE `psi_management_pruchase_request_basics`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '状态',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `purchase_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购编号',
+  `purchase_sketch` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购简述',
+  `handled_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人',
+  `handled_by_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人部门',
+  `purchase_date` datetime(0) NULL DEFAULT NULL COMMENT '采购时间',
+  `purchase_mode` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购方式',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-采购基础表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_pruchase_request_detailed
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_pruchase_request_detailed`;
+CREATE TABLE `psi_management_pruchase_request_detailed`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购基础表id',
+  `purchaser_agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购人',
+  `procurement_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购部门',
+  `procurement_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购类型',
+  `trade_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
+  `trade_price` double(10, 2) NULL DEFAULT NULL COMMENT '商品单价',
+  `trade_number` double(10, 2) NULL DEFAULT NULL COMMENT '商品数量',
+  `company` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `trade_total_price` double(10, 2) NULL DEFAULT NULL COMMENT '商品总价',
+  `supplier_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商',
+  `is_supplier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '1手填供应商 2选择供应商',
+  `supplier_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商名称',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-采购明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_type
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_type`;
+CREATE TABLE `psi_management_type`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '逻辑删除标记(0:显示;1:隐藏)',
+  `level` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '层级',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报销内容名称',
+  `sort` int(10) NULL DEFAULT NULL COMMENT '序号',
+  `parent_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '父节点id',
+  `parent_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父节点id集合',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-物资类型信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_warehouse_basics
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_warehouse_basics`;
+CREATE TABLE `psi_management_warehouse_basics`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `ware_house_number` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库编号',
+  `ware_house_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库名称',
+  `purchase_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购编号',
+  `ware_house_date` datetime(0) NULL DEFAULT NULL COMMENT '入库时间',
+  `handled_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人',
+  `handled_by_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经办人部门',
+  `proc_ins_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程id',
+  `process_definition_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程信息',
+  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '状态',
+  `contract_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '合同编号',
+  `update_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前修改申请人',
+  `update_cause` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改原因',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-入库基础表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_warehouse_detailed
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_warehouse_detailed`;
+CREATE TABLE `psi_management_warehouse_detailed`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `ware_house_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库类型',
+  `trade_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
+  `trade_price` double(10, 2) NULL DEFAULT NULL COMMENT '商品单价',
+  `trade_number` double(10, 2) NULL DEFAULT NULL COMMENT '商品数量',
+  `company` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '单位',
+  `trade_total_price` double(10, 2) NULL DEFAULT NULL COMMENT '商品总价',
+  `basic_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库基础表id',
+  `ware_house_man` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库人',
+  `ware_house_man_office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库人部门',
+  `actual_price` double(10, 2) NULL DEFAULT NULL COMMENT '实际价格',
+  `source_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库详情数据id',
+  `up_flag` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '修改标记',
+  `current_inventory` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前库存',
+  `warn_num` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '5' COMMENT '警告数量',
+  `warn_flag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '警告标记 0不警告 1警告',
+  `supplier_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商',
+  `is_supplier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '1手填供应商 2选择供应商',
+  `supplier_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商名称',
+  `produce_date` date NULL DEFAULT NULL COMMENT '生产日期',
+  `shelf_life` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期',
+  `spec` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '包装规格',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  `shelf_life_unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '保质期单位:年,月,天',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-入库明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_warehouse_summary
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_warehouse_summary`;
+CREATE TABLE `psi_management_warehouse_summary`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `ware_house_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库类型',
+  `trade_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
+  `trade_number` double(10, 2) NULL DEFAULT NULL COMMENT '商品数量',
+  `detailed_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库详情表id',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-入库汇总表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_management_warehouse_up_hi
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_management_warehouse_up_hi`;
+CREATE TABLE `psi_management_warehouse_up_hi`  (
+  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键ID',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT NULL COMMENT '逻辑删除标识',
+  `remarks` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `warehouse_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入库id',
+  `json_data` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT 'json数据',
+  `endflag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标记  0为原数据 1为已完成数据',
+  `update_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改申请人',
+  `update_trade_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改数据的商品名称',
+  `update_trade_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改数据的商品类型',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '入库数据修改历史表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_supplier
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_supplier`;
+CREATE TABLE `psi_supplier`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '编号',
+  `create_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT 0 COMMENT '删除标记',
+  `remarks` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '备注信息',
+  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
+  `is_usc_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否有社会统一信用代码',
+  `usc_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '统一社会信用代码',
+  `reconciliation_area` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '归属地区',
+  `character_is` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商性质',
+  `zip_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮政编码',
+  `fax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '传真',
+  `tel_phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系电话',
+  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
+  `tenant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '物资-供应商' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_supplier_link
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_supplier_link`;
+CREATE TABLE `psi_supplier_link`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '编号',
+  `create_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` int(1) NULL DEFAULT 0 COMMENT '删除标记',
+  `remarks` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '备注信息',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系人姓名',
+  `office` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门',
+  `position` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '职务',
+  `sex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '性别',
+  `link_phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系方式1',
+  `link_mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系方式2',
+  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'E-mail',
+  `supplier_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '供应商id',
+  `tenant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `supplier_id`(`supplier_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '物资-供应商-联系人' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_type_info
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_type_info`;
+CREATE TABLE `psi_type_info`  (
+  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '主键',
+  `create_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `create_time` datetime(0) NULL DEFAULT NULL,
+  `update_by_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `update_time` datetime(0) NULL DEFAULT NULL,
+  `del_flag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '逻辑删除标记(0:显示;1:隐藏)',
+  `level` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '层级',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报销内容名称',
+  `sort` int(10) NULL DEFAULT NULL COMMENT '序号',
+  `parent_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '父节点id',
+  `parent_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父节点id集合',
+  `business_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '业务类型',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '财务-报销类型信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for psi_user
+-- ----------------------------
+DROP TABLE IF EXISTS `psi_user`;
+CREATE TABLE `psi_user`  (
+  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户id',
+  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名称',
+  `tenant_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '租户id',
+  PRIMARY KEY (`user_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '物资管理-用户表' ROW_FORMAT = Dynamic;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 90 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishLibraryController.java

@@ -0,0 +1,90 @@
+package com.mdframe.forge.business.core.restaurant.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.mdframe.forge.business.core.restaurant.domain.DishLibrary;
+import com.mdframe.forge.business.core.restaurant.dto.DishLibraryDTO;
+import com.mdframe.forge.business.core.restaurant.service.DishLibraryService;
+import com.mdframe.forge.business.core.restaurant.vo.DishLibraryVO;
+import com.mdframe.forge.starter.core.annotation.log.OperationLog;
+import com.mdframe.forge.starter.core.domain.OperationType;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+import com.mdframe.forge.starter.core.domain.RespInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 菜品库管理接口。
+ * <p>
+ * 提供菜品的分页查询、详情查询、新增、编辑、删除、状态切换等操作。
+ * 分页查询支持按分类(含下级)、名称模糊、编码模糊、状态筛选。
+ */
+@RestController
+@RequestMapping("/restaurant/dish")
+public class DishLibraryController {
+
+    @Autowired
+    private DishLibraryService dishLibraryService;
+
+    /**
+     * 分页查询菜品(筛选参数用实体类接收)。
+     */
+    @GetMapping("/page")
+    @OperationLog(module = "菜品库", type = OperationType.QUERY, desc = "分页查询菜品")
+    public RespInfo<IPage<DishLibraryVO>> dishPage(PageQuery pageQuery, DishLibrary query) {
+        return RespInfo.success(dishLibraryService.dishPage(pageQuery, query));
+    }
+
+    /**
+     * 查询菜品详情。
+     */
+    @GetMapping("/detail")
+    @OperationLog(module = "菜品库", type = OperationType.QUERY, desc = "查询菜品详情")
+    public RespInfo<DishLibraryVO> dishDetail(@RequestParam Long id) {
+        return RespInfo.success(dishLibraryService.dishDetail(id));
+    }
+
+    /**
+     * 新增菜品。
+     */
+    @PostMapping
+    @OperationLog(module = "菜品库", type = OperationType.ADD, desc = "新增菜品")
+    public RespInfo<Long> dishCreate(@RequestBody DishLibraryDTO dto) {
+        return RespInfo.success(dishLibraryService.dishCreate(dto));
+    }
+
+    /**
+     * 编辑菜品。
+     */
+    @PutMapping
+    @OperationLog(module = "菜品库", type = OperationType.UPDATE, desc = "编辑菜品")
+    public RespInfo<Void> dishUpdate(@RequestBody DishLibraryDTO dto) {
+        dishLibraryService.dishUpdate(dto);
+        return RespInfo.success();
+    }
+
+    /**
+     * 删除菜品(逻辑删除)。
+     */
+    @PostMapping("/remove")
+    @OperationLog(module = "菜品库", type = OperationType.DELETE, desc = "删除菜品")
+    public RespInfo<Void> dishDelete(@RequestParam Long id) {
+        dishLibraryService.dishDelete(id);
+        return RespInfo.success();
+    }
+
+    /**
+     * 切换菜品状态(启用/禁用)。
+     */
+    @PostMapping("/toggleStatus")
+    @OperationLog(module = "菜品库", type = OperationType.UPDATE, desc = "切换菜品状态")
+    public RespInfo<Void> dishToggleStatus(@RequestParam Long id) {
+        dishLibraryService.dishToggleStatus(id);
+        return RespInfo.success();
+    }
+}

+ 154 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishMenuController.java

@@ -0,0 +1,154 @@
+package com.mdframe.forge.business.core.restaurant.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuAddDishDTO;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuPriceDTO;
+import com.mdframe.forge.business.core.restaurant.service.DishMenuService;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuAvailableDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuHistoryVO;
+import com.mdframe.forge.starter.core.annotation.log.OperationLog;
+import com.mdframe.forge.starter.core.domain.OperationType;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+import com.mdframe.forge.starter.core.domain.RespInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 菜单管理接口。
+ * <p>
+ * 提供当前菜单菜品列表、历史菜单列表、历史详情、可添加菜品、
+ * 点单菜品/分类、添加菜品、全量保存、调价、删除菜品、更新菜单(归档)等操作。
+ */
+@RestController
+@RequestMapping("/restaurant/dishMenu")
+public class DishMenuController {
+
+    @Autowired
+    private DishMenuService dishMenuService;
+
+    /**
+     * 当前菜单菜品列表(分页)。
+     */
+    @GetMapping("/list")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询当前菜单菜品列表")
+    public RespInfo<IPage<DishMenuDishVO>> currentDishList(PageQuery pageQuery,
+                                                            @RequestParam(required = false) Long typeId) {
+        return RespInfo.success(dishMenuService.getCurrentDishList(pageQuery, typeId));
+    }
+
+    /**
+     * 当前菜单全部菜品(不分页,用于前端构建树形表格)。
+     */
+    @GetMapping("/allDishList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询当前菜单全部菜品")
+    public RespInfo<List<DishMenuDishVO>> allDishList() {
+        return RespInfo.success(dishMenuService.getCurrentDishListAll());
+    }
+
+    /**
+     * 历史菜单列表(分页)。
+     */
+    @GetMapping("/historyList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询历史菜单列表")
+    public RespInfo<IPage<DishMenuHistoryVO>> historyList(PageQuery pageQuery) {
+        return RespInfo.success(dishMenuService.getHistoryList(pageQuery));
+    }
+
+    /**
+     * 历史菜单菜品详情(分页)。
+     */
+    @GetMapping("/historyDetailList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询历史菜单菜品详情")
+    public RespInfo<IPage<DishMenuDishVO>> historyDetailList(PageQuery pageQuery,
+                                                              @RequestParam Long menuId) {
+        return RespInfo.success(dishMenuService.getHistoryDetailList(pageQuery, menuId));
+    }
+
+    /**
+     * 可添加菜品列表(排除已加入当前菜单的菜品)。
+     */
+    @GetMapping("/availableDishList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询可添加菜品列表")
+    public RespInfo<List<DishMenuAvailableDishVO>> availableDishList(
+            @RequestParam(required = false) String dishName) {
+        return RespInfo.success(dishMenuService.getAvailableDishList(dishName));
+    }
+
+    /**
+     * 点单菜品列表(菜单价覆盖菜品库价)。
+     */
+    @GetMapping("/orderDishList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询点单菜品列表")
+    public RespInfo<List<DishMenuDishVO>> orderDishList(
+            @RequestParam(required = false) Long typeId,
+            @RequestParam(required = false) String dishName) {
+        return RespInfo.success(dishMenuService.getOrderDishList(typeId, dishName));
+    }
+
+    /**
+     * 点单分类列表(仅含菜单中有菜品的分类)。
+     */
+    @GetMapping("/orderTypeList")
+    @OperationLog(module = "菜单管理", type = OperationType.QUERY, desc = "查询点单分类列表")
+    public RespInfo<List<DishMenuDishVO>> orderTypeList() {
+        return RespInfo.success(dishMenuService.getOrderTypeList());
+    }
+
+    /**
+     * 添加菜品到当前菜单。
+     */
+    @PostMapping("/addDishes")
+    @OperationLog(module = "菜单管理", type = OperationType.ADD, desc = "添加菜品到菜单")
+    public RespInfo<Void> addDishes(@RequestBody List<DishMenuAddDishDTO> dishes) {
+        dishMenuService.addDishes(dishes);
+        return RespInfo.success();
+    }
+
+    /**
+     * 全量保存菜品到当前菜单。
+     */
+    @PostMapping("/saveDishes")
+    @OperationLog(module = "菜单管理", type = OperationType.UPDATE, desc = "全量保存菜单菜品")
+    public RespInfo<Void> saveDishes(@RequestBody List<DishMenuAddDishDTO> dishes) {
+        dishMenuService.saveDishes(dishes);
+        return RespInfo.success();
+    }
+
+    /**
+     * 修改菜单菜品价格。
+     */
+    @PostMapping("/updatePrice")
+    @OperationLog(module = "菜单管理", type = OperationType.UPDATE, desc = "修改菜单菜品价格")
+    public RespInfo<Void> updatePrice(@RequestBody DishMenuPriceDTO priceDTO) {
+        dishMenuService.updatePrice(priceDTO);
+        return RespInfo.success();
+    }
+
+    /**
+     * 从当前菜单删除菜品。
+     */
+    @PostMapping("/remove")
+    @OperationLog(module = "菜单管理", type = OperationType.DELETE, desc = "删除菜单菜品")
+    public RespInfo<Void> deleteDish(@RequestParam Long id) {
+        dishMenuService.deleteDish(id);
+        return RespInfo.success();
+    }
+
+    /**
+     * 更新菜单(归档旧菜单,创建新菜单并复制菜品)。
+     */
+    @PostMapping("/updateMenu")
+    @OperationLog(module = "菜单管理", type = OperationType.UPDATE, desc = "更新菜单(归档旧版本)")
+    public RespInfo<Void> updateMenu(@RequestParam(required = false) String remark) {
+        dishMenuService.updateMenu(remark);
+        return RespInfo.success();
+    }
+}

+ 81 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishRoomController.java

@@ -0,0 +1,81 @@
+package com.mdframe.forge.business.core.restaurant.controller;
+
+import com.mdframe.forge.business.core.restaurant.dto.DishRoomDTO;
+import com.mdframe.forge.business.core.restaurant.service.DishRoomService;
+import com.mdframe.forge.business.core.restaurant.vo.DishRoomVO;
+import com.mdframe.forge.starter.core.annotation.log.OperationLog;
+import com.mdframe.forge.starter.core.domain.OperationType;
+import com.mdframe.forge.starter.core.domain.RespInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 包房管理接口。
+ * <p>
+ * 提供包房的列表查询(不分页)、详情、新增、编辑、删除、状态切换等操作。
+ * 包房以卡片形式展示,支持按名称、编号、状态筛选。
+ */
+@RestController
+@RequestMapping("/restaurant/dishRoom")
+public class DishRoomController {
+
+    @Autowired
+    private DishRoomService dishRoomService;
+
+    /**
+     * 查询包房列表(不分页,卡片展示)。
+     */
+    @GetMapping("/list")
+    @OperationLog(module = "包房管理", type = OperationType.QUERY, desc = "查询包房列表")
+    public RespInfo<List<DishRoomVO>> dishRoomList(
+            @RequestParam(required = false) String roomName,
+            @RequestParam(required = false) String roomNo,
+            @RequestParam(required = false) String status) {
+        return RespInfo.success(dishRoomService.dishRoomList(roomName, roomNo, status));
+    }
+
+    /**
+     * 查询包房详情。
+     */
+    @GetMapping("/detail")
+    @OperationLog(module = "包房管理", type = OperationType.QUERY, desc = "查询包房详情")
+    public RespInfo<DishRoomVO> dishRoomDetail(@RequestParam Long id) {
+        return RespInfo.success(dishRoomService.dishRoomDetail(id));
+    }
+
+    /**
+     * 新增/修改包房。
+     */
+    @PostMapping("/save")
+    @OperationLog(module = "包房管理", type = OperationType.ADD, desc = "保存包房")
+    public RespInfo<Long> dishRoomSave(@RequestBody DishRoomDTO dto) {
+        return RespInfo.success(dishRoomService.dishRoomSave(dto));
+    }
+
+    /**
+     * 删除包房(逻辑删除)。
+     */
+    @PostMapping("/remove")
+    @OperationLog(module = "包房管理", type = OperationType.DELETE, desc = "删除包房")
+    public RespInfo<Void> dishRoomDelete(@RequestParam Long id) {
+        dishRoomService.dishRoomDelete(id);
+        return RespInfo.success();
+    }
+
+    /**
+     * 切换包房状态(启用/停用)。
+     */
+    @PostMapping("/toggleStatus")
+    @OperationLog(module = "包房管理", type = OperationType.UPDATE, desc = "切换包房状态")
+    public RespInfo<Void> dishRoomToggleStatus(@RequestParam Long id) {
+        dishRoomService.dishRoomToggleStatus(id);
+        return RespInfo.success();
+    }
+}

+ 99 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/controller/DishTypeController.java

@@ -0,0 +1,99 @@
+package com.mdframe.forge.business.core.restaurant.controller;
+
+import com.mdframe.forge.business.core.restaurant.dto.DishTypeDTO;
+import com.mdframe.forge.business.core.restaurant.service.DishTypeService;
+import com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO;
+import com.mdframe.forge.starter.core.annotation.log.OperationLog;
+import com.mdframe.forge.starter.core.domain.OperationType;
+import com.mdframe.forge.starter.core.domain.RespInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 菜品分类管理接口。
+ * <p>
+ * 提供菜品分类的树形查询、列表查询、新增、编辑、删除、状态切换等操作。
+ * 分类为树形结构,支持多级分类。
+ */
+@RestController
+@RequestMapping("/restaurant/dishType")
+public class DishTypeController {
+
+    @Autowired
+    private DishTypeService dishTypeService;
+
+    /**
+     * 查询分类树(仅启用状态,用于左侧树形导航和下拉选择)。
+     */
+    @GetMapping("/tree")
+    @OperationLog(module = "菜品分类", type = OperationType.QUERY, desc = "查询分类树")
+    public RespInfo<List<DishTypeTreeVO>> dishTypeTree() {
+        return RespInfo.success(dishTypeService.dishTypeTree());
+    }
+
+    /**
+     * 查询分类列表(平铺,支持名称模糊和状态筛选)。
+     */
+    @GetMapping("/list")
+    @OperationLog(module = "菜品分类", type = OperationType.QUERY, desc = "查询分类列表")
+    public RespInfo<List<DishTypeTreeVO>> dishTypeList(@RequestParam(required = false) String name,
+                                                       @RequestParam(required = false) String status) {
+        return RespInfo.success(dishTypeService.dishTypeList(name, status));
+    }
+
+    /**
+     * 查询分类详情。
+     */
+    @GetMapping("/detail")
+    @OperationLog(module = "菜品分类", type = OperationType.QUERY, desc = "查询分类详情")
+    public RespInfo<DishTypeTreeVO> dishTypeDetail(@RequestParam Long id) {
+        return RespInfo.success(dishTypeService.dishTypeDetail(id));
+    }
+
+    /**
+     * 新增分类。
+     */
+    @PostMapping
+    @OperationLog(module = "菜品分类", type = OperationType.ADD, desc = "新增分类")
+    public RespInfo<Long> dishTypeCreate(@RequestBody DishTypeDTO dto) {
+        return RespInfo.success(dishTypeService.dishTypeCreate(dto));
+    }
+
+    /**
+     * 编辑分类。
+     */
+    @PutMapping
+    @OperationLog(module = "菜品分类", type = OperationType.UPDATE, desc = "编辑分类")
+    public RespInfo<Void> dishTypeUpdate(@RequestBody DishTypeDTO dto) {
+        dishTypeService.dishTypeUpdate(dto);
+        return RespInfo.success();
+    }
+
+    /**
+     * 删除分类(逻辑删除,需无子分类且无关联菜品)。
+     */
+    @PostMapping("/remove")
+    @OperationLog(module = "菜品分类", type = OperationType.DELETE, desc = "删除分类")
+    public RespInfo<Void> dishTypeDelete(@RequestParam Long id) {
+        dishTypeService.dishTypeDelete(id);
+        return RespInfo.success();
+    }
+
+    /**
+     * 切换分类状态(启用/禁用)。
+     */
+    @PostMapping("/toggleStatus")
+    @OperationLog(module = "菜品分类", type = OperationType.UPDATE, desc = "切换分类状态")
+    public RespInfo<Void> dishTypeToggleStatus(@RequestParam Long id) {
+        dishTypeService.dishTypeToggleStatus(id);
+        return RespInfo.success();
+    }
+}

+ 71 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishLibrary.java

@@ -0,0 +1,71 @@
+package com.mdframe.forge.business.core.restaurant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.mdframe.forge.starter.tenant.core.TenantEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 菜品库实体。
+ * <p>
+ * 存储所有菜品的基础信息,每个菜品必须归属一个最子级分类。
+ * 菜品编码(dishCode)自动生成,格式为 CP+时间戳,全局唯一。
+ * 价格字段统一使用分为单位(bigint),前端展示时除以 100 转换为元。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("restaurant_dish_library")
+public class DishLibrary extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID(雪花算法) */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 菜品编码(自动生成 CP+yyyyMMddHHmmss,全局唯一) */
+    private String dishCode;
+
+    /** 菜品名称 */
+    private String dishName;
+
+    /** 分类ID(必须为最子级分类,不能选有下级分类的父分类) */
+    private Long typeId;
+
+    /** 菜品图片(文件ID,通过 getFileUrl 获取访问地址) */
+    private String image;
+
+    /** 单位(如:份、碗、杯) */
+    private String unit;
+
+    /** 规格(如:大份、小份) */
+    private String spec;
+
+    /** 销售价(单位:分) */
+    private Long salePrice;
+
+    /** 参考成本价(单位:分) */
+    private Long costPrice;
+
+    /** 口味(如:微辣、酸甜) */
+    private String taste;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标记(0=未删除,删除时写入主键ID) */
+    @TableLogic(value = "0", delval = "id")
+    private Long delFlag;
+}

+ 44 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishMenu.java

@@ -0,0 +1,44 @@
+package com.mdframe.forge.business.core.restaurant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.mdframe.forge.starter.tenant.core.TenantEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 菜单主表实体。
+ * <p>
+ * 版本归档机制:系统中始终只有一份"当前菜单"(is_history='0')。
+ * 每次更新菜单时,旧菜单归档为历史版本(is_history='1'),然后创建新菜单。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("restaurant_dish_menu")
+public class DishMenu extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID(雪花算法) */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 是否历史菜单:0-当前, 1-历史 */
+    private String isHistory;
+
+    /** 成为历史菜单的时间 */
+    private LocalDateTime historyTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标记(0=未删除,删除时写入主键ID) */
+    @TableLogic(value = "0", delval = "id")
+    private Long delFlag;
+}

+ 103 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishMenuDish.java

@@ -0,0 +1,103 @@
+package com.mdframe.forge.business.core.restaurant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.mdframe.forge.starter.tenant.core.TenantEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 菜单菜品关联实体。
+ * <p>
+ * 菜单级独立定价:salePrice 为菜单价(覆盖菜品库价格),点菜时使用菜单价而非菜品库价。
+ * 调整菜单价不影响菜品库价格,也不影响历史订单。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("restaurant_dish_menu_dish")
+public class DishMenuDish extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID(雪花算法) */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 菜单ID */
+    private Long menuId;
+
+    /** 菜品ID */
+    private Long dishId;
+
+    /** 菜单销售价(单位:分,覆盖菜品库价格) */
+    private Long salePrice;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+
+    /** 是否历史菜单:0-当前, 1-历史 */
+    private String isHistory;
+
+    /** 成为历史菜单的时间 */
+    private LocalDateTime historyTime;
+
+    /** 删除标记(0=未删除,删除时写入主键ID) */
+    @TableLogic(value = "0", delval = "id")
+    private Long delFlag;
+
+    // ==================== 虚拟字段(不映射数据库列) ====================
+
+    /** 菜品编码 */
+    @TableField(exist = false)
+    private String dishCode;
+
+    /** 菜品名称 */
+    @TableField(exist = false)
+    private String dishName;
+
+    /** 分类ID */
+    @TableField(exist = false)
+    private Long typeId;
+
+    /** 分类名称 */
+    @TableField(exist = false)
+    private String typeName;
+
+    /** 菜品图片(文件ID) */
+    @TableField(exist = false)
+    private String image;
+
+    /** 单位 */
+    @TableField(exist = false)
+    private String unit;
+
+    /** 规格 */
+    @TableField(exist = false)
+    private String spec;
+
+    /** 口味 */
+    @TableField(exist = false)
+    private String taste;
+
+    /** 菜品状态 */
+    @TableField(exist = false)
+    private String status;
+
+    /** 菜品库销售价(单位:分) */
+    @TableField(exist = false)
+    private Long librarySalePrice;
+
+    /** 创建人姓名 */
+    @TableField(exist = false)
+    private String createName;
+}

+ 59 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishRoom.java

@@ -0,0 +1,59 @@
+package com.mdframe.forge.business.core.restaurant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.mdframe.forge.starter.tenant.core.TenantEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 包房实体。
+ * <p>
+ * 包房编号(roomNo)自动生成,格式为 BF+时间戳,全局唯一。
+ * 包房名称(roomName)全局唯一。
+ * useStatus 由订单模块自动管理(下单占用、结账/取消释放),手动不可修改。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("restaurant_dish_room")
+public class DishRoom extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID(雪花算法) */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 包房编号(自动生成 BF+yyyyMMddHHmmss,全局唯一) */
+    private String roomNo;
+
+    /** 包房名称 */
+    private String roomName;
+
+    /** 容纳人数 */
+    private Integer capacity;
+
+    /** 最低消费(单位:分) */
+    private Long minConsumption;
+
+    /** 状态:ENABLED-启用, DISABLED-停用 */
+    private String status;
+
+    /** 使用状态:IDLE-空闲, IN_USE-使用中(由订单模块自动管理) */
+    private String useStatus;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标记(0=未删除,删除时写入主键ID) */
+    @TableLogic(value = "0", delval = "id")
+    private Long delFlag;
+}

+ 55 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/domain/DishType.java

@@ -0,0 +1,55 @@
+package com.mdframe.forge.business.core.restaurant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.mdframe.forge.starter.tenant.core.TenantEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 菜品分类实体(树形结构,支持多级分类)。
+ * <p>
+ * 分类通过 parent_id 形成树形层级,顶级分类的 parent_id 为 0。
+ * parent_ids 记录从根到父级的完整链路(逗号分隔),用于快速查询某分类的所有后代。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("restaurant_dish_type")
+public class DishType extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID(雪花算法) */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 分类名称 */
+    private String name;
+
+    /** 父级ID(0=顶级分类) */
+    private Long parentId;
+
+    /** 父级链路(逗号分隔祖先ID,如 1,2,3),用于快速查询所有后代分类 */
+    private String parentIds;
+
+    /** 层级(1=顶级,2=二级,依次递增) */
+    private Integer level;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标记(0=未删除,删除时写入主键ID) */
+    @TableLogic(value = "0", delval = "id")
+    private Long delFlag;
+}

+ 59 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishLibraryDTO.java

@@ -0,0 +1,59 @@
+package com.mdframe.forge.business.core.restaurant.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 菜品库请求DTO。
+ * <p>
+ * 用于新增和编辑菜品时传递参数。
+ * 新增时 id 留空,编辑时 id 必填。
+ * 价格字段统一使用分为单位。
+ */
+@Data
+public class DishLibraryDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品ID(修改时必填,新增时留空) */
+    private Long id;
+
+    /** 菜品编码(不传则自动生成 CP+时间戳) */
+    private String dishCode;
+
+    /** 菜品名称 */
+    private String dishName;
+
+    /** 分类ID(必须为最子级分类) */
+    private Long typeId;
+
+    /** 菜品图片(文件ID) */
+    private String image;
+
+    /** 单位(如:份、碗、杯) */
+    private String unit;
+
+    /** 规格(如:大份、小份) */
+    private String spec;
+
+    /** 销售价(单位:分) */
+    private Long salePrice;
+
+    /** 参考成本价(单位:分) */
+    private Long costPrice;
+
+    /** 口味(如:微辣、酸甜) */
+    private String taste;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+}

+ 31 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishMenuAddDishDTO.java

@@ -0,0 +1,31 @@
+package com.mdframe.forge.business.core.restaurant.dto;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 添加菜品到菜单请求DTO。
+ */
+@Data
+public class DishMenuAddDishDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long dishId;
+
+    /** 菜单销售价(单位:分,不传则默认使用菜品库价格) */
+    private Long salePrice;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+}

+ 25 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishMenuPriceDTO.java

@@ -0,0 +1,25 @@
+package com.mdframe.forge.business.core.restaurant.dto;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 修改菜单菜品价格请求DTO。
+ */
+@Data
+public class DishMenuPriceDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 菜单菜品关联ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 新的菜单销售价(单位:分) */
+    private Long salePrice;
+}

+ 34 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishRoomDTO.java

@@ -0,0 +1,34 @@
+package com.mdframe.forge.business.core.restaurant.dto;
+
+import lombok.Data;
+
+/**
+ * 包房保存 DTO。
+ */
+@Data
+public class DishRoomDTO {
+
+    /** 主键ID(修改时必传) */
+    private Long id;
+
+    /** 包房编号(不填则自动生成) */
+    private String roomNo;
+
+    /** 包房名称(必填) */
+    private String roomName;
+
+    /** 容纳人数 */
+    private Integer capacity;
+
+    /** 最低消费(单位:元,前端传入) */
+    private Long minConsumption;
+
+    /** 状态:ENABLED-启用, DISABLED-停用 */
+    private String status;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+}

+ 37 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/dto/DishTypeDTO.java

@@ -0,0 +1,37 @@
+package com.mdframe.forge.business.core.restaurant.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 菜品分类请求DTO。
+ * <p>
+ * 用于新增和编辑菜品分类时传递参数。
+ * 新增时 id 留空,编辑时 id 必填。
+ */
+@Data
+public class DishTypeDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 分类ID(修改时必填,新增时留空) */
+    private Long id;
+
+    /** 分类名称 */
+    private String name;
+
+    /** 父级ID(0=顶级分类,不传默认 0) */
+    private Long parentId;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+}

+ 78 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishLibraryMapper.java

@@ -0,0 +1,78 @@
+package com.mdframe.forge.business.core.restaurant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.mdframe.forge.business.core.restaurant.domain.DishLibrary;
+import com.mdframe.forge.business.core.restaurant.vo.DishLibraryVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 菜品库Mapper。
+ */
+@Mapper
+public interface DishLibraryMapper extends BaseMapper<DishLibrary> {
+
+    /**
+     * 分页查询菜品(关联分类名称)。
+     * <p>
+     * 支持按分类ID(含所有下级分类)、菜品名称模糊、编码模糊、状态筛选。
+     *
+     * @param page     分页参数
+     * @param tenantId 租户ID
+     * @param query    查询条件实体
+     * @param typeIds  分类ID列表(含下级,由 Service 层计算后传入)
+     * @return 菜品分页数据
+     */
+    IPage<DishLibraryVO> selectPage(Page<DishLibraryVO> page,
+                                    @Param("tenantId") Long tenantId,
+                                    @Param("query") DishLibrary query,
+                                    @Param("typeIds") List<Long> typeIds);
+
+    /**
+     * 查询菜品详情(关联分类名称)。
+     *
+     * @param tenantId 租户ID
+     * @param id       菜品ID
+     * @return 菜品详情
+     */
+    DishLibraryVO selectDetail(@Param("tenantId") Long tenantId,
+                               @Param("id") Long id);
+
+    /**
+     * 检查菜品编码是否重复。
+     *
+     * @param tenantId  租户ID
+     * @param dishCode  菜品编码
+     * @param excludeId 排除的ID(编辑时排除自身,新增时传 null)
+     * @return 重复数量
+     */
+    int checkCodeExists(@Param("tenantId") Long tenantId,
+                        @Param("dishCode") String dishCode,
+                        @Param("excludeId") Long excludeId);
+
+    /**
+     * 检查同分类下菜品名称是否重复。
+     *
+     * @param tenantId  租户ID
+     * @param dishName  菜品名称
+     * @param typeId    分类ID
+     * @param excludeId 排除的ID
+     * @return 重复数量
+     */
+    int checkNameExists(@Param("tenantId") Long tenantId,
+                        @Param("dishName") String dishName,
+                        @Param("typeId") Long typeId,
+                        @Param("excludeId") Long excludeId);
+
+    /**
+     * 统计分类下的菜品数量(删除分类前校验用)。
+     *
+     * @param typeId 分类ID
+     * @return 菜品数量
+     */
+    int countByTypeId(@Param("typeId") Long typeId);
+}

+ 120 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishMenuDishMapper.java

@@ -0,0 +1,120 @@
+package com.mdframe.forge.business.core.restaurant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.mdframe.forge.business.core.restaurant.domain.DishMenuDish;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuAvailableDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuDishVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 菜单菜品关联Mapper。
+ * <p>
+ * 包含菜单管理最复杂的 SQL:4 表 JOIN、NOT EXISTS 排除、菜单价覆盖等。
+ */
+@Mapper
+public interface DishMenuDishMapper extends BaseMapper<DishMenuDish> {
+
+    /**
+     * 查询当前菜单菜品列表(4 表 JOIN,分页)。
+     *
+     * @param page     分页参数
+     * @param tenantId 租户ID
+     * @param menuId   当前菜单ID
+     * @param typeId   分类ID(可选)
+     * @return 菜单菜品列表
+     */
+    IPage<DishMenuDishVO> findCurrentDishList(Page<DishMenuDishVO> page,
+                                               @Param("tenantId") Long tenantId,
+                                               @Param("menuId") Long menuId,
+                                               @Param("typeId") Long typeId);
+
+    /**
+     * 查询历史菜单菜品列表(4 表 JOIN,分页)。
+     *
+     * @param page     分页参数
+     * @param tenantId 租户ID
+     * @param menuId   历史菜单ID
+     * @return 历史菜单菜品列表
+     */
+    IPage<DishMenuDishVO> findHistoryDishList(Page<DishMenuDishVO> page,
+                                               @Param("tenantId") Long tenantId,
+                                               @Param("menuId") Long menuId);
+
+    /**
+     * 查询可添加菜品列表(NOT EXISTS 排除已加入当前菜单的菜品)。
+     *
+     * @param tenantId 租户ID
+     * @param menuId   当前菜单ID
+     * @param dishName 菜品名称(模糊查询)
+     * @return 可添加菜品列表
+     */
+    List<DishMenuAvailableDishVO> findAvailableDishList(@Param("tenantId") Long tenantId,
+                                                         @Param("menuId") Long menuId,
+                                                         @Param("dishName") String dishName);
+
+    /**
+     * 查询点单菜品列表(菜单价覆盖菜品库价,仅启用菜品+启用分类)。
+     *
+     * @param tenantId 租户ID
+     * @param menuId   当前菜单ID
+     * @param typeId   分类ID(可选)
+     * @param dishName 菜品名称(模糊查询)
+     * @return 点单菜品列表
+     */
+    List<DishMenuDishVO> findOrderDishList(@Param("tenantId") Long tenantId,
+                                            @Param("menuId") Long menuId,
+                                            @Param("typeId") Long typeId,
+                                            @Param("dishName") String dishName);
+
+    /**
+     * 查询点单分类列表(仅含菜单中有菜品的分类,DISTINCT)。
+     *
+     * @param tenantId 租户ID
+     * @param menuId   当前菜单ID
+     * @return 分类列表
+     */
+    List<DishMenuDishVO> findOrderTypeList(@Param("tenantId") Long tenantId,
+                                            @Param("menuId") Long menuId);
+
+    /**
+     * 按菜品ID查找当前菜单中的关联记录。
+     *
+     * @param tenantId 租户ID
+     * @param menuId   当前菜单ID
+     * @param dishId   菜品ID
+     * @return 菜单菜品关联记录
+     */
+    DishMenuDish findCurrentByDishId(@Param("tenantId") Long tenantId,
+                                      @Param("menuId") Long menuId,
+                                      @Param("dishId") Long dishId);
+
+    /**
+     * 获取当前菜单最大排序号。
+     *
+     * @param menuId 菜单ID
+     * @return 最大排序号,无记录时返回 null
+     */
+    Integer getMaxSort(@Param("menuId") Long menuId);
+
+    /**
+     * 统计菜单中菜品数量。
+     *
+     * @param menuId 菜单ID
+     * @return 菜品数量
+     */
+    int countByMenuId(@Param("menuId") Long menuId);
+
+    /**
+     * 批量将菜单菜品标记为历史。
+     *
+     * @param menuId      菜单ID
+     * @param historyTime 归档时间
+     */
+    void archiveByMenuId(@Param("menuId") Long menuId,
+                         @Param("historyTime") String historyTime);
+}

+ 32 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishMenuMapper.java

@@ -0,0 +1,32 @@
+package com.mdframe.forge.business.core.restaurant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.mdframe.forge.business.core.restaurant.domain.DishMenu;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuHistoryVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 菜单主表Mapper。
+ */
+@Mapper
+public interface DishMenuMapper extends BaseMapper<DishMenu> {
+
+    /**
+     * 查找当前菜单(is_history='0' or null)。
+     *
+     * @param tenantId 租户ID
+     * @return 当前菜单实体
+     */
+    DishMenu findCurrentMenu(@Param("tenantId") Long tenantId);
+
+    /**
+     * 查询历史菜单列表(分页)。
+     *
+     * @param tenantId 租户ID
+     * @return 历史菜单列表
+     */
+    List<DishMenuHistoryVO> findHistoryList(@Param("tenantId") Long tenantId);
+}

+ 42 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishRoomMapper.java

@@ -0,0 +1,42 @@
+package com.mdframe.forge.business.core.restaurant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.mdframe.forge.business.core.restaurant.domain.DishRoom;
+import com.mdframe.forge.business.core.restaurant.vo.DishRoomVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 包房 Mapper。
+ */
+public interface DishRoomMapper extends BaseMapper<DishRoom> {
+
+    /**
+     * 列表查询(支持名称、编号、状态筛选)。
+     */
+    List<DishRoomVO> selectList(@Param("tenantId") Long tenantId,
+                                @Param("roomName") String roomName,
+                                @Param("roomNo") String roomNo,
+                                @Param("status") String status);
+
+    /**
+     * 详情查询。
+     */
+    DishRoomVO selectDetail(@Param("tenantId") Long tenantId, @Param("id") Long id);
+
+    /**
+     * 检查包房编号是否重复(排除自身)。
+     */
+    int checkNoExists(@Param("tenantId") Long tenantId, @Param("roomNo") String roomNo, @Param("excludeId") Long excludeId);
+
+    /**
+     * 检查包房名称是否重复(排除自身)。
+     */
+    int checkNameExists(@Param("tenantId") Long tenantId, @Param("roomName") String roomName, @Param("excludeId") Long excludeId);
+
+    /**
+     * 检查包房是否正在使用中。
+     */
+    int countInUse(@Param("id") Long id);
+}

+ 58 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/mapper/DishTypeMapper.java

@@ -0,0 +1,58 @@
+package com.mdframe.forge.business.core.restaurant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.mdframe.forge.business.core.restaurant.domain.DishType;
+import com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 菜品分类Mapper。
+ */
+@Mapper
+public interface DishTypeMapper extends BaseMapper<DishType> {
+
+    /**
+     * 查询分类列表(平铺,按排序号升序)。
+     *
+     * @param tenantId 租户ID
+     * @param name     分类名称(模糊匹配,可为 null)
+     * @param status   状态(精确匹配,可为 null)
+     * @return 分类列表
+     */
+    List<DishTypeTreeVO> selectList(@Param("tenantId") Long tenantId,
+                                    @Param("name") String name,
+                                    @Param("status") String status);
+
+    /**
+     * 查询启用分类(树形数据源,Service 层构建树)。
+     *
+     * @param tenantId 租户ID
+     * @return 启用分类列表
+     */
+    List<DishTypeTreeVO> selectEnabledList(@Param("tenantId") Long tenantId);
+
+    /**
+     * 检查同级分类名称是否重复。
+     *
+     * @param tenantId 租户ID
+     * @param name     分类名称
+     * @param parentId 父级ID
+     * @param excludeId 排除的ID(编辑时排除自身,新增时传 null)
+     * @return 重复数量
+     */
+    int checkNameExists(@Param("tenantId") Long tenantId,
+                        @Param("name") String name,
+                        @Param("parentId") Long parentId,
+                        @Param("excludeId") Long excludeId);
+
+    /**
+     * 统计指定分类的子分类数量(删除前校验用)。
+     *
+     * @param id 分类ID
+     * @return 子分类数量
+     */
+    int countChildren(@Param("id") Long id);
+}

+ 69 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishLibraryService.java

@@ -0,0 +1,69 @@
+package com.mdframe.forge.business.core.restaurant.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.mdframe.forge.business.core.restaurant.domain.DishLibrary;
+import com.mdframe.forge.business.core.restaurant.dto.DishLibraryDTO;
+import com.mdframe.forge.business.core.restaurant.vo.DishLibraryVO;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+
+/**
+ * 菜品库服务接口。
+ * <p>
+ * 提供菜品的分页查询、新增、编辑、删除、状态切换等操作。
+ * 每个菜品必须归属一个最子级分类,菜品编码自动生成。
+ * 左树点击父分类时,查询连同其所有下级分类的菜品。
+ */
+public interface DishLibraryService {
+
+    /**
+     * 分页查询菜品。
+     * <p>
+     * 支持按分类ID(含所有下级分类)、菜品名称模糊、编码模糊、状态筛选。
+     *
+     * @param pageQuery 分页参数
+     * @param query     查询条件实体
+     * @return 菜品分页数据
+     */
+    IPage<DishLibraryVO> dishPage(PageQuery pageQuery, DishLibrary query);
+
+    /**
+     * 查询菜品详情。
+     *
+     * @param id 菜品ID
+     * @return 菜品详情
+     */
+    DishLibraryVO dishDetail(Long id);
+
+    /**
+     * 新增菜品。
+     * <p>
+     * 校验:分类必须启用且为最子级;编码全局唯一(不传则自动生成);同分类下名称唯一。
+     *
+     * @param dto 菜品信息
+     * @return 新建菜品ID
+     */
+    Long dishCreate(DishLibraryDTO dto);
+
+    /**
+     * 编辑菜品。
+     * <p>
+     * 校验同新增,但唯一性检查排除自身。
+     *
+     * @param dto 菜品信息(id 必填)
+     */
+    void dishUpdate(DishLibraryDTO dto);
+
+    /**
+     * 删除菜品(逻辑删除)。
+     *
+     * @param id 菜品ID
+     */
+    void dishDelete(Long id);
+
+    /**
+     * 切换菜品状态(ENABLED ↔ DISABLED)。
+     *
+     * @param id 菜品ID
+     */
+    void dishToggleStatus(Long id);
+}

+ 81 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishMenuService.java

@@ -0,0 +1,81 @@
+package com.mdframe.forge.business.core.restaurant.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuAddDishDTO;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuPriceDTO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuAvailableDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuHistoryVO;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+
+import java.util.List;
+
+/**
+ * 菜单管理服务接口。
+ * <p>
+ * 核心机制 — 菜单版本归档:
+ * 每次更新菜单时,旧菜单归档为历史版本,然后创建新菜单。
+ * 菜单级独立定价:salePrice 覆盖菜品库价格,点菜时使用菜单价。
+ */
+public interface DishMenuService {
+
+    /**
+     * 查询当前菜单菜品列表(分页)。
+     */
+    IPage<DishMenuDishVO> getCurrentDishList(PageQuery pageQuery, Long typeId);
+
+    /**
+     * 查询当前菜单全部菜品(不分页,用于前端构建树形表格)。
+     */
+    List<DishMenuDishVO> getCurrentDishListAll();
+
+    /**
+     * 查询历史菜单列表(分页)。
+     */
+    IPage<DishMenuHistoryVO> getHistoryList(PageQuery pageQuery);
+
+    /**
+     * 查询历史菜单菜品详情(分页)。
+     */
+    IPage<DishMenuDishVO> getHistoryDetailList(PageQuery pageQuery, Long menuId);
+
+    /**
+     * 查询可添加菜品列表。
+     */
+    List<DishMenuAvailableDishVO> getAvailableDishList(String dishName);
+
+    /**
+     * 查询点单菜品列表(菜单价覆盖菜品库价)。
+     */
+    List<DishMenuDishVO> getOrderDishList(Long typeId, String dishName);
+
+    /**
+     * 查询点单分类列表(仅含菜单中有菜品的分类)。
+     */
+    List<DishMenuDishVO> getOrderTypeList();
+
+    /**
+     * 添加菜品到当前菜单。
+     */
+    void addDishes(List<DishMenuAddDishDTO> dishes);
+
+    /**
+     * 全量保存菜品到当前菜单(归档旧菜单,创建新菜单)。
+     */
+    void saveDishes(List<DishMenuAddDishDTO> dishes);
+
+    /**
+     * 修改菜单菜品价格。
+     */
+    void updatePrice(DishMenuPriceDTO priceDTO);
+
+    /**
+     * 从当前菜单删除菜品。
+     */
+    void deleteDish(Long id);
+
+    /**
+     * 更新菜单(归档旧菜单,创建新菜单并复制菜品)。
+     */
+    void updateMenu(String remark);
+}

+ 37 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishRoomService.java

@@ -0,0 +1,37 @@
+package com.mdframe.forge.business.core.restaurant.service;
+
+import com.mdframe.forge.business.core.restaurant.dto.DishRoomDTO;
+import com.mdframe.forge.business.core.restaurant.vo.DishRoomVO;
+
+import java.util.List;
+
+/**
+ * 包房服务接口。
+ */
+public interface DishRoomService {
+
+    /**
+     * 列表查询(不分页)。
+     */
+    List<DishRoomVO> dishRoomList(String roomName, String roomNo, String status);
+
+    /**
+     * 详情查询。
+     */
+    DishRoomVO dishRoomDetail(Long id);
+
+    /**
+     * 新增/修改包房。
+     */
+    Long dishRoomSave(DishRoomDTO dto);
+
+    /**
+     * 删除包房(逻辑删除)。
+     */
+    void dishRoomDelete(Long id);
+
+    /**
+     * 切换启用/停用状态。
+     */
+    void dishRoomToggleStatus(Long id);
+}

+ 75 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/DishTypeService.java

@@ -0,0 +1,75 @@
+package com.mdframe.forge.business.core.restaurant.service;
+
+import com.mdframe.forge.business.core.restaurant.dto.DishTypeDTO;
+import com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO;
+
+import java.util.List;
+
+/**
+ * 菜品分类服务接口。
+ * <p>
+ * 提供菜品分类的树形查询、新增、编辑、删除、状态切换等操作。
+ * 分类为树形结构,支持多级分类,删除前需校验无子分类且无关联菜品。
+ */
+public interface DishTypeService {
+
+    /**
+     * 查询分类树(仅启用状态,用于左侧树形导航)。
+     *
+     * @return 分类树列表
+     */
+    List<DishTypeTreeVO> dishTypeTree();
+
+    /**
+     * 查询分类列表(平铺,支持名称模糊和状态筛选)。
+     *
+     * @param name   分类名称(模糊匹配,可为 null)
+     * @param status 状态(精确匹配,可为 null)
+     * @return 分类平铺列表
+     */
+    List<DishTypeTreeVO> dishTypeList(String name, String status);
+
+    /**
+     * 查询分类详情。
+     *
+     * @param id 分类ID
+     * @return 分类详情
+     */
+    DishTypeTreeVO dishTypeDetail(Long id);
+
+    /**
+     * 新增分类。
+     * <p>
+     * 自动维护 parentIds 和 level 字段,自动生成排序号。
+     * 同级分类名称不能重复。
+     *
+     * @param dto 分类信息
+     * @return 新建分类ID
+     */
+    Long dishTypeCreate(DishTypeDTO dto);
+
+    /**
+     * 编辑分类。
+     * <p>
+     * 同级分类名称不能重复(排除自身)。
+     *
+     * @param dto 分类信息(id 必填)
+     */
+    void dishTypeUpdate(DishTypeDTO dto);
+
+    /**
+     * 删除分类(逻辑删除)。
+     * <p>
+     * 删除前校验:不能有子分类,不能有关联菜品。
+     *
+     * @param id 分类ID
+     */
+    void dishTypeDelete(Long id);
+
+    /**
+     * 切换分类状态(ENABLED ↔ DISABLED)。
+     *
+     * @param id 分类ID
+     */
+    void dishTypeToggleStatus(Long id);
+}

+ 258 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishLibraryServiceImpl.java

@@ -0,0 +1,258 @@
+package com.mdframe.forge.business.core.restaurant.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.mdframe.forge.business.core.restaurant.domain.DishLibrary;
+import com.mdframe.forge.business.core.restaurant.domain.DishType;
+import com.mdframe.forge.business.core.restaurant.dto.DishLibraryDTO;
+import com.mdframe.forge.business.core.restaurant.mapper.DishLibraryMapper;
+import com.mdframe.forge.business.core.restaurant.mapper.DishTypeMapper;
+import com.mdframe.forge.business.core.restaurant.service.DishLibraryService;
+import com.mdframe.forge.business.core.restaurant.vo.DishLibraryVO;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+import com.mdframe.forge.starter.core.exception.BusinessException;
+import com.mdframe.forge.starter.core.session.SessionHelper;
+import com.mdframe.forge.starter.tenant.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 菜品库服务实现。
+ * <p>
+ * 核心业务逻辑:
+ * - 左树点击父分类时,连同其所有下级分类的菜品一起查询(通过 parentIds LIKE 实现)
+ * - 菜品编码自动生成(CP+时间戳),全局唯一
+ * - 保存时校验分类必须启用且为最子级
+ * - 同分类下菜品名称不能重复
+ * - 价格字段统一使用分为单位
+ */
+@Slf4j
+@Service
+public class DishLibraryServiceImpl implements DishLibraryService {
+
+    @Autowired
+    private DishLibraryMapper dishLibraryMapper;
+
+    @Autowired
+    private DishTypeMapper dishTypeMapper;
+
+    @Override
+    public IPage<DishLibraryVO> dishPage(PageQuery pageQuery, DishLibrary query) {
+        Page<DishLibraryVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        Long tenantId = resolveTenantId();
+
+        // 如果指定了分类ID,需要连同其所有下级分类的ID一起查询
+        List<Long> typeIds = null;
+        if (query != null && query.getTypeId() != null) {
+            typeIds = collectTypeIds(query.getTypeId());
+        }
+
+        return dishLibraryMapper.selectPage(page, tenantId, query, typeIds);
+    }
+
+    @Override
+    public DishLibraryVO dishDetail(Long id) {
+        if (id == null) {
+            throw new BusinessException("菜品ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishLibraryVO vo = dishLibraryMapper.selectDetail(tenantId, id);
+        if (vo == null) {
+            throw new BusinessException("菜品不存在");
+        }
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long dishCreate(DishLibraryDTO dto) {
+        validateDTO(dto);
+        Long tenantId = resolveTenantId();
+
+        // 校验分类必须启用且为最子级
+        validateDishType(dto.getTypeId());
+
+        // 校验编码唯一性(不传则自动生成)
+        String dishCode = dto.getDishCode();
+        if (!StringUtils.hasText(dishCode)) {
+            dishCode = genDishCode();
+        }
+        int codeCount = dishLibraryMapper.checkCodeExists(tenantId, dishCode, null);
+        if (codeCount > 0) {
+            throw new BusinessException("菜品编码重复,请重新填写");
+        }
+
+        // 校验同分类下名称唯一
+        int nameCount = dishLibraryMapper.checkNameExists(tenantId, dto.getDishName(), dto.getTypeId(), null);
+        if (nameCount > 0) {
+            throw new BusinessException("同一分类下菜品名称重复,请重新填写");
+        }
+
+        DishLibrary entity = new DishLibrary();
+        BeanUtils.copyProperties(dto, entity, "id");
+        entity.setDishCode(dishCode);
+        if (!StringUtils.hasText(entity.getStatus())) {
+            entity.setStatus("ENABLED");
+        }
+        if (entity.getSort() == null) {
+            entity.setSort(0);
+        }
+
+        dishLibraryMapper.insert(entity);
+        log.info("新增菜品: {} ({})", entity.getDishName(), entity.getDishCode());
+        return entity.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishUpdate(DishLibraryDTO dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new BusinessException("菜品ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishLibrary entity = dishLibraryMapper.selectById(dto.getId());
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("菜品不存在");
+        }
+
+        validateDTO(dto);
+
+        // 如果分类变更,重新校验新分类
+        if (dto.getTypeId() != null && !dto.getTypeId().equals(entity.getTypeId())) {
+            validateDishType(dto.getTypeId());
+        }
+
+        // 校验编码唯一性(排除自身)
+        String dishCode = StringUtils.hasText(dto.getDishCode()) ? dto.getDishCode() : entity.getDishCode();
+        int codeCount = dishLibraryMapper.checkCodeExists(tenantId, dishCode, dto.getId());
+        if (codeCount > 0) {
+            throw new BusinessException("菜品编码重复,请重新填写");
+        }
+
+        // 校验同分类下名称唯一(排除自身)
+        Long typeId = dto.getTypeId() != null ? dto.getTypeId() : entity.getTypeId();
+        int nameCount = dishLibraryMapper.checkNameExists(tenantId, dto.getDishName(), typeId, dto.getId());
+        if (nameCount > 0) {
+            throw new BusinessException("同一分类下菜品名称重复,请重新填写");
+        }
+
+        BeanUtils.copyProperties(dto, entity, "id");
+        entity.setDishCode(dishCode);
+
+        dishLibraryMapper.updateById(entity);
+        log.info("编辑菜品: {} ({})", entity.getDishName(), entity.getDishCode());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishDelete(Long id) {
+        if (id == null) {
+            throw new BusinessException("菜品ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishLibrary entity = dishLibraryMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("菜品不存在");
+        }
+        dishLibraryMapper.deleteById(id);
+        log.info("删除菜品: {} ({})", entity.getDishName(), entity.getDishCode());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishToggleStatus(Long id) {
+        if (id == null) {
+            throw new BusinessException("菜品ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishLibrary entity = dishLibraryMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("菜品不存在");
+        }
+        entity.setStatus("ENABLED".equals(entity.getStatus()) ? "DISABLED" : "ENABLED");
+        dishLibraryMapper.updateById(entity);
+        log.info("菜品状态变更: {} -> {}", entity.getDishName(), entity.getStatus());
+    }
+
+    // ==================== 私有方法 ====================
+
+    /**
+     * 收集指定分类及其所有下级分类的ID列表。
+     * <p>
+     * 先查指定分类本身,再通过 parentIds LIKE 查找所有后代分类。
+     * 用于左树点击父分类时,连同其所有下级分类的菜品一起查询。
+     */
+    private List<Long> collectTypeIds(Long typeId) {
+        List<Long> typeIds = new ArrayList<>();
+        typeIds.add(typeId);
+
+        // 查找所有 parentIds 包含该分类ID 的下级分类
+        QueryWrapper<DishType> wrapper = new QueryWrapper<>();
+        wrapper.like("parent_ids", typeId.toString())
+                .eq("del_flag", 0);
+        List<DishType> childTypes = dishTypeMapper.selectList(wrapper);
+        for (DishType child : childTypes) {
+            typeIds.add(child.getId());
+        }
+        return typeIds;
+    }
+
+    /**
+     * 校验菜品分类:必须存在、必须启用、必须为最子级(无下级分类)。
+     */
+    private void validateDishType(Long typeId) {
+        if (typeId == null) {
+            throw new BusinessException("请选择菜品分类");
+        }
+        DishType dishType = dishTypeMapper.selectById(typeId);
+        if (dishType == null) {
+            throw new BusinessException("菜品分类不存在");
+        }
+        if (!"ENABLED".equals(dishType.getStatus())) {
+            throw new BusinessException("请选择启用状态的菜品分类");
+        }
+        int childCount = dishTypeMapper.countChildren(typeId);
+        if (childCount > 0) {
+            throw new BusinessException("菜品分类只能选择最子级分类");
+        }
+    }
+
+    /**
+     * 生成菜品编码:CP + yyyyMMddHHmmss。
+     */
+    private String genDishCode() {
+        return "CP" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+    }
+
+    private void validateDTO(DishLibraryDTO dto) {
+        if (dto == null) {
+            throw new BusinessException("菜品参数不能为空");
+        }
+        if (!StringUtils.hasText(dto.getDishName())) {
+            throw new BusinessException("菜品名称不能为空");
+        }
+        if (dto.getTypeId() == null) {
+            throw new BusinessException("请选择菜品分类");
+        }
+    }
+
+    private Long resolveTenantId() {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            return tenantId;
+        }
+        tenantId = SessionHelper.getTenantId();
+        return tenantId == null ? 1L : tenantId;
+    }
+}

+ 356 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishMenuServiceImpl.java

@@ -0,0 +1,356 @@
+package com.mdframe.forge.business.core.restaurant.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.mdframe.forge.business.core.restaurant.domain.DishLibrary;
+import com.mdframe.forge.business.core.restaurant.domain.DishMenu;
+import com.mdframe.forge.business.core.restaurant.domain.DishMenuDish;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuAddDishDTO;
+import com.mdframe.forge.business.core.restaurant.dto.DishMenuPriceDTO;
+import com.mdframe.forge.business.core.restaurant.mapper.DishLibraryMapper;
+import com.mdframe.forge.business.core.restaurant.mapper.DishMenuDishMapper;
+import com.mdframe.forge.business.core.restaurant.mapper.DishMenuMapper;
+import com.mdframe.forge.business.core.restaurant.service.DishMenuService;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuAvailableDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuDishVO;
+import com.mdframe.forge.business.core.restaurant.vo.DishMenuHistoryVO;
+import com.mdframe.forge.starter.core.domain.PageQuery;
+import com.mdframe.forge.starter.core.exception.BusinessException;
+import com.mdframe.forge.starter.core.session.SessionHelper;
+import com.mdframe.forge.starter.tenant.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 菜单管理服务实现。
+ * <p>
+ * 核心机制 — 菜单版本归档:
+ * <ul>
+ *   <li>系统中始终只有一份"当前菜单"(is_history='0')</li>
+ *   <li>updateMenu 时将当前菜单归档为历史,创建新菜单并复制菜品</li>
+ *   <li>addDishes / updatePrice / deleteDish 直接操作当前菜单,不触发归档</li>
+ * </ul>
+ * <p>
+ * 菜单级独立定价:DishMenuDish.salePrice 覆盖 DishLibrary.salePrice,
+ * 点菜时使用菜单价而非菜品库价,调整菜单价不影响菜品库价格和历史订单。
+ */
+@Slf4j
+@Service
+public class DishMenuServiceImpl implements DishMenuService {
+
+    private static final DateTimeFormatter HISTORY_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Autowired
+    private DishMenuMapper dishMenuMapper;
+
+    @Autowired
+    private DishMenuDishMapper dishMenuDishMapper;
+
+    @Autowired
+    private DishLibraryMapper dishLibraryMapper;
+
+    @Override
+    public IPage<DishMenuDishVO> getCurrentDishList(PageQuery pageQuery, Long typeId) {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        Page<DishMenuDishVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        return dishMenuDishMapper.findCurrentDishList(page, tenantId, currentMenu.getId(), typeId);
+    }
+
+    @Override
+    public List<DishMenuDishVO> getCurrentDishListAll() {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        // 用一个大分页获取全部数据(菜单菜品数量通常不会超过 1000)
+        Page<DishMenuDishVO> page = new Page<>(1, 1000);
+        IPage<DishMenuDishVO> result = dishMenuDishMapper.findCurrentDishList(page, tenantId, currentMenu.getId(), null);
+        return result.getRecords();
+    }
+
+    @Override
+    public IPage<DishMenuHistoryVO> getHistoryList(PageQuery pageQuery) {
+        Long tenantId = resolveTenantId();
+        List<DishMenuHistoryVO> list = dishMenuMapper.findHistoryList(tenantId);
+
+        // 手动分页
+        int total = list.size();
+        int from = (pageQuery.getPageNum() - 1) * pageQuery.getPageSize();
+        int to = Math.min(from + pageQuery.getPageSize(), total);
+        List<DishMenuHistoryVO> pageList = from < total ? list.subList(from, to) : List.of();
+
+        Page<DishMenuHistoryVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        page.setTotal(total);
+        page.setRecords(pageList);
+        return page;
+    }
+
+    @Override
+    public IPage<DishMenuDishVO> getHistoryDetailList(PageQuery pageQuery, Long menuId) {
+        if (menuId == null) {
+            throw new BusinessException("菜单ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        Page<DishMenuDishVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        return dishMenuDishMapper.findHistoryDishList(page, tenantId, menuId);
+    }
+
+    @Override
+    public List<DishMenuAvailableDishVO> getAvailableDishList(String dishName) {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        return dishMenuDishMapper.findAvailableDishList(tenantId, currentMenu.getId(), dishName);
+    }
+
+    @Override
+    public List<DishMenuDishVO> getOrderDishList(Long typeId, String dishName) {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        return dishMenuDishMapper.findOrderDishList(tenantId, currentMenu.getId(), typeId, dishName);
+    }
+
+    @Override
+    public List<DishMenuDishVO> getOrderTypeList() {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        return dishMenuDishMapper.findOrderTypeList(tenantId, currentMenu.getId());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addDishes(List<DishMenuAddDishDTO> dishes) {
+        if (dishes == null || dishes.isEmpty()) {
+            throw new BusinessException("请选择要添加的菜品");
+        }
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        Long menuId = currentMenu.getId();
+
+        Integer maxSort = dishMenuDishMapper.getMaxSort(menuId);
+        if (maxSort == null) {
+            maxSort = 0;
+        }
+
+        for (DishMenuAddDishDTO dto : dishes) {
+            // 检查是否已在菜单中
+            DishMenuDish existing = dishMenuDishMapper.findCurrentByDishId(tenantId, menuId, dto.getDishId());
+            if (existing != null) {
+                log.warn("菜品已在菜单中,跳过: dishId={}", dto.getDishId());
+                continue;
+            }
+
+            // 如果未指定价格,使用菜品库价格
+            Long salePrice = dto.getSalePrice();
+            if (salePrice == null) {
+                DishLibrary dish = dishLibraryMapper.selectById(dto.getDishId());
+                if (dish != null) {
+                    salePrice = dish.getSalePrice();
+                }
+            }
+
+            maxSort++;
+            DishMenuDish menuDish = new DishMenuDish();
+            menuDish.setMenuId(menuId);
+            menuDish.setDishId(dto.getDishId());
+            menuDish.setSalePrice(salePrice);
+            menuDish.setSort(dto.getSort() != null ? dto.getSort() : maxSort);
+            menuDish.setRemark(dto.getRemark());
+            menuDish.setIsHistory("0");
+            dishMenuDishMapper.insert(menuDish);
+        }
+        log.info("添加菜品到菜单: menuId={}, count={}", menuId, dishes.size());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveDishes(List<DishMenuAddDishDTO> dishes) {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = getOrCreateCurrentMenu(tenantId);
+        Long menuId = currentMenu.getId();
+
+        // 收集前端提交的菜品 ID 集合
+        java.util.Set<Long> submittedDishIds = new java.util.HashSet<>();
+        if (dishes != null) {
+            for (DishMenuAddDishDTO dto : dishes) {
+                if (dto.getDishId() != null) {
+                    submittedDishIds.add(dto.getDishId());
+                }
+            }
+        }
+
+        // 1. 删除:数据库中有的但前端没提交的 → 逻辑删除
+        List<DishMenuDish> currentDishes = dishMenuDishMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DishMenuDish>()
+                        .eq("menu_id", menuId)
+                        .eq("del_flag", 0)
+                        .eq("is_history", "0")
+        );
+        for (DishMenuDish current : currentDishes) {
+            if (!submittedDishIds.contains(current.getDishId())) {
+                dishMenuDishMapper.deleteById(current.getId());
+            }
+        }
+
+        // 2. 新增/更新:遍历前端提交的菜品
+        int sort = 0;
+        if (dishes != null) {
+            for (DishMenuAddDishDTO dto : dishes) {
+                DishMenuDish existing = dishMenuDishMapper.findCurrentByDishId(tenantId, menuId, dto.getDishId());
+                if (existing != null) {
+                    // 更新已有记录
+                    Long salePrice = dto.getSalePrice() != null ? dto.getSalePrice() : existing.getSalePrice();
+                    existing.setSalePrice(salePrice);
+                    existing.setSort(dto.getSort() != null ? dto.getSort() : sort);
+                    existing.setRemark(dto.getRemark());
+                    dishMenuDishMapper.updateById(existing);
+                } else {
+                    // 新增记录
+                    Long salePrice = dto.getSalePrice();
+                    if (salePrice == null) {
+                        DishLibrary dish = dishLibraryMapper.selectById(dto.getDishId());
+                        if (dish != null) {
+                            salePrice = dish.getSalePrice();
+                        }
+                    }
+                    DishMenuDish menuDish = new DishMenuDish();
+                    menuDish.setMenuId(menuId);
+                    menuDish.setDishId(dto.getDishId());
+                    menuDish.setSalePrice(salePrice);
+                    menuDish.setSort(dto.getSort() != null ? dto.getSort() : sort);
+                    menuDish.setRemark(dto.getRemark());
+                    menuDish.setIsHistory("0");
+                    dishMenuDishMapper.insert(menuDish);
+                }
+                sort++;
+            }
+        }
+        log.info("全量保存菜单菜品: menuId={}, count={}", menuId, submittedDishIds.size());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePrice(DishMenuPriceDTO priceDTO) {
+        if (priceDTO == null || priceDTO.getId() == null) {
+            throw new BusinessException("菜单菜品ID不能为空");
+        }
+        if (priceDTO.getSalePrice() == null) {
+            throw new BusinessException("价格不能为空");
+        }
+        DishMenuDish menuDish = dishMenuDishMapper.selectById(priceDTO.getId());
+        if (menuDish == null) {
+            throw new BusinessException("菜单菜品不存在");
+        }
+        menuDish.setSalePrice(priceDTO.getSalePrice());
+        dishMenuDishMapper.updateById(menuDish);
+        log.info("修改菜单菜品价格: id={}, salePrice={}", priceDTO.getId(), priceDTO.getSalePrice());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDish(Long id) {
+        if (id == null) {
+            throw new BusinessException("菜单菜品ID不能为空");
+        }
+        DishMenuDish menuDish = dishMenuDishMapper.selectById(id);
+        if (menuDish == null) {
+            throw new BusinessException("菜单菜品不存在");
+        }
+        dishMenuDishMapper.deleteById(id);
+        log.info("从菜单删除菜品: id={}, dishId={}", id, menuDish.getDishId());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateMenu(String remark) {
+        Long tenantId = resolveTenantId();
+        DishMenu currentMenu = dishMenuMapper.findCurrentMenu(tenantId);
+        if (currentMenu == null) {
+            throw new BusinessException("当前没有菜单可以更新");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        String historyTimeStr = now.format(HISTORY_TIME_FMT);
+
+        // 1. 将当前菜单标记为历史
+        currentMenu.setIsHistory("1");
+        currentMenu.setHistoryTime(now);
+        currentMenu.setRemark(remark);
+        dishMenuMapper.updateById(currentMenu);
+
+        // 2. 将当前菜单的所有菜品也标记为历史
+        dishMenuDishMapper.archiveByMenuId(currentMenu.getId(), historyTimeStr);
+
+        // 3. 创建新菜单
+        DishMenu newMenu = new DishMenu();
+        newMenu.setTenantId(tenantId);
+        newMenu.setIsHistory("0");
+        newMenu.setRemark(remark);
+        dishMenuMapper.insert(newMenu);
+
+        // 4. 复制菜品到新菜单(保持价格独立)
+        int dishCount = dishMenuDishMapper.countByMenuId(currentMenu.getId());
+        // 历史菜单的菜品已经被标记为历史,需要查询历史菜品来复制
+        // 注意:countByMenuId 只统计未标记为历史的,所以这里需要直接查历史的
+        // 实际上 archiveByMenuId 已经将所有菜品标记为历史了
+        // 需要查询刚被归档的菜品来复制
+
+        // 直接查询历史菜品记录(用自定义SQL或者通过 MP 查)
+        // 这里用简单方式:查所有属于旧菜单的菜品(包括刚标记为历史的)
+        List<DishMenuDish> archivedDishes = dishMenuDishMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DishMenuDish>()
+                        .eq("menu_id", currentMenu.getId())
+                        .eq("is_history", "1")
+                        .eq("history_time", historyTimeStr)
+                        .eq("del_flag", 0)
+        );
+
+        int sort = 0;
+        for (DishMenuDish archived : archivedDishes) {
+            DishMenuDish newDish = new DishMenuDish();
+            newDish.setMenuId(newMenu.getId());
+            newDish.setDishId(archived.getDishId());
+            newDish.setSalePrice(archived.getSalePrice());
+            newDish.setSort(sort++);
+            newDish.setRemark(archived.getRemark());
+            newDish.setIsHistory("0");
+            dishMenuDishMapper.insert(newDish);
+        }
+
+        log.info("菜单更新归档: oldMenuId={}, newMenuId={}, dishCount={}", currentMenu.getId(), newMenu.getId(), archivedDishes.size());
+    }
+
+    // ==================== 私有方法 ====================
+
+    /**
+     * 获取或创建当前菜单。
+     * <p>
+     * 如果当前没有菜单(首次使用),自动创建一个空菜单。
+     */
+    private DishMenu getOrCreateCurrentMenu(Long tenantId) {
+        DishMenu currentMenu = dishMenuMapper.findCurrentMenu(tenantId);
+        if (currentMenu == null) {
+            currentMenu = new DishMenu();
+            currentMenu.setTenantId(tenantId);
+            currentMenu.setIsHistory("0");
+            currentMenu.setRemark("默认菜单");
+            dishMenuMapper.insert(currentMenu);
+            log.info("自动创建当前菜单: menuId={}, tenantId={}", currentMenu.getId(), tenantId);
+        }
+        return currentMenu;
+    }
+
+    private Long resolveTenantId() {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            return tenantId;
+        }
+        tenantId = SessionHelper.getTenantId();
+        return tenantId == null ? 1L : tenantId;
+    }
+}

+ 173 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishRoomServiceImpl.java

@@ -0,0 +1,173 @@
+package com.mdframe.forge.business.core.restaurant.service.impl;
+
+import com.mdframe.forge.business.core.restaurant.domain.DishRoom;
+import com.mdframe.forge.business.core.restaurant.dto.DishRoomDTO;
+import com.mdframe.forge.business.core.restaurant.mapper.DishRoomMapper;
+import com.mdframe.forge.business.core.restaurant.service.DishRoomService;
+import com.mdframe.forge.business.core.restaurant.vo.DishRoomVO;
+import com.mdframe.forge.starter.core.exception.BusinessException;
+import com.mdframe.forge.starter.core.session.SessionHelper;
+import com.mdframe.forge.starter.tenant.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 包房服务实现。
+ * <p>
+ * 核心业务逻辑:
+ * - 包房编号自动生成(BF+时间戳),全局唯一
+ * - 包房名称全局唯一
+ * - 停用前检查使用状态,使用中不允许停用
+ * - 删除使用逻辑删除
+ */
+@Slf4j
+@Service
+public class DishRoomServiceImpl implements DishRoomService {
+
+    @Autowired
+    private DishRoomMapper dishRoomMapper;
+
+    @Override
+    public List<DishRoomVO> dishRoomList(String roomName, String roomNo, String status) {
+        Long tenantId = resolveTenantId();
+        return dishRoomMapper.selectList(tenantId, roomName, roomNo, status);
+    }
+
+    @Override
+    public DishRoomVO dishRoomDetail(Long id) {
+        if (id == null) {
+            throw new BusinessException("包房ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishRoomVO vo = dishRoomMapper.selectDetail(tenantId, id);
+        if (vo == null) {
+            throw new BusinessException("包房不存在");
+        }
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long dishRoomSave(DishRoomDTO dto) {
+        if (dto == null) {
+            throw new BusinessException("包房参数不能为空");
+        }
+        if (!StringUtils.hasText(dto.getRoomName())) {
+            throw new BusinessException("包房名称不能为空");
+        }
+        Long tenantId = resolveTenantId();
+
+        // 自动生成包房编号
+        String roomNo = dto.getRoomNo();
+        if (!StringUtils.hasText(roomNo)) {
+            roomNo = genRoomNo();
+        }
+
+        // 校验编号唯一性
+        int noCount = dishRoomMapper.checkNoExists(tenantId, roomNo, dto.getId());
+        if (noCount > 0) {
+            throw new BusinessException("包房编号重复,请重新填写");
+        }
+
+        // 校验名称唯一性
+        int nameCount = dishRoomMapper.checkNameExists(tenantId, dto.getRoomName(), dto.getId());
+        if (nameCount > 0) {
+            throw new BusinessException("包房名称重复,请重新填写");
+        }
+
+        if (dto.getId() != null) {
+            // 修改
+            DishRoom entity = dishRoomMapper.selectById(dto.getId());
+            if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+                throw new BusinessException("包房不存在");
+            }
+            BeanUtils.copyProperties(dto, entity, "id");
+            entity.setRoomNo(roomNo);
+            dishRoomMapper.updateById(entity);
+            log.info("修改包房: {} ({})", entity.getRoomName(), entity.getRoomNo());
+        } else {
+            // 新增
+            DishRoom entity = new DishRoom();
+            BeanUtils.copyProperties(dto, entity);
+            entity.setRoomNo(roomNo);
+            entity.setTenantId(tenantId);
+            if (!StringUtils.hasText(entity.getStatus())) {
+                entity.setStatus("ENABLED");
+            }
+            if (!StringUtils.hasText(entity.getUseStatus())) {
+                entity.setUseStatus("IDLE");
+            }
+            if (entity.getSort() == null) {
+                entity.setSort(0);
+            }
+            dishRoomMapper.insert(entity);
+            log.info("新增包房: {} ({})", entity.getRoomName(), entity.getRoomNo());
+            return entity.getId();
+        }
+        return dto.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishRoomDelete(Long id) {
+        if (id == null) {
+            throw new BusinessException("包房ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishRoom entity = dishRoomMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("包房不存在");
+        }
+        dishRoomMapper.deleteById(id);
+        log.info("删除包房: {} ({})", entity.getRoomName(), entity.getRoomNo());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishRoomToggleStatus(Long id) {
+        if (id == null) {
+            throw new BusinessException("包房ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishRoom entity = dishRoomMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("包房不存在");
+        }
+
+        // 使用中不允许停用
+        if ("ENABLED".equals(entity.getStatus()) && "IN_USE".equals(entity.getUseStatus())) {
+            throw new BusinessException("包房正在使用中,无法停用");
+        }
+
+        entity.setStatus("ENABLED".equals(entity.getStatus()) ? "DISABLED" : "ENABLED");
+        dishRoomMapper.updateById(entity);
+        log.info("切换包房状态: {} -> {}", entity.getRoomName(), entity.getStatus());
+    }
+
+    // ==================== 私有方法 ====================
+
+    /**
+     * 生成包房编号:BF + yyyyMMddHHmmss。
+     */
+    private String genRoomNo() {
+        return "BF" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+    }
+
+    private Long resolveTenantId() {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            return tenantId;
+        }
+        tenantId = SessionHelper.getTenantId();
+        return tenantId == null ? 1L : tenantId;
+    }
+}

+ 275 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/service/impl/DishTypeServiceImpl.java

@@ -0,0 +1,275 @@
+package com.mdframe.forge.business.core.restaurant.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.mdframe.forge.business.core.restaurant.domain.DishType;
+import com.mdframe.forge.business.core.restaurant.dto.DishTypeDTO;
+import com.mdframe.forge.business.core.restaurant.mapper.DishLibraryMapper;
+import com.mdframe.forge.business.core.restaurant.mapper.DishTypeMapper;
+import com.mdframe.forge.business.core.restaurant.service.DishTypeService;
+import com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO;
+import com.mdframe.forge.starter.core.exception.BusinessException;
+import com.mdframe.forge.starter.core.session.SessionHelper;
+import com.mdframe.forge.starter.tenant.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 菜品分类服务实现。
+ * <p>
+ * 树形分类管理核心逻辑:
+ * - 新增时自动维护 parentIds(祖先链路)和 level(层级深度)
+ * - 删除前校验无子分类且无关联菜品
+ * - 树形数据通过递归构建,在内存中组装父子关系
+ */
+@Slf4j
+@Service
+public class DishTypeServiceImpl implements DishTypeService {
+
+    @Autowired
+    private DishTypeMapper dishTypeMapper;
+
+    @Autowired
+    private DishLibraryMapper dishLibraryMapper;
+
+    @Override
+    public List<DishTypeTreeVO> dishTypeTree() {
+        Long tenantId = resolveTenantId();
+        List<DishTypeTreeVO> allList = dishTypeMapper.selectEnabledList(tenantId);
+        return buildTree(allList);
+    }
+
+    @Override
+    public List<DishTypeTreeVO> dishTypeList(String name, String status) {
+        Long tenantId = resolveTenantId();
+        return dishTypeMapper.selectList(tenantId, name, status);
+    }
+
+    @Override
+    public DishTypeTreeVO dishTypeDetail(Long id) {
+        if (id == null) {
+            throw new BusinessException("分类ID不能为空");
+        }
+        DishType entity = dishTypeMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), resolveTenantId())) {
+            throw new BusinessException("分类不存在");
+        }
+        DishTypeTreeVO vo = new DishTypeTreeVO();
+        BeanUtils.copyProperties(entity, vo);
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long dishTypeCreate(DishTypeDTO dto) {
+        validateDTO(dto);
+        Long tenantId = resolveTenantId();
+
+        // 校验同级名称不重复
+        Long parentId = dto.getParentId() != null ? dto.getParentId() : 0L;
+        int nameCount = dishTypeMapper.checkNameExists(tenantId, dto.getName(), parentId, null);
+        if (nameCount > 0) {
+            throw new BusinessException("同级分类名称重复,请重新填写");
+        }
+
+        DishType entity = new DishType();
+        BeanUtils.copyProperties(dto, entity, "id");
+        if (entity.getParentId() == null) {
+            entity.setParentId(0L);
+        }
+        if (!StringUtils.hasText(entity.getStatus())) {
+            entity.setStatus("ENABLED");
+        }
+
+        // 自动维护 parentIds 和 level
+        fillParentInfo(entity);
+
+        // 自动生成排序号
+        if (entity.getSort() == null) {
+            entity.setSort(generateSort(entity.getParentId()));
+        }
+
+        dishTypeMapper.insert(entity);
+        log.info("新增菜品分类: {}", entity.getName());
+        return entity.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishTypeUpdate(DishTypeDTO dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new BusinessException("分类ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishType entity = dishTypeMapper.selectById(dto.getId());
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("分类不存在");
+        }
+
+        validateDTO(dto);
+        Long parentId = dto.getParentId() != null ? dto.getParentId() : entity.getParentId();
+        int nameCount = dishTypeMapper.checkNameExists(tenantId, dto.getName(), parentId, dto.getId());
+        if (nameCount > 0) {
+            throw new BusinessException("同级分类名称重复,请重新填写");
+        }
+
+        BeanUtils.copyProperties(dto, entity, "id");
+        if (entity.getParentId() == null) {
+            entity.setParentId(0L);
+        }
+
+        // 重新计算 parentIds 和 level(父级可能变更)
+        fillParentInfo(entity);
+
+        dishTypeMapper.updateById(entity);
+        log.info("编辑菜品分类: {}", entity.getName());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishTypeDelete(Long id) {
+        if (id == null) {
+            throw new BusinessException("分类ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishType entity = dishTypeMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("分类不存在");
+        }
+
+        // 校验无子分类
+        int childCount = dishTypeMapper.countChildren(id);
+        if (childCount > 0) {
+            throw new BusinessException("当前分类存在下级分类,不能删除");
+        }
+
+        // 校验无关联菜品
+        int dishCount = dishLibraryMapper.countByTypeId(id);
+        if (dishCount > 0) {
+            throw new BusinessException("当前分类已被菜品使用,不能删除");
+        }
+
+        dishTypeMapper.deleteById(id);
+        log.info("删除菜品分类: {}", entity.getName());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dishTypeToggleStatus(Long id) {
+        if (id == null) {
+            throw new BusinessException("分类ID不能为空");
+        }
+        Long tenantId = resolveTenantId();
+        DishType entity = dishTypeMapper.selectById(id);
+        if (entity == null || !Objects.equals(entity.getTenantId(), tenantId)) {
+            throw new BusinessException("分类不存在");
+        }
+        entity.setStatus("ENABLED".equals(entity.getStatus()) ? "DISABLED" : "ENABLED");
+        dishTypeMapper.updateById(entity);
+        log.info("切换分类状态: {} -> {}", entity.getName(), entity.getStatus());
+    }
+
+    // ==================== 私有方法 ====================
+
+    /**
+     * 根据父级信息自动填充 parentIds 和 level。
+     * 顶级分类(parentId=0)的 parentIds 为 "0",level 为 1。
+     * 子分类的 parentIds 为 "父级的parentIds,父级ID",level 为 父级level+1。
+     */
+    private void fillParentInfo(DishType entity) {
+        if (entity.getParentId() == null || entity.getParentId() == 0L) {
+            entity.setParentIds("0");
+            entity.setLevel(1);
+        } else {
+            DishType parentInfo = dishTypeMapper.selectById(entity.getParentId());
+            if (parentInfo == null) {
+                throw new BusinessException("父级分类不存在");
+            }
+            if (!StringUtils.hasText(parentInfo.getParentIds())) {
+                entity.setParentIds(parentInfo.getId().toString());
+            } else {
+                entity.setParentIds(parentInfo.getParentIds() + "," + parentInfo.getId());
+            }
+            entity.setLevel(parentInfo.getLevel() + 1);
+        }
+    }
+
+    /**
+     * 生成排序号:取同级最大排序号 + 1。
+     */
+    private int generateSort(Long parentId) {
+        QueryWrapper<DishType> wrapper = new QueryWrapper<>();
+        wrapper.eq("parent_id", parentId != null ? parentId : 0L)
+                .eq("del_flag", 0)
+                .orderByDesc("sort")
+                .last("LIMIT 1");
+        DishType maxSort = dishTypeMapper.selectOne(wrapper);
+        if (maxSort != null && maxSort.getSort() != null) {
+            return maxSort.getSort() + 1;
+        }
+        return 1;
+    }
+
+    /**
+     * 将平铺列表构建为树形结构。
+     * 先找出所有顶级分类(parentId=0),再递归填充子分类。
+     */
+    private List<DishTypeTreeVO> buildTree(List<DishTypeTreeVO> source) {
+        if (source == null || source.isEmpty()) {
+            return new ArrayList<>();
+        }
+        // 找出顶级分类
+        List<DishTypeTreeVO> roots = new ArrayList<>();
+        for (DishTypeTreeVO item : source) {
+            if (item.getParentId() == null || item.getParentId() == 0L) {
+                roots.add(item);
+            }
+        }
+        // 递归填充子分类
+        for (DishTypeTreeVO root : roots) {
+            root.setChildren(getChildren(root.getId(), source));
+        }
+        return roots;
+    }
+
+    /**
+     * 递归获取指定父级下的所有子分类。
+     */
+    private List<DishTypeTreeVO> getChildren(Long parentId, List<DishTypeTreeVO> source) {
+        List<DishTypeTreeVO> children = new ArrayList<>();
+        for (DishTypeTreeVO item : source) {
+            if (parentId.equals(item.getParentId())) {
+                children.add(item);
+            }
+        }
+        for (DishTypeTreeVO child : children) {
+            child.setChildren(getChildren(child.getId(), source));
+        }
+        return children;
+    }
+
+    private void validateDTO(DishTypeDTO dto) {
+        if (dto == null) {
+            throw new BusinessException("分类参数不能为空");
+        }
+        if (!StringUtils.hasText(dto.getName())) {
+            throw new BusinessException("分类名称不能为空");
+        }
+    }
+
+    private Long resolveTenantId() {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            return tenantId;
+        }
+        tenantId = SessionHelper.getTenantId();
+        return tenantId == null ? 1L : tenantId;
+    }
+}

+ 75 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishLibraryVO.java

@@ -0,0 +1,75 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 菜品库展示对象。
+ * <p>
+ * 包含菜品基本信息及关联的分类名称,用于列表和详情展示。
+ * 价格字段为分,前端需除以 100 转换为元显示。
+ */
+@Data
+public class DishLibraryVO {
+
+    /** 菜品ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 菜品编码 */
+    private String dishCode;
+
+    /** 菜品名称 */
+    private String dishName;
+
+    /** 分类ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long typeId;
+
+    /** 分类名称(关联查询) */
+    private String typeName;
+
+    /** 菜品图片(文件ID) */
+    private String image;
+
+    /** 单位 */
+    private String unit;
+
+    /** 规格 */
+    private String spec;
+
+    /** 销售价(单位:分) */
+    private Long salePrice;
+
+    /** 参考成本价(单位:分) */
+    private Long costPrice;
+
+    /** 口味 */
+    private String taste;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+
+    /** 创建人ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long createBy;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 更新人ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long updateBy;
+
+    /** 更新时间 */
+    private Date updateTime;
+}

+ 46 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuAvailableDishVO.java

@@ -0,0 +1,46 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+/**
+ * 可添加菜品展示对象。
+ * <p>
+ * 查询菜品库中尚未加入当前菜单的菜品(NOT EXISTS 排除已选)。
+ */
+@Data
+public class DishMenuAvailableDishVO {
+
+    /** 菜品ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 菜品编码 */
+    private String dishCode;
+
+    /** 菜品名称 */
+    private String dishName;
+
+    /** 分类ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long typeId;
+
+    /** 分类名称 */
+    private String typeName;
+
+    /** 菜品图片(文件ID) */
+    private String image;
+
+    /** 单位 */
+    private String unit;
+
+    /** 规格 */
+    private String spec;
+
+    /** 口味 */
+    private String taste;
+
+    /** 菜品库销售价(单位:分) */
+    private Long salePrice;
+}

+ 76 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuDishVO.java

@@ -0,0 +1,76 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 当前菜单菜品展示对象。
+ * <p>
+ * 4 表 JOIN 查询结果:菜单菜品关联 + 菜品库 + 菜品分类 + 操作人。
+ * 价格字段为分(菜单价 salePrice 覆盖菜品库价 librarySalePrice)。
+ */
+@Data
+public class DishMenuDishVO {
+
+    /** 菜单菜品关联ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 菜单ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long menuId;
+
+    /** 菜品ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long dishId;
+
+    /** 菜品编码 */
+    private String dishCode;
+
+    /** 菜品名称 */
+    private String dishName;
+
+    /** 分类ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long typeId;
+
+    /** 分类名称 */
+    private String typeName;
+
+    /** 菜品图片(文件ID) */
+    private String image;
+
+    /** 单位 */
+    private String unit;
+
+    /** 规格 */
+    private String spec;
+
+    /** 口味 */
+    private String taste;
+
+    /** 菜单销售价(单位:分,覆盖菜品库价格) */
+    private Long salePrice;
+
+    /** 菜品库销售价(单位:分) */
+    private Long librarySalePrice;
+
+    /** 菜品状态(ENABLED/DISABLED,来自菜品库) */
+    private String status;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 备注 */
+    private String remark;
+
+    /** 创建人ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long createBy;
+
+    /** 创建时间 */
+    private Date createTime;
+}

+ 31 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishMenuHistoryVO.java

@@ -0,0 +1,31 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 历史菜单列表展示对象。
+ */
+@Data
+public class DishMenuHistoryVO {
+
+    /** 菜单ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 成为历史的时间 */
+    private LocalDateTime historyTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 菜单中菜品数量 */
+    private Integer dishCount;
+
+    /** 创建时间 */
+    private Date createTime;
+}

+ 38 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishRoomVO.java

@@ -0,0 +1,38 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 包房列表/详情 VO。
+ */
+@Data
+public class DishRoomVO {
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    private String roomNo;
+
+    private String roomName;
+
+    private Integer capacity;
+
+    /** 最低消费(单位:分) */
+    private Long minConsumption;
+
+    private String status;
+
+    private String useStatus;
+
+    private Integer sort;
+
+    private String remark;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 44 - 0
forge-server/forge-business/forge-restaurant/src/main/java/com/mdframe/forge/business/core/restaurant/vo/DishTypeTreeVO.java

@@ -0,0 +1,44 @@
+package com.mdframe.forge.business.core.restaurant.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 菜品分类树形展示对象。
+ * <p>
+ * 包含分类基本信息及子分类列表,用于前端树形组件渲染。
+ * children 字段在 Service 层递归构建。
+ */
+@Data
+public class DishTypeTreeVO {
+
+    /** 分类ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 分类名称 */
+    private String name;
+
+    /** 父级ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long parentId;
+
+    /** 排序号 */
+    private Integer sort;
+
+    /** 状态:ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 子分类列表(Service 层递归填充) */
+    private List<DishTypeTreeVO> children;
+}

+ 103 - 0
forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishLibraryMapper.xml

@@ -0,0 +1,103 @@
+<?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.mdframe.forge.business.core.restaurant.mapper.DishLibraryMapper">
+
+    <resultMap id="DishLibraryVOMap" type="com.mdframe.forge.business.core.restaurant.vo.DishLibraryVO">
+        <id column="id" property="id" />
+        <result column="dish_code" property="dishCode" />
+        <result column="dish_name" property="dishName" />
+        <result column="type_id" property="typeId" />
+        <result column="type_name" property="typeName" />
+        <result column="image" property="image" />
+        <result column="unit" property="unit" />
+        <result column="spec" property="spec" />
+        <result column="sale_price" property="salePrice" />
+        <result column="cost_price" property="costPrice" />
+        <result column="taste" property="taste" />
+        <result column="status" property="status" />
+        <result column="sort" property="sort" />
+        <result column="remark" property="remark" />
+        <result column="create_by" property="createBy" />
+        <result column="create_time" property="createTime" />
+        <result column="update_by" property="updateBy" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <sql id="DishColumns">
+        a.id, a.dish_code, a.dish_name, a.type_id,
+        dt.name AS type_name,
+        a.image, a.unit, a.spec, a.sale_price, a.cost_price,
+        a.taste, a.status, a.sort, a.remark,
+        a.create_by, a.create_time, a.update_by, a.update_time
+    </sql>
+
+    <!-- 分页查询菜品(关联分类名称) -->
+    <select id="selectPage" resultMap="DishLibraryVOMap">
+        SELECT <include refid="DishColumns" />
+        FROM restaurant_dish_library a
+        LEFT JOIN restaurant_dish_type dt ON dt.id = a.type_id AND dt.del_flag = 0
+        WHERE a.tenant_id = #{tenantId}
+          AND a.del_flag = 0
+        <if test="query.typeId != null">
+            AND a.type_id IN
+            <foreach collection="typeIds" item="tid" open="(" separator="," close=")">
+                #{tid}
+            </foreach>
+        </if>
+        <if test="query.dishName != null and query.dishName != ''">
+            AND a.dish_name LIKE CONCAT('%', #{query.dishName}, '%')
+        </if>
+        <if test="query.dishCode != null and query.dishCode != ''">
+            AND a.dish_code LIKE CONCAT('%', #{query.dishCode}, '%')
+        </if>
+        <if test="query.status != null and query.status != ''">
+            AND a.status = #{query.status}
+        </if>
+        ORDER BY a.sort ASC, a.create_time DESC
+    </select>
+
+    <!-- 菜品详情(关联分类名称) -->
+    <select id="selectDetail" resultMap="DishLibraryVOMap">
+        SELECT <include refid="DishColumns" />
+        FROM restaurant_dish_library a
+        LEFT JOIN restaurant_dish_type dt ON dt.id = a.type_id AND dt.del_flag = 0
+        WHERE a.tenant_id = #{tenantId}
+          AND a.del_flag = 0
+          AND a.id = #{id}
+        LIMIT 1
+    </select>
+
+    <!-- 检查菜品编码是否重复 -->
+    <select id="checkCodeExists" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_library
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND dish_code = #{dishCode}
+        <if test="excludeId != null">
+          AND id != #{excludeId}
+        </if>
+    </select>
+
+    <!-- 检查同分类下菜品名称是否重复 -->
+    <select id="checkNameExists" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_library
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND dish_name = #{dishName}
+          AND type_id = #{typeId}
+        <if test="excludeId != null">
+          AND id != #{excludeId}
+        </if>
+    </select>
+
+    <!-- 统计分类下的菜品数量 -->
+    <select id="countByTypeId" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_library
+        WHERE del_flag = 0
+          AND type_id = #{typeId}
+    </select>
+
+</mapper>

+ 162 - 0
forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishMenuDishMapper.xml

@@ -0,0 +1,162 @@
+<?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.mdframe.forge.business.core.restaurant.mapper.DishMenuDishMapper">
+
+    <resultMap id="DishMenuDishVOMap" type="com.mdframe.forge.business.core.restaurant.vo.DishMenuDishVO">
+        <id column="id" property="id" />
+        <result column="menu_id" property="menuId" />
+        <result column="dish_id" property="dishId" />
+        <result column="dish_code" property="dishCode" />
+        <result column="dish_name" property="dishName" />
+        <result column="type_id" property="typeId" />
+        <result column="type_name" property="typeName" />
+        <result column="image" property="image" />
+        <result column="unit" property="unit" />
+        <result column="spec" property="spec" />
+        <result column="taste" property="taste" />
+        <result column="sale_price" property="salePrice" />
+        <result column="library_sale_price" property="librarySalePrice" />
+        <result column="status" property="status" />
+        <result column="sort" property="sort" />
+        <result column="remark" property="remark" />
+        <result column="create_by" property="createBy" />
+        <result column="create_time" property="createTime" />
+    </resultMap>
+
+    <sql id="DishMenuDishColumns">
+        md.id, md.menu_id, md.dish_id,
+        dl.dish_code, dl.dish_name, dl.type_id,
+        dt.name AS type_name,
+        dl.image, dl.unit, dl.spec, dl.taste,
+        md.sale_price,
+        dl.sale_price AS library_sale_price,
+        dl.status,
+        md.sort, md.remark,
+        md.create_by, md.create_time
+    </sql>
+
+    <!-- 当前菜单菜品列表(4 表 JOIN) -->
+    <select id="findCurrentDishList" resultMap="DishMenuDishVOMap">
+        SELECT <include refid="DishMenuDishColumns" />
+        FROM restaurant_dish_menu_dish md
+        LEFT JOIN restaurant_dish_library dl ON dl.id = md.dish_id AND dl.del_flag = 0
+        LEFT JOIN restaurant_dish_type dt ON dt.id = dl.type_id AND dt.del_flag = 0
+        WHERE md.menu_id = #{menuId}
+          AND md.del_flag = 0
+          AND (md.is_history = '0' OR md.is_history IS NULL)
+        <if test="typeId != null">
+            AND dl.type_id = #{typeId}
+        </if>
+        ORDER BY md.sort ASC, md.create_time ASC
+    </select>
+
+    <!-- 历史菜单菜品列表(4 表 JOIN) -->
+    <select id="findHistoryDishList" resultMap="DishMenuDishVOMap">
+        SELECT <include refid="DishMenuDishColumns" />
+        FROM restaurant_dish_menu_dish md
+        LEFT JOIN restaurant_dish_library dl ON dl.id = md.dish_id AND dl.del_flag = 0
+        LEFT JOIN restaurant_dish_type dt ON dt.id = dl.type_id AND dt.del_flag = 0
+        WHERE md.menu_id = #{menuId}
+          AND md.del_flag = 0
+          AND md.is_history = '1'
+        ORDER BY md.sort ASC, md.create_time ASC
+    </select>
+
+    <!-- 可添加菜品列表(NOT EXISTS 排除已加入当前菜单的菜品) -->
+    <select id="findAvailableDishList" resultType="com.mdframe.forge.business.core.restaurant.vo.DishMenuAvailableDishVO">
+        SELECT dl.id, dl.dish_code, dl.dish_name, dl.type_id,
+               dt.name AS type_name,
+               dl.image, dl.unit, dl.spec, dl.taste, dl.sale_price
+        FROM restaurant_dish_library dl
+        LEFT JOIN restaurant_dish_type dt ON dt.id = dl.type_id AND dt.del_flag = 0
+        WHERE dl.tenant_id = #{tenantId}
+          AND dl.del_flag = 0
+          AND dl.status = 'ENABLED'
+          AND NOT EXISTS (
+              SELECT 1 FROM restaurant_dish_menu_dish md
+              WHERE md.menu_id = #{menuId}
+                AND md.dish_id = dl.id
+                AND md.del_flag = 0
+                AND (md.is_history = '0' OR md.is_history IS NULL)
+          )
+        <if test="dishName != null and dishName != ''">
+            AND dl.dish_name LIKE CONCAT('%', #{dishName}, '%')
+        </if>
+        ORDER BY dl.sort ASC, dl.create_time ASC
+    </select>
+
+    <!-- 点单菜品列表(菜单价覆盖菜品库价,仅启用菜品+启用分类) -->
+    <select id="findOrderDishList" resultMap="DishMenuDishVOMap">
+        SELECT <include refid="DishMenuDishColumns" />
+        FROM restaurant_dish_menu_dish md
+        LEFT JOIN restaurant_dish_library dl ON dl.id = md.dish_id AND dl.del_flag = 0
+        LEFT JOIN restaurant_dish_type dt ON dt.id = dl.type_id AND dt.del_flag = 0
+        WHERE md.menu_id = #{menuId}
+          AND md.del_flag = 0
+          AND (md.is_history = '0' OR md.is_history IS NULL)
+          AND dl.status = 'ENABLED'
+          AND dt.status = 'ENABLED'
+        <if test="typeId != null">
+            AND dl.type_id = #{typeId}
+        </if>
+        <if test="dishName != null and dishName != ''">
+            AND dl.dish_name LIKE CONCAT('%', #{dishName}, '%')
+        </if>
+        ORDER BY md.sort ASC, md.create_time ASC
+    </select>
+
+    <!-- 点单分类列表(DISTINCT,仅含菜单中有菜品的分类) -->
+    <select id="findOrderTypeList" resultMap="DishMenuDishVOMap">
+        SELECT DISTINCT dt.id AS type_id, dt.name AS type_name
+        FROM restaurant_dish_menu_dish md
+        LEFT JOIN restaurant_dish_library dl ON dl.id = md.dish_id AND dl.del_flag = 0
+        LEFT JOIN restaurant_dish_type dt ON dt.id = dl.type_id AND dt.del_flag = 0
+        WHERE md.menu_id = #{menuId}
+          AND md.del_flag = 0
+          AND (md.is_history = '0' OR md.is_history IS NULL)
+          AND dl.status = 'ENABLED'
+          AND dt.status = 'ENABLED'
+        ORDER BY dt.sort ASC
+    </select>
+
+    <!-- 按菜品ID查找当前菜单中的关联记录 -->
+    <select id="findCurrentByDishId" resultType="com.mdframe.forge.business.core.restaurant.domain.DishMenuDish">
+        SELECT id, tenant_id, menu_id, dish_id, sale_price, sort, remark,
+               is_history, history_time, del_flag,
+               create_by, create_time, create_dept, update_by, update_time
+        FROM restaurant_dish_menu_dish
+        WHERE menu_id = #{menuId}
+          AND dish_id = #{dishId}
+          AND del_flag = 0
+          AND (is_history = '0' OR is_history IS NULL)
+        LIMIT 1
+    </select>
+
+    <!-- 获取当前菜单最大排序号 -->
+    <select id="getMaxSort" resultType="java.lang.Integer">
+        SELECT MAX(sort)
+        FROM restaurant_dish_menu_dish
+        WHERE menu_id = #{menuId}
+          AND del_flag = 0
+          AND (is_history = '0' OR is_history IS NULL)
+    </select>
+
+    <!-- 统计菜单中菜品数量 -->
+    <select id="countByMenuId" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_menu_dish
+        WHERE menu_id = #{menuId}
+          AND del_flag = 0
+          AND (is_history = '0' OR is_history IS NULL)
+    </select>
+
+    <!-- 批量将菜单菜品标记为历史 -->
+    <update id="archiveByMenuId">
+        UPDATE restaurant_dish_menu_dish
+        SET is_history = '1', history_time = #{historyTime}, update_time = NOW()
+        WHERE menu_id = #{menuId}
+          AND del_flag = 0
+          AND (is_history = '0' OR is_history IS NULL)
+    </update>
+
+</mapper>

+ 28 - 0
forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishMenuMapper.xml

@@ -0,0 +1,28 @@
+<?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.mdframe.forge.business.core.restaurant.mapper.DishMenuMapper">
+
+    <!-- 查找当前菜单(is_history='0' or null) -->
+    <select id="findCurrentMenu" resultType="com.mdframe.forge.business.core.restaurant.domain.DishMenu">
+        SELECT id, tenant_id, is_history, history_time, remark, del_flag,
+               create_by, create_time, create_dept, update_by, update_time
+        FROM restaurant_dish_menu
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND (is_history = '0' OR is_history IS NULL)
+        LIMIT 1
+    </select>
+
+    <!-- 查询历史菜单列表 -->
+    <select id="findHistoryList" resultType="com.mdframe.forge.business.core.restaurant.vo.DishMenuHistoryVO">
+        SELECT m.id, m.history_time, m.remark, m.create_time,
+               (SELECT COUNT(1) FROM restaurant_dish_menu_dish md
+                WHERE md.menu_id = m.id AND md.del_flag = 0) AS dish_count
+        FROM restaurant_dish_menu m
+        WHERE m.tenant_id = #{tenantId}
+          AND m.del_flag = 0
+          AND m.is_history = '1'
+        ORDER BY m.history_time DESC
+    </select>
+
+</mapper>

+ 67 - 0
forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishRoomMapper.xml

@@ -0,0 +1,67 @@
+<?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.mdframe.forge.business.core.restaurant.mapper.DishRoomMapper">
+
+    <!-- 列表查询 -->
+    <select id="selectList" resultType="com.mdframe.forge.business.core.restaurant.vo.DishRoomVO">
+        SELECT id, room_no, room_name, capacity, min_consumption, status, use_status, sort, remark,
+               create_time, update_time
+        FROM restaurant_dish_room
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+        <if test="roomName != null and roomName != ''">
+            AND room_name LIKE CONCAT('%', #{roomName}, '%')
+        </if>
+        <if test="roomNo != null and roomNo != ''">
+            AND room_no LIKE CONCAT('%', #{roomNo}, '%')
+        </if>
+        <if test="status != null and status != ''">
+            AND status = #{status}
+        </if>
+        ORDER BY sort ASC, create_time DESC
+    </select>
+
+    <!-- 详情查询 -->
+    <select id="selectDetail" resultType="com.mdframe.forge.business.core.restaurant.vo.DishRoomVO">
+        SELECT id, room_no, room_name, capacity, min_consumption, status, use_status, sort, remark,
+               create_time, update_time
+        FROM restaurant_dish_room
+        WHERE tenant_id = #{tenantId}
+          AND id = #{id}
+          AND del_flag = 0
+    </select>
+
+    <!-- 检查包房编号重复 -->
+    <select id="checkNoExists" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_room
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND room_no = #{roomNo}
+        <if test="excludeId != null">
+            AND id != #{excludeId}
+        </if>
+    </select>
+
+    <!-- 检查包房名称重复 -->
+    <select id="checkNameExists" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_room
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND room_name = #{roomName}
+        <if test="excludeId != null">
+            AND id != #{excludeId}
+        </if>
+    </select>
+
+    <!-- 检查包房是否使用中 -->
+    <select id="countInUse" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_room
+        WHERE id = #{id}
+          AND del_flag = 0
+          AND use_status = 'IN_USE'
+    </select>
+
+</mapper>

+ 51 - 0
forge-server/forge-business/forge-restaurant/src/main/resources/mapper/business/restaurant/DishTypeMapper.xml

@@ -0,0 +1,51 @@
+<?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.mdframe.forge.business.core.restaurant.mapper.DishTypeMapper">
+
+    <!-- 查询分类列表(平铺,按排序号升序) -->
+    <select id="selectList" resultType="com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO">
+        SELECT id, name, parent_id, sort, status, remark, create_time
+        FROM restaurant_dish_type
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+        <if test="name != null and name != ''">
+            AND name LIKE CONCAT('%', #{name}, '%')
+        </if>
+        <if test="status != null and status != ''">
+            AND status = #{status}
+        </if>
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 查询启用分类(树形数据源) -->
+    <select id="selectEnabledList" resultType="com.mdframe.forge.business.core.restaurant.vo.DishTypeTreeVO">
+        SELECT id, name, parent_id, sort, status, remark, create_time
+        FROM restaurant_dish_type
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND status = 'ENABLED'
+        ORDER BY sort ASC
+    </select>
+
+    <!-- 检查同级分类名称是否重复 -->
+    <select id="checkNameExists" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_type
+        WHERE tenant_id = #{tenantId}
+          AND del_flag = 0
+          AND name = #{name}
+          AND parent_id = #{parentId}
+        <if test="excludeId != null">
+          AND id != #{excludeId}
+        </if>
+    </select>
+
+    <!-- 统计子分类数量 -->
+    <select id="countChildren" resultType="int">
+        SELECT COUNT(1)
+        FROM restaurant_dish_type
+        WHERE del_flag = 0
+          AND parent_id = #{id}
+    </select>
+
+</mapper>

Разлика између датотеке није приказан због своје велике величине
+ 1758 - 0
forge-server/forge-business/forge-restaurant/餐饮模块迁移分析文档.md


+ 1 - 0
forge-server/forge-business/pom.xml

@@ -16,5 +16,6 @@
 	<modules>
 		<module>forge-business-core</module>
 				<module>forge-hotel</module>
+				<module>forge-restaurant</module>
 	</modules>
 </project>