Browse Source

酒店管理。菜品管理、房间管理部分代码

徐滕 2 days atrás
parent
commit
716501350e
58 changed files with 3798 additions and 0 deletions
  1. 4 0
      jeeplus-common/jeeplus-common-core/src/main/java/com/jeeplus/common/constant/AppNameConstants.java
  2. 115 0
      jeeplus-modules/jeeplus-hotel-order/pom.xml
  3. 14 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/JeeplusHotelOrderApplication.java
  4. 83 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/controller/HotelDishCategoryController.java
  5. 210 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/controller/HotelDishController.java
  6. 70 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDish.java
  7. 28 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishAddition.java
  8. 31 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishCategory.java
  9. 32 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishLog.java
  10. 30 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishSpec.java
  11. 23 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishSpecOption.java
  12. 22 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishAdditionMapper.java
  13. 16 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishCategoryMapper.java
  14. 9 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishLogMapper.java
  15. 37 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishMapper.java
  16. 22 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishSpecMapper.java
  17. 22 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishSpecOptionMapper.java
  18. 21 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishAdditionMapper.xml
  19. 66 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishMapper.xml
  20. 21 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishSpecMapper.xml
  21. 21 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishSpecOptionMapper.xml
  22. 122 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/HotelDishCategoryService.java
  23. 410 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/HotelDishService.java
  24. 34 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/dto/HotelDishCategoryDTO.java
  25. 75 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/dto/HotelDishDTO.java
  26. 15 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/DishBatchOperationDTO.java
  27. 42 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/OrderCreateDTO.java
  28. 28 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/OrderDashboardDTO.java
  29. 56 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/controller/HotelConfigController.java
  30. 29 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/domain/HotelBusinessHours.java
  31. 28 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/domain/HotelConfig.java
  32. 9 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/HotelBusinessHoursMapper.java
  33. 14 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/HotelConfigMapper.java
  34. 16 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/xml/HotelConfigMapper.xml
  35. 94 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/service/HotelConfigService.java
  36. 136 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/controller/HotelOrderController.java
  37. 96 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/domain/HotelOrder.java
  38. 43 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/domain/HotelOrderItem.java
  39. 22 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/HotelOrderItemMapper.java
  40. 38 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/HotelOrderMapper.java
  41. 36 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/xml/HotelOrderItemMapper.xml
  42. 98 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/xml/HotelOrderMapper.xml
  43. 400 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/service/HotelOrderService.java
  44. 149 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/controller/HotelRoomController.java
  45. 98 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/controller/HotelRoomTypeController.java
  46. 66 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/domain/HotelRoom.java
  47. 31 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/domain/HotelRoomType.java
  48. 14 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/HotelRoomMapper.java
  49. 11 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/HotelRoomTypeMapper.java
  50. 5 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/xml/HotelRoomMapper.xml
  51. 286 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/service/HotelRoomService.java
  52. 102 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/service/HotelRoomTypeService.java
  53. 99 0
      jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/utils/QrCodeUtils.java
  54. 69 0
      jeeplus-modules/jeeplus-hotel-order/src/main/resources/bootstrap.yml
  55. 19 0
      jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/dish/mapper/HotelDishCategoryMapper.xml
  56. 57 0
      jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/dish/mapper/HotelDishMapper.xml
  57. 53 0
      jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/order/mapper/HotelOrderMapper.xml
  58. 1 0
      jeeplus-modules/pom.xml

+ 4 - 0
jeeplus-common/jeeplus-common-core/src/main/java/com/jeeplus/common/constant/AppNameConstants.java

@@ -49,4 +49,8 @@ public interface AppNameConstants {
      * psi管理模块
      */
     String APP_PSI_MANAGEMENT_MODULES = "jeeplus-psi-management";
+    /**
+     * 酒店点餐模块
+     */
+    String APP_HOTEL_ORDER_MODULES = "jeeplus-hotel-order";
 }

+ 115 - 0
jeeplus-modules/jeeplus-hotel-order/pom.xml

@@ -0,0 +1,115 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jeeplus-modules</artifactId>
+        <groupId>org.jeeplus</groupId>
+        <version>9.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>jeeplus-hotel-order</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jeeplus</groupId>
+            <artifactId>jeeplus-common-mybatis-plus</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <!-- SpringCloud Alibaba Nacos -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <!-- SpringCloud Alibaba Nacos Config -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <!-- SpringCloud Alibaba Sentinel -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+        </dependency>
+
+        <!--添加actuator依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!--spring boot admin依赖-->
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jeeplus</groupId>
+            <artifactId>jeeplus-common-core</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jeeplus</groupId>
+            <artifactId>jeeplus-common-security</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jeeplus</groupId>
+            <artifactId>jeeplus-common-log</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jeeplus</groupId>
+            <artifactId>jeeplus-common-swagger</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <!-- ZXing 二维码生成 -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>${zxing.core}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>${zxing.core}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.2.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+
+                </configuration>
+            </plugin>
+
+        </plugins>
+    </build>
+</project>

+ 14 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/JeeplusHotelOrderApplication.java

@@ -0,0 +1,14 @@
+package com.jeeplus.hotelorder;
+
+import com.jeeplus.core.annotation.JeeplusCloudApplication;
+import org.springframework.boot.SpringApplication;
+import org.springframework.cache.annotation.EnableCaching;
+
+@EnableCaching
+@JeeplusCloudApplication
+public class JeeplusHotelOrderApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(JeeplusHotelOrderApplication.class, args);
+        System.out.println("===== jeeplus-hotel-order 启动成功 =====");
+    }
+}

+ 83 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/controller/HotelDishCategoryController.java

@@ -0,0 +1,83 @@
+package com.jeeplus.hotelorder.dish.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.hotelorder.dish.domain.HotelDishCategory;
+import com.jeeplus.hotelorder.dish.service.HotelDishCategoryService;
+import com.jeeplus.hotelorder.dish.service.dto.HotelDishCategoryDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Api(tags = "菜品分类管理")
+@RestController
+@RequestMapping(value = "/dishCategory")
+public class HotelDishCategoryController {
+
+    @Resource
+    private HotelDishCategoryService hotelDishCategoryService;
+
+    /**
+     * 分页查询分类(带菜品数量)
+     * @param page 分页参数
+     * @return
+     */
+    @ApiOperation(value = "分页查询分类(带菜品数量)")
+    @GetMapping("page")
+    public ResponseEntity<IPage<HotelDishCategoryDTO>> page(Page<HotelDishCategoryDTO> page) {
+        Page<HotelDishCategory> entityPage = new Page<>(page.getCurrent(), page.getSize());
+        IPage<HotelDishCategoryDTO> result = hotelDishCategoryService.findPage(entityPage);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 查询所有启用的分类
+     * @return
+     */
+    @ApiOperation(value = "查询所有启用的分类")
+    @GetMapping("all")
+    public ResponseEntity<List<HotelDishCategoryDTO>> all() {
+        List<HotelDishCategoryDTO> list = hotelDishCategoryService.findAll();
+        return ResponseEntity.ok(list);
+    }
+
+    /**
+     * 新增/编辑分类
+     * @param dto 分类信息
+     * @return
+     */
+    @ApiOperation(value = "新增/编辑分类")
+    @PostMapping("save")
+    public ResponseEntity<String> save(@RequestBody HotelDishCategoryDTO dto) {
+        String result = hotelDishCategoryService.save(dto);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 切换分类状态(启用/禁用)
+     * @param id 分类ID
+     * @return
+     */
+    @ApiOperation(value = "切换分类状态(启用/禁用)")
+    @PostMapping("toggleStatus/{id}")
+    public ResponseEntity<String> toggleStatus(@PathVariable String id) {
+        String result = hotelDishCategoryService.toggleStatus(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 删除分类
+     * @param id 分类ID
+     * @return
+     */
+    @ApiOperation(value = "删除分类")
+    @PostMapping("delete/{id}")
+    public ResponseEntity<String> delete(@PathVariable String id) {
+        String result = hotelDishCategoryService.delete(id);
+        return ResponseEntity.ok(result);
+    }
+}

+ 210 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/controller/HotelDishController.java

@@ -0,0 +1,210 @@
+package com.jeeplus.hotelorder.dish.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.hotelorder.dish.domain.HotelDishAddition;
+import com.jeeplus.hotelorder.dish.domain.HotelDishSpec;
+import com.jeeplus.hotelorder.dish.domain.HotelDishSpecOption;
+import com.jeeplus.hotelorder.dish.service.HotelDishService;
+import com.jeeplus.hotelorder.dish.service.dto.HotelDishDTO;
+import com.jeeplus.hotelorder.dto.DishBatchOperationDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Api(tags = "菜品管理")
+@RestController
+@RequestMapping(value = "/dish")
+public class HotelDishController {
+
+    @Resource
+    private HotelDishService hotelDishService;
+
+    /**
+     * 管理后台-菜品分页查询
+     * @param queryDTO 查询条件
+     * @param page 分页参数
+     * @return
+     */
+    @ApiOperation(value = "管理后台-菜品分页查询")
+    @GetMapping("page")
+    public ResponseEntity<IPage<HotelDishDTO>> page(HotelDishDTO queryDTO, Page<HotelDishDTO> page) {
+        Page<com.jeeplus.hotelorder.dish.domain.HotelDish> entityPage = new Page<>(page.getCurrent(), page.getSize());
+        IPage<HotelDishDTO> result = hotelDishService.findAdminPage(entityPage, queryDTO);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 客户端-菜品分页查询(在售且未售罄)
+     * @param queryDTO 查询条件
+     * @param page 分页参数
+     * @return
+     */
+    @ApiOperation(value = "客户端-菜品分页查询(在售且未售罄)")
+    @GetMapping("clientPage")
+    public ResponseEntity<IPage<HotelDishDTO>> clientPage(HotelDishDTO queryDTO, Page<HotelDishDTO> page) {
+        Page<com.jeeplus.hotelorder.dish.domain.HotelDish> entityPage = new Page<>(page.getCurrent(), page.getSize());
+        IPage<HotelDishDTO> result = hotelDishService.findClientPage(entityPage, queryDTO);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 查询菜品详情(含规格和加料)
+     * @param id 菜品ID
+     * @return
+     */
+    @ApiOperation(value = "查询菜品详情(含规格和加料)")
+    @GetMapping("detail/{id}")
+    public ResponseEntity<HotelDishDTO> getDetail(@PathVariable String id) {
+        HotelDishDTO dto = hotelDishService.getDetail(id);
+        return ResponseEntity.ok(dto);
+    }
+
+    /**
+     * 新增/编辑菜品
+     * @param dto 菜品信息
+     * @return
+     */
+    @ApiOperation(value = "新增/编辑菜品")
+    @PostMapping("save")
+    public ResponseEntity<String> saveDish(@RequestBody HotelDishDTO dto) {
+        String result = hotelDishService.saveDish(dto);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 删除菜品(含级联删除规格和加料)
+     * @param id 菜品ID
+     * @return
+     */
+    @ApiOperation(value = "删除菜品(含级联删除规格和加料)")
+    @PostMapping("delete/{id}")
+    public ResponseEntity<String> deleteDish(@PathVariable String id) {
+        String result = hotelDishService.deleteDish(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 更新菜品状态(上架/下架)
+     * @param dto 包含id和status
+     * @return
+     */
+    @ApiOperation(value = "更新菜品状态(上架/下架)")
+    @PostMapping("updateStatus")
+    public ResponseEntity<String> updateStatus(@RequestBody HotelDishDTO dto) {
+        String result = hotelDishService.updateStatus(dto.getId(), dto.getStatus());
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 标记售罄
+     * @param id 菜品ID
+     * @return
+     */
+    @ApiOperation(value = "标记售罄")
+    @PostMapping("soldOut/{id}")
+    public ResponseEntity<String> markSoldOut(@PathVariable String id) {
+        String result = hotelDishService.markSoldOut(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 恢复上架(取消售罄)
+     * @param id 菜品ID
+     * @return
+     */
+    @ApiOperation(value = "恢复上架(取消售罄)")
+    @PostMapping("restore/{id}")
+    public ResponseEntity<String> restoreDish(@PathVariable String id) {
+        String result = hotelDishService.restoreDish(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 批量操作(上架/下架/售罄/恢复/删除)
+     * @param dto 批量操作参数
+     * @return
+     */
+    @ApiOperation(value = "批量操作(上架/下架/售罄/恢复/删除)")
+    @PostMapping("batch")
+    public ResponseEntity<String> batch(@RequestBody DishBatchOperationDTO dto) {
+        String result = hotelDishService.batchOperate(dto.getIds(), dto.getOperation());
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 保存菜品规格组
+     * @param spec 规格组信息
+     * @return
+     */
+    @ApiOperation(value = "保存菜品规格组")
+    @PostMapping("spec/save")
+    public ResponseEntity<String> saveSpec(@RequestBody HotelDishSpec spec) {
+        String result = hotelDishService.saveSpec(spec);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 删除菜品规格组(含级联删除选项)
+     * @param id 规格组ID
+     * @return
+     */
+    @ApiOperation(value = "删除菜品规格组(含级联删除选项)")
+    @PostMapping("spec/delete/{id}")
+    public ResponseEntity<String> deleteSpec(@PathVariable String id) {
+        String result = hotelDishService.deleteSpec(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 保存规格选项
+     * @param option 规格选项信息
+     * @return
+     */
+    @ApiOperation(value = "保存规格选项")
+    @PostMapping("specOption/save")
+    public ResponseEntity<String> saveSpecOption(@RequestBody HotelDishSpecOption option) {
+        String result = hotelDishService.saveSpecOption(option);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 保存加料
+     * @param addition 加料信息
+     * @return
+     */
+    @ApiOperation(value = "保存加料")
+    @PostMapping("addition/save")
+    public ResponseEntity<String> saveAddition(@RequestBody HotelDishAddition addition) {
+        String result = hotelDishService.saveAddition(addition);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 删除加料
+     * @param id 加料ID
+     * @return
+     */
+    @ApiOperation(value = "删除加料")
+    @PostMapping("addition/delete/{id}")
+    public ResponseEntity<String> deleteAddition(@PathVariable String id) {
+        String result = hotelDishService.deleteAddition(id);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 销量排行
+     * @param limit 数量限制
+     * @return
+     */
+    @ApiOperation(value = "销量排行")
+    @GetMapping("ranking")
+    public ResponseEntity<List<HotelDishDTO>> salesRanking(@RequestParam(defaultValue = "10") int limit) {
+        List<HotelDishDTO> list = hotelDishService.salesRanking(limit);
+        return ResponseEntity.ok(list);
+    }
+}

+ 70 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDish.java

@@ -0,0 +1,70 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import com.jeeplus.core.query.Query;
+import com.jeeplus.core.query.QueryType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish")
+public class HotelDish extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品名称 */
+    @Query(tableColumn = "a.name", type = QueryType.LIKE)
+    private String name;
+
+    /** 分类ID */
+    private String categoryId;
+
+    /** 基础价格 */
+    private BigDecimal price;
+
+    /** 菜品图片URL */
+    private String image;
+
+    /** 菜品描述 */
+    private String description;
+
+    /** 标签(逗号分隔) */
+    private String tags;
+
+    /** 预计制作时长(分钟) */
+    private Integer prepTime;
+
+    /** 月销量 */
+    private Integer sales;
+
+    /** 状态:ON_SALE-在售, OFF_SHELF-下架 */
+    @Query(tableColumn = "a.status", type = QueryType.EQ)
+    private String status;
+
+    /** 是否售罄(0正常 1售罄) */
+    private Integer soldOut;
+
+    /** 是否主厨推荐(0否 1是) */
+    private Integer isRecommended;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+
+    /** 分类名称(非DB字段,查询用) */
+    @TableField(exist = false)
+    private String categoryName;
+
+    /** 规格组列表(非DB字段) */
+    @TableField(exist = false)
+    private List<HotelDishSpec> specGroups;
+
+    /** 加料列表(非DB字段) */
+    @TableField(exist = false)
+    private List<HotelDishAddition> additions;
+}

+ 28 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishAddition.java

@@ -0,0 +1,28 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish_addition")
+public class HotelDishAddition extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品ID */
+    private String dishId;
+
+    /** 加料名称 */
+    private String name;
+
+    /** 加料价格 */
+    private BigDecimal extraPrice;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+}

+ 31 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishCategory.java

@@ -0,0 +1,31 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish_category")
+public class HotelDishCategory extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 分类名称 */
+    private String name;
+
+    /** 图标(emoji) */
+    private String icon;
+
+    /** 排序号 */
+    private Integer sortOrder;
+
+    /** 状态: ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 菜品数量(非DB字段,查询用) */
+    @TableField(exist = false)
+    private Integer dishCount;
+}

+ 32 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishLog.java

@@ -0,0 +1,32 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish_log")
+public class HotelDishLog extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品ID */
+    private String dishId;
+
+    /** 菜品名称(冗余) */
+    private String dishName;
+
+    /** 操作类型: CREATE/EDIT/DELETE/ON_SALE/OFF_SHELF/SOLD_OUT/RESTORE */
+    private String operationType;
+
+    /** 变更前值(JSON) */
+    private String oldValue;
+
+    /** 变更后值(JSON) */
+    private String newValue;
+
+    /** 备注 */
+    private String remark;
+}

+ 30 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishSpec.java

@@ -0,0 +1,30 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish_spec")
+public class HotelDishSpec extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 菜品ID */
+    private String dishId;
+
+    /** 规格组名称(如: 份量、口味) */
+    private String groupName;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+
+    /** 规格选项列表(非DB字段) */
+    @TableField(exist = false)
+    private List<HotelDishSpecOption> options;
+}

+ 23 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/domain/HotelDishSpecOption.java

@@ -0,0 +1,23 @@
+package com.jeeplus.hotelorder.dish.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_dish_spec_option")
+public class HotelDishSpecOption extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 规格组ID */
+    private String specId;
+
+    /** 选项名称(如: 大份、小份) */
+    private String optionName;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+}

+ 22 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishAdditionMapper.java

@@ -0,0 +1,22 @@
+package com.jeeplus.hotelorder.dish.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.dish.domain.HotelDishAddition;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface HotelDishAdditionMapper extends BaseMapper<HotelDishAddition> {
+
+    /**
+     * 根据菜品ID查询加料
+     */
+    List<HotelDishAddition> selectByDishId(@Param("dishId") String dishId);
+
+    /**
+     * 根据菜品ID删除加料(软删除)
+     */
+    int deleteByDishId(@Param("dishId") String dishId);
+}

+ 16 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishCategoryMapper.java

@@ -0,0 +1,16 @@
+package com.jeeplus.hotelorder.dish.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.jeeplus.hotelorder.dish.domain.HotelDishCategory;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HotelDishCategoryMapper extends BaseMapper<HotelDishCategory> {
+
+    /**
+     * 分页查询分类(带菜品数量统计)
+     */
+    IPage<HotelDishCategory> selectCategoryWithCount(Page<HotelDishCategory> page);
+}

+ 9 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishLogMapper.java

@@ -0,0 +1,9 @@
+package com.jeeplus.hotelorder.dish.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.dish.domain.HotelDishLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HotelDishLogMapper extends BaseMapper<HotelDishLog> {
+}

+ 37 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishMapper.java

@@ -0,0 +1,37 @@
+package com.jeeplus.hotelorder.dish.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.jeeplus.hotelorder.dish.domain.HotelDish;
+import com.jeeplus.hotelorder.order.domain.HotelOrderItem;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface HotelDishMapper extends BaseMapper<HotelDish> {
+
+    /**
+     * 客户端菜品列表(只查在售)
+     */
+    IPage<HotelDish> selectClientList(Page<HotelDish> page, @Param("categoryId") String categoryId,
+                                      @Param("keyword") String keyword);
+
+    /**
+     * 管理后台菜品分页
+     */
+    IPage<HotelDish> selectAdminPage(Page<HotelDish> page, @Param("categoryId") String categoryId,
+                                     @Param("status") String status, @Param("name") String name);
+
+    /**
+     * 销量排行
+     */
+    List<HotelDish> selectSalesRanking(@Param("limit") int limit);
+
+    /**
+     * 批量更新销量
+     */
+    int batchUpdateSales(@Param("items") List<HotelOrderItem> items);
+}

+ 22 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishSpecMapper.java

@@ -0,0 +1,22 @@
+package com.jeeplus.hotelorder.dish.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.dish.domain.HotelDishSpec;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface HotelDishSpecMapper extends BaseMapper<HotelDishSpec> {
+
+    /**
+     * 根据菜品ID查询规格组
+     */
+    List<HotelDishSpec> selectByDishId(@Param("dishId") String dishId);
+
+    /**
+     * 根据菜品ID删除规格组(软删除)
+     */
+    int deleteByDishId(@Param("dishId") String dishId);
+}

+ 22 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/HotelDishSpecOptionMapper.java

@@ -0,0 +1,22 @@
+package com.jeeplus.hotelorder.dish.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.dish.domain.HotelDishSpecOption;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface HotelDishSpecOptionMapper extends BaseMapper<HotelDishSpecOption> {
+
+    /**
+     * 根据规格组ID查询选项
+     */
+    List<HotelDishSpecOption> selectBySpecId(@Param("specId") String specId);
+
+    /**
+     * 根据规格组ID删除选项(软删除)
+     */
+    int deleteBySpecId(@Param("specId") String specId);
+}

+ 21 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishAdditionMapper.xml

@@ -0,0 +1,21 @@
+<?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.jeeplus.hotelorder.dish.mapper.HotelDishAdditionMapper">
+
+    <!-- 根据菜品ID查询加料 -->
+    <select id="selectByDishId" resultType="com.jeeplus.hotelorder.dish.domain.HotelDishAddition">
+        SELECT id, dish_id as dishId, name, extra_price as extraPrice, sort_order as sortOrder,
+               create_time as createTime, update_time as updateTime,
+               del_flag as delFlag, tenant_id as tenantId
+        FROM hotel_dish_addition
+        WHERE del_flag = 0 AND dish_id = #{dishId}
+        ORDER BY sort_order ASC
+    </select>
+
+    <!-- 根据菜品ID删除加料 -->
+    <update id="deleteByDishId">
+        UPDATE hotel_dish_addition SET del_flag = 1, update_time = NOW()
+        WHERE dish_id = #{dishId} AND del_flag = 0
+    </update>
+
+</mapper>

+ 66 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishMapper.xml

@@ -0,0 +1,66 @@
+<?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.jeeplus.hotelorder.dish.mapper.HotelDishMapper">
+
+    <sql id="dish_Column_List">
+        a.id, a.name, a.category_id as categoryId, a.price,
+        a.image, a.description, a.tags, a.prep_time as prepTime,
+        a.sales, a.status, a.sold_out as soldOut,
+        a.is_recommended as isRecommended, a.sort_order as sortOrder,
+        a.create_time as createTime, a.create_by_id as createById,
+        a.update_time as updateTime, a.update_by_id as updateById,
+        a.del_flag as delFlag, a.tenant_id as tenantId
+    </sql>
+
+    <!-- 客户端菜品列表(只查在售 + 分类筛选) -->
+    <select id="selectClientList" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT <include refid="dish_Column_List"/>
+        FROM hotel_dish a
+        WHERE a.del_flag = 0 AND a.status = 'ON_SALE' AND (a.sold_out IS NULL OR a.sold_out = 0)
+        <if test="categoryId != null and categoryId != ''">
+            AND a.category_id = #{categoryId}
+        </if>
+        <if test="keyword != null and keyword != ''">
+            AND a.name LIKE CONCAT('%', #{keyword}, '%')
+        </if>
+        ORDER BY a.is_recommended DESC, a.sort_order ASC, a.sales DESC
+    </select>
+
+    <!-- 管理后台菜品列表(全部状态) -->
+    <select id="selectAdminPage" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT <include refid="dish_Column_List"/>,
+            (SELECT dc.name FROM hotel_dish_category dc WHERE dc.id = a.category_id AND dc.del_flag = 0) as categoryName
+        FROM hotel_dish a
+        WHERE a.del_flag = 0
+        <if test="categoryId != null and categoryId != ''">
+            AND a.category_id = #{categoryId}
+        </if>
+        <if test="status != null and status != ''">
+            AND a.status = #{status}
+        </if>
+        <if test="name != null and name != ''">
+            AND a.name LIKE CONCAT('%', #{name}, '%')
+        </if>
+        ORDER BY a.sort_order ASC, a.sales DESC
+    </select>
+
+    <!-- 销量排行 -->
+    <select id="selectSalesRanking" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT <include refid="dish_Column_List"/>
+        FROM hotel_dish a
+        WHERE a.del_flag = 0 AND a.status = 'ON_SALE'
+        ORDER BY a.sales DESC
+        LIMIT #{limit}
+    </select>
+
+    <!-- 批量更新销量 -->
+    <update id="batchUpdateSales">
+        <foreach collection="items" item="item" separator=";">
+            UPDATE hotel_dish
+            SET sales = IFNULL(sales, 0) + #{item.quantity},
+                update_time = NOW()
+            WHERE id = #{item.dishId} AND del_flag = 0
+        </foreach>
+    </update>
+
+</mapper>

+ 21 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishSpecMapper.xml

@@ -0,0 +1,21 @@
+<?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.jeeplus.hotelorder.dish.mapper.HotelDishSpecMapper">
+
+    <!-- 根据菜品ID查询规格组 -->
+    <select id="selectByDishId" resultType="com.jeeplus.hotelorder.dish.domain.HotelDishSpec">
+        SELECT id, dish_id as dishId, group_name as groupName, sort_order as sortOrder,
+               create_time as createTime, update_time as updateTime,
+               del_flag as delFlag, tenant_id as tenantId
+        FROM hotel_dish_spec
+        WHERE del_flag = 0 AND dish_id = #{dishId}
+        ORDER BY sort_order ASC
+    </select>
+
+    <!-- 根据菜品ID删除规格组(级联) -->
+    <update id="deleteByDishId">
+        UPDATE hotel_dish_spec SET del_flag = 1, update_time = NOW()
+        WHERE dish_id = #{dishId} AND del_flag = 0
+    </update>
+
+</mapper>

+ 21 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/mapper/xml/HotelDishSpecOptionMapper.xml

@@ -0,0 +1,21 @@
+<?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.jeeplus.hotelorder.dish.mapper.HotelDishSpecOptionMapper">
+
+    <!-- 根据规格组ID查询选项 -->
+    <select id="selectBySpecId" resultType="com.jeeplus.hotelorder.dish.domain.HotelDishSpecOption">
+        SELECT id, spec_id as specId, option_name as optionName, sort_order as sortOrder,
+               create_time as createTime, update_time as updateTime,
+               del_flag as delFlag, tenant_id as tenantId
+        FROM hotel_dish_spec_option
+        WHERE del_flag = 0 AND spec_id = #{specId}
+        ORDER BY sort_order ASC
+    </select>
+
+    <!-- 根据规格组ID删除选项 -->
+    <update id="deleteBySpecId">
+        UPDATE hotel_dish_spec_option SET del_flag = 1, update_time = NOW()
+        WHERE spec_id = #{specId} AND del_flag = 0
+    </update>
+
+</mapper>

+ 122 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/HotelDishCategoryService.java

@@ -0,0 +1,122 @@
+package com.jeeplus.hotelorder.dish.service;
+
+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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.jeeplus.hotelorder.dish.domain.HotelDishCategory;
+import com.jeeplus.hotelorder.dish.mapper.HotelDishCategoryMapper;
+import com.jeeplus.hotelorder.dish.service.dto.HotelDishCategoryDTO;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class HotelDishCategoryService extends ServiceImpl<HotelDishCategoryMapper, HotelDishCategory> {
+
+    @Resource
+    private HotelDishCategoryMapper hotelDishCategoryMapper;
+
+    /**
+     * 分页查询分类(带菜品数量)
+     */
+    public IPage<HotelDishCategoryDTO> findPage(Page<HotelDishCategory> page) {
+        IPage<HotelDishCategory> result = hotelDishCategoryMapper.selectCategoryWithCount(page);
+        IPage<HotelDishCategoryDTO> dtoPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
+        List<HotelDishCategoryDTO> dtoList = result.getRecords().stream().map(entity -> {
+            HotelDishCategoryDTO dto = new HotelDishCategoryDTO();
+            dto.setId(entity.getId());
+            dto.setName(entity.getName());
+            dto.setIcon(entity.getIcon());
+            dto.setSortOrder(entity.getSortOrder());
+            dto.setStatus(entity.getStatus());
+            dto.setDishCount(entity.getDishCount());
+            dto.setCreateTime(entity.getCreateTime());
+            dto.setUpdateTime(entity.getUpdateTime());
+            return dto;
+        }).collect(java.util.stream.Collectors.toList());
+        dtoPage.setRecords(dtoList);
+        return dtoPage;
+    }
+
+    /**
+     * 查询所有启用的分类
+     */
+    public List<HotelDishCategoryDTO> findAll() {
+        QueryWrapper<HotelDishCategory> qw = new QueryWrapper<>();
+        qw.eq("status", "ENABLED").eq("del_flag", 0).orderByAsc("sort_order");
+        List<HotelDishCategory> list = hotelDishCategoryMapper.selectList(qw);
+        return list.stream().map(entity -> {
+            HotelDishCategoryDTO dto = new HotelDishCategoryDTO();
+            dto.setId(entity.getId());
+            dto.setName(entity.getName());
+            dto.setIcon(entity.getIcon());
+            dto.setSortOrder(entity.getSortOrder());
+            dto.setStatus(entity.getStatus());
+            return dto;
+        }).collect(java.util.stream.Collectors.toList());
+    }
+
+    /**
+     * 新增/编辑分类
+     */
+    public String save(HotelDishCategoryDTO dto) {
+        if (StringUtils.isBlank(dto.getId())) {
+            HotelDishCategory entity = new HotelDishCategory();
+            entity.setId(UUID.randomUUID().toString().replace("-", ""));
+            entity.setName(dto.getName());
+            entity.setIcon(dto.getIcon());
+            entity.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
+            entity.setStatus("ENABLED");
+            entity.setDelFlag(0);
+            entity.setCreateTime(new Date());
+            entity.setUpdateTime(new Date());
+            hotelDishCategoryMapper.insert(entity);
+        } else {
+            HotelDishCategory entity = hotelDishCategoryMapper.selectById(dto.getId());
+            if (entity == null) {
+                throw new RuntimeException("分类不存在");
+            }
+            entity.setName(dto.getName());
+            entity.setIcon(dto.getIcon());
+            entity.setSortOrder(dto.getSortOrder());
+            entity.setUpdateTime(new Date());
+            hotelDishCategoryMapper.updateById(entity);
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 切换分类状态(启用/禁用)
+     */
+    public String toggleStatus(String id) {
+        HotelDishCategory entity = hotelDishCategoryMapper.selectById(id);
+        if (entity == null) {
+            throw new RuntimeException("分类不存在");
+        }
+        entity.setStatus("ENABLED".equals(entity.getStatus()) ? "DISABLED" : "ENABLED");
+        entity.setUpdateTime(new Date());
+        hotelDishCategoryMapper.updateById(entity);
+        return "操作成功";
+    }
+
+    /**
+     * 删除分类
+     */
+    public String delete(String id) {
+        HotelDishCategory entity = hotelDishCategoryMapper.selectById(id);
+        if (entity == null) {
+            throw new RuntimeException("分类不存在");
+        }
+        entity.setDelFlag(1);
+        entity.setUpdateTime(new Date());
+        hotelDishCategoryMapper.updateById(entity);
+        return "操作成功";
+    }
+}

+ 410 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/HotelDishService.java

@@ -0,0 +1,410 @@
+package com.jeeplus.hotelorder.dish.service;
+
+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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jeeplus.hotelorder.dish.domain.*;
+import com.jeeplus.hotelorder.dish.mapper.*;
+import com.jeeplus.hotelorder.dish.service.dto.HotelDishDTO;
+import com.jeeplus.hotelorder.order.domain.HotelOrder;
+import com.jeeplus.hotelorder.order.mapper.HotelOrderMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class HotelDishService extends ServiceImpl<HotelDishMapper, HotelDish> {
+
+    private static final Logger log = LoggerFactory.getLogger(HotelDishService.class);
+
+    @Resource
+    private HotelDishMapper hotelDishMapper;
+
+    @Resource
+    private HotelDishSpecMapper hotelDishSpecMapper;
+
+    @Resource
+    private HotelDishSpecOptionMapper hotelDishSpecOptionMapper;
+
+    @Resource
+    private HotelDishAdditionMapper hotelDishAdditionMapper;
+
+    @Resource
+    private HotelDishCategoryMapper hotelDishCategoryMapper;
+
+    @Resource
+    private HotelDishLogMapper hotelDishLogMapper;
+
+    @Resource
+    private HotelOrderMapper hotelOrderMapper;
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 管理后台菜品分页查询(含分类名称)
+     */
+    public IPage<HotelDishDTO> findAdminPage(Page<HotelDish> page, HotelDishDTO queryDTO) {
+        IPage<HotelDish> result = hotelDishMapper.selectAdminPage(page, queryDTO.getCategoryId(), queryDTO.getStatus(), queryDTO.getName());
+        return convertToDtoPage(result);
+    }
+
+    /**
+     * 客户端菜品列表(只查在售且未售罄)
+     */
+    public IPage<HotelDishDTO> findClientPage(Page<HotelDish> page, HotelDishDTO queryDTO) {
+        IPage<HotelDish> result = hotelDishMapper.selectClientList(page, queryDTO.getCategoryId(), queryDTO.getKeyword());
+        return convertToDtoPage(result);
+    }
+
+    /**
+     * Entity分页结果转DTO分页结果
+     */
+    private IPage<HotelDishDTO> convertToDtoPage(IPage<HotelDish> result) {
+        IPage<HotelDishDTO> dtoPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
+        List<HotelDishDTO> dtoList = result.getRecords().stream().map(this::entityToDto).collect(Collectors.toList());
+        dtoPage.setRecords(dtoList);
+        return dtoPage;
+    }
+
+    /**
+     * Entity转DTO
+     */
+    private HotelDishDTO entityToDto(HotelDish entity) {
+        HotelDishDTO dto = new HotelDishDTO();
+        dto.setId(entity.getId());
+        dto.setName(entity.getName());
+        dto.setCategoryId(entity.getCategoryId());
+        dto.setPrice(entity.getPrice());
+        dto.setImage(entity.getImage());
+        dto.setDescription(entity.getDescription());
+        dto.setTags(entity.getTags());
+        dto.setPrepTime(entity.getPrepTime());
+        dto.setSales(entity.getSales());
+        dto.setStatus(entity.getStatus());
+        dto.setSoldOut(entity.getSoldOut());
+        dto.setIsRecommended(entity.getIsRecommended());
+        dto.setSortOrder(entity.getSortOrder());
+        dto.setCategoryName(entity.getCategoryName());
+        dto.setSpecGroups(entity.getSpecGroups());
+        dto.setAdditions(entity.getAdditions());
+        dto.setCreateTime(entity.getCreateTime());
+        dto.setUpdateTime(entity.getUpdateTime());
+        return dto;
+    }
+
+    /**
+     * DTO转Entity
+     */
+    private HotelDish dtoToEntity(HotelDishDTO dto) {
+        HotelDish entity = new HotelDish();
+        entity.setId(dto.getId());
+        entity.setName(dto.getName());
+        entity.setCategoryId(dto.getCategoryId());
+        entity.setPrice(dto.getPrice());
+        entity.setImage(dto.getImage());
+        entity.setDescription(dto.getDescription());
+        entity.setTags(dto.getTags());
+        entity.setPrepTime(dto.getPrepTime());
+        entity.setSales(dto.getSales());
+        entity.setStatus(dto.getStatus());
+        entity.setSoldOut(dto.getSoldOut());
+        entity.setIsRecommended(dto.getIsRecommended());
+        entity.setSortOrder(dto.getSortOrder());
+        return entity;
+    }
+
+    /**
+     * 查询菜品详情
+     */
+    public HotelDishDTO getDetail(String id) {
+        HotelDish dish = hotelDishMapper.selectById(id);
+        if (dish != null) {
+            fillDishDetail(dish);
+        }
+        return dish != null ? entityToDto(dish) : null;
+    }
+
+    /**
+     * 填充菜品的规格组和加料信息
+     */
+    private void fillDishDetail(HotelDish dish) {
+        QueryWrapper<HotelDishSpec> specQw = new QueryWrapper<>();
+        specQw.eq("dish_id", dish.getId()).eq("del_flag", 0).orderByAsc("sort_order");
+        List<HotelDishSpec> specs = hotelDishSpecMapper.selectList(specQw);
+        for (HotelDishSpec spec : specs) {
+            QueryWrapper<HotelDishSpecOption> optQw = new QueryWrapper<>();
+            optQw.eq("spec_id", spec.getId()).eq("del_flag", 0).orderByAsc("sort_order");
+            spec.setOptions(hotelDishSpecOptionMapper.selectList(optQw));
+        }
+        dish.setSpecGroups(specs);
+        QueryWrapper<HotelDishAddition> addQw = new QueryWrapper<>();
+        addQw.eq("dish_id", dish.getId()).eq("del_flag", 0).orderByAsc("sort_order");
+        dish.setAdditions(hotelDishAdditionMapper.selectList(addQw));
+    }
+
+    /**
+     * 新增/编辑菜品(管理后台)
+     */
+    public String saveDish(HotelDishDTO dto) {
+        if (StringUtils.isBlank(dto.getId())) {
+            HotelDish dish = dtoToEntity(dto);
+            dish.setId(UUID.randomUUID().toString().replace("-", ""));
+            dish.setDelFlag(0);
+            if (dish.getStatus() == null) {
+                dish.setStatus("ON_SALE");
+            }
+            if (dish.getSales() == null) {
+                dish.setSales(0);
+            }
+            if (dish.getSoldOut() == null) {
+                dish.setSoldOut(0);
+            }
+            hotelDishMapper.insert(dish);
+            saveLog(dish.getId(), dish.getName(), "CREATE", null, toJson(dish), "新增菜品");
+            log.info("新增菜品: {}", dish.getName());
+        } else {
+            HotelDish oldDish = hotelDishMapper.selectById(dto.getId());
+            String oldJson = oldDish != null ? toJson(oldDish) : null;
+            HotelDish dish = dtoToEntity(dto);
+            hotelDishMapper.updateById(dish);
+            saveLog(dish.getId(), dish.getName() != null ? dish.getName() : (oldDish != null ? oldDish.getName() : ""), "EDIT", oldJson, toJson(dish), "编辑菜品");
+            log.info("编辑菜品: {}", dish.getName());
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 删除菜品(级联删除规格组和加料,需校验未完成订单)
+     */
+    public String deleteDish(String id) {
+        HotelDish dish = hotelDishMapper.selectById(id);
+        if (dish == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        // 校验未完成订单
+        checkUnfinishedOrders(id);
+        // 软删除菜品
+        dish.setDelFlag(1);
+        hotelDishMapper.updateById(dish);
+        // 级联软删除规格组
+        QueryWrapper<HotelDishSpec> specQw = new QueryWrapper<>();
+        specQw.eq("dish_id", id).eq("del_flag", 0);
+        List<HotelDishSpec> specs = hotelDishSpecMapper.selectList(specQw);
+        for (HotelDishSpec spec : specs) {
+            spec.setDelFlag(1);
+            hotelDishSpecMapper.updateById(spec);
+            QueryWrapper<HotelDishSpecOption> optQw = new QueryWrapper<>();
+            optQw.eq("spec_id", spec.getId()).eq("del_flag", 0);
+            List<HotelDishSpecOption> options = hotelDishSpecOptionMapper.selectList(optQw);
+            for (HotelDishSpecOption opt : options) {
+                opt.setDelFlag(1);
+                hotelDishSpecOptionMapper.updateById(opt);
+            }
+        }
+        // 软删除加料
+        QueryWrapper<HotelDishAddition> addQw = new QueryWrapper<>();
+        addQw.eq("dish_id", id).eq("del_flag", 0);
+        List<HotelDishAddition> additions = hotelDishAdditionMapper.selectList(addQw);
+        for (HotelDishAddition add : additions) {
+            add.setDelFlag(1);
+            hotelDishAdditionMapper.updateById(add);
+        }
+        saveLog(id, dish.getName(), "DELETE", toJson(dish), null, "删除菜品");
+        log.info("删除菜品: {}", dish.getName());
+        return "操作成功";
+    }
+
+    /**
+     * 标记售罄
+     */
+    public String markSoldOut(String id) {
+        HotelDish dish = hotelDishMapper.selectById(id);
+        if (dish != null) {
+            dish.setSoldOut(1);
+            hotelDishMapper.updateById(dish);
+            saveLog(id, dish.getName(), "SOLD_OUT", null, null, "标记售罄");
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 恢复上架(取消售罄)
+     */
+    public String restoreDish(String id) {
+        HotelDish dish = hotelDishMapper.selectById(id);
+        if (dish != null) {
+            dish.setSoldOut(0);
+            dish.setStatus("ON_SALE");
+            hotelDishMapper.updateById(dish);
+            saveLog(id, dish.getName(), "RESTORE", null, null, "恢复上架");
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 更新菜品状态(上架/下架)
+     */
+    public String updateStatus(String id, String status) {
+        HotelDish dish = hotelDishMapper.selectById(id);
+        if (dish == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        dish.setStatus(status);
+        hotelDishMapper.updateById(dish);
+        String opType = "ON_SALE".equals(status) ? "ON_SALE" : "OFF_SHELF";
+        saveLog(id, dish.getName(), opType, null, null, "ON_SALE".equals(status) ? "上架" : "下架");
+        log.info("菜品状态变更: {} -> {}", dish.getName(), status);
+        return "操作成功";
+    }
+
+    /**
+     * 批量操作
+     */
+    public String batchOperate(List<String> ids, String operation) {
+        for (String id : ids) {
+            switch (operation) {
+                case "ON_SALE":
+                    updateStatus(id, "ON_SALE");
+                    break;
+                case "OFF_SHELF":
+                    updateStatus(id, "OFF_SHELF");
+                    break;
+                case "SOLD_OUT":
+                    markSoldOut(id);
+                    break;
+                case "RESTORE":
+                    restoreDish(id);
+                    break;
+                case "DELETE":
+                    deleteDish(id);
+                    break;
+                default:
+                    throw new RuntimeException("不支持的操作类型: " + operation);
+            }
+        }
+        return "批量操作成功";
+    }
+
+    /**
+     * 校验菜品是否有未完成订单
+     */
+    private void checkUnfinishedOrders(String dishId) {
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        qw.in("status", Arrays.asList("PREPARING", "READY", "DELIVERING", "ACCEPTED"))
+                .eq("del_flag", 0);
+        List<HotelOrder> orders = hotelOrderMapper.selectList(qw);
+        if (!orders.isEmpty()) {
+            throw new RuntimeException("该菜品存在未完成的订单,无法删除");
+        }
+    }
+
+    /**
+     * 保存操作日志
+     */
+    private void saveLog(String dishId, String dishName, String operationType, String oldValue, String newValue, String remark) {
+        HotelDishLog logEntity = new HotelDishLog();
+        logEntity.setId(UUID.randomUUID().toString().replace("-", ""));
+        logEntity.setDishId(dishId);
+        logEntity.setDishName(dishName);
+        logEntity.setOperationType(operationType);
+        logEntity.setOldValue(oldValue);
+        logEntity.setNewValue(newValue);
+        logEntity.setRemark(remark);
+        logEntity.setDelFlag(0);
+        hotelDishLogMapper.insert(logEntity);
+    }
+
+    /**
+     * 简单转JSON(用于日志记录)
+     */
+    private String toJson(Object obj) {
+        if (obj == null) return null;
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (Exception e) {
+            return obj.toString();
+        }
+    }
+
+    // ============ 规格和加料方法 ============
+
+    public String saveSpec(HotelDishSpec spec) {
+        if (StringUtils.isBlank(spec.getId())) {
+            spec.setId(UUID.randomUUID().toString().replace("-", ""));
+            spec.setDelFlag(0);
+            hotelDishSpecMapper.insert(spec);
+        } else {
+            hotelDishSpecMapper.updateById(spec);
+        }
+        return "操作成功";
+    }
+
+    public String deleteSpec(String id) {
+        HotelDishSpec spec = hotelDishSpecMapper.selectById(id);
+        if (spec == null) {
+            throw new RuntimeException("规格组不存在");
+        }
+        spec.setDelFlag(1);
+        hotelDishSpecMapper.updateById(spec);
+        QueryWrapper<HotelDishSpecOption> optQw = new QueryWrapper<>();
+        optQw.eq("spec_id", id).eq("del_flag", 0);
+        List<HotelDishSpecOption> options = hotelDishSpecOptionMapper.selectList(optQw);
+        for (HotelDishSpecOption opt : options) {
+            opt.setDelFlag(1);
+            hotelDishSpecOptionMapper.updateById(opt);
+        }
+        return "操作成功";
+    }
+
+    public String saveSpecOption(HotelDishSpecOption option) {
+        if (StringUtils.isBlank(option.getId())) {
+            option.setId(UUID.randomUUID().toString().replace("-", ""));
+            option.setDelFlag(0);
+            hotelDishSpecOptionMapper.insert(option);
+        } else {
+            hotelDishSpecOptionMapper.updateById(option);
+        }
+        return "操作成功";
+    }
+
+    public String saveAddition(HotelDishAddition addition) {
+        if (StringUtils.isBlank(addition.getId())) {
+            addition.setId(UUID.randomUUID().toString().replace("-", ""));
+            addition.setDelFlag(0);
+            hotelDishAdditionMapper.insert(addition);
+        } else {
+            hotelDishAdditionMapper.updateById(addition);
+        }
+        return "操作成功";
+    }
+
+    public String deleteAddition(String id) {
+        HotelDishAddition addition = hotelDishAdditionMapper.selectById(id);
+        if (addition == null) {
+            throw new RuntimeException("加料不存在");
+        }
+        addition.setDelFlag(1);
+        hotelDishAdditionMapper.updateById(addition);
+        return "操作成功";
+    }
+
+    /**
+     * 销量排行
+     */
+    public List<HotelDishDTO> salesRanking(int limit) {
+        List<HotelDish> list = hotelDishMapper.selectSalesRanking(limit);
+        return list.stream().map(this::entityToDto).collect(Collectors.toList());
+    }
+}

+ 34 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/dto/HotelDishCategoryDTO.java

@@ -0,0 +1,34 @@
+package com.jeeplus.hotelorder.dish.service.dto;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.jeeplus.core.service.dto.BaseDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 菜品分类管理-DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class HotelDishCategoryDTO extends BaseDTO {
+
+    public static final String BIZ_CODE = "hotel_dish_category";
+
+    /** 分类名称 */
+    private String name;
+
+    /** 图标(emoji) */
+    private String icon;
+
+    /** 排序号 */
+    private Integer sortOrder;
+
+    /** 状态: ENABLED-启用, DISABLED-禁用 */
+    private String status;
+
+    /** 菜品数量(非DB字段,查询用) */
+    @TableField(exist = false)
+    private Integer dishCount;
+
+    private static final long serialVersionUID = 1L;
+}

+ 75 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dish/service/dto/HotelDishDTO.java

@@ -0,0 +1,75 @@
+package com.jeeplus.hotelorder.dish.service.dto;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.jeeplus.core.service.dto.BaseDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 菜品管理-DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class HotelDishDTO extends BaseDTO {
+
+    public static final String BIZ_CODE = "hotel_dish";
+
+    /** 菜品名称 */
+    private String name;
+
+    /** 分类ID */
+    private String categoryId;
+
+    /** 基础价格 */
+    private BigDecimal price;
+
+    /** 菜品图片URL */
+    private String image;
+
+    /** 菜品描述 */
+    private String description;
+
+    /** 标签(逗号分隔) */
+    private String tags;
+
+    /** 预计制作时长(分钟) */
+    private Integer prepTime;
+
+    /** 月销量 */
+    private Integer sales;
+
+    /** 状态:ON_SALE-在售, OFF_SHELF-下架 */
+    private String status;
+
+    /** 是否售罄(0正常 1售罄) */
+    private Integer soldOut;
+
+    /** 是否主厨推荐(0否 1是) */
+    private Integer isRecommended;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+
+    /** 分类名称(非DB字段,查询用) */
+    @TableField(exist = false)
+    private String categoryName;
+
+    /** 规格组列表(非DB字段) */
+    @TableField(exist = false)
+    private List<com.jeeplus.hotelorder.dish.domain.HotelDishSpec> specGroups;
+
+    /** 加料列表(非DB字段) */
+    @TableField(exist = false)
+    private List<com.jeeplus.hotelorder.dish.domain.HotelDishAddition> additions;
+
+    // ============ 查询条件扩展字段 ============
+
+    /** 客户端搜索关键词 */
+    @TableField(exist = false)
+    private String keyword;
+
+    private static final long serialVersionUID = 1L;
+}

+ 15 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/DishBatchOperationDTO.java

@@ -0,0 +1,15 @@
+package com.jeeplus.hotelorder.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DishBatchOperationDTO {
+
+    /** 菜品ID列表 */
+    private List<String> ids;
+
+    /** 操作类型: ON_SALE-上架, OFF_SHELF-下架, SOLD_OUT-售罄, RESTORE-恢复上架, DELETE-删除 */
+    private String operation;
+}

+ 42 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/OrderCreateDTO.java

@@ -0,0 +1,42 @@
+package com.jeeplus.hotelorder.dto;
+
+import com.jeeplus.hotelorder.order.domain.HotelOrderItem;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 创建订单请求DTO
+ */
+@Data
+public class OrderCreateDTO {
+
+    /** 房间号 */
+    private String roomNo;
+
+    /** 住客姓名 */
+    private String contactName;
+
+    /** 联系电话 */
+    private String contactPhone;
+
+    /** 配送费 */
+    private BigDecimal deliveryFee;
+
+    /** 支付方式 */
+    private String payMethod;
+
+    /** 配送时间类型 */
+    private String deliveryTimeType;
+
+    /** 自定义配送时间 */
+    private Date customDeliveryTime;
+
+    /** 订单备注 */
+    private String note;
+
+    /** 订单明细 */
+    private List<HotelOrderItem> items;
+}

+ 28 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/dto/OrderDashboardDTO.java

@@ -0,0 +1,28 @@
+package com.jeeplus.hotelorder.dto;
+
+import lombok.Data;
+
+/**
+ * 订单看板统计DTO
+ */
+@Data
+public class OrderDashboardDTO {
+
+    /** 待接单数 */
+    private Integer pendingCount;
+
+    /** 备餐中数 */
+    private Integer preparingCount;
+
+    /** 待配送数 */
+    private Integer readyCount;
+
+    /** 配送中数 */
+    private Integer deliveringCount;
+
+    /** 今日订单总数 */
+    private Integer todayTotal;
+
+    /** 今日营业额 */
+    private String todayRevenue;
+}

+ 56 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/controller/HotelConfigController.java

@@ -0,0 +1,56 @@
+package com.jeeplus.hotelorder.hotelconfig.controller;
+
+import com.jeeplus.common.utils.ResponseUtil;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelBusinessHours;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelConfig;
+import com.jeeplus.hotelorder.hotelconfig.service.HotelConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping(value = "/config")
+@Api(tags = "酒店配置管理")
+public class HotelConfigController {
+
+    @Resource
+    private HotelConfigService hotelConfigService;
+
+    @GetMapping("get")
+    @ApiOperation(value = "获取酒店配置")
+    public ResponseEntity<?> getConfig() {
+        HotelConfig config = hotelConfigService.getConfig();
+        return ResponseUtil.newInstance()
+                .add("config", config)
+                .ok("操作成功");
+    }
+
+    @PostMapping("save")
+    @ApiOperation(value = "保存/更新酒店配置")
+    public ResponseEntity<?> saveConfig(@RequestBody HotelConfig config) {
+        return ResponseUtil.newInstance().ok(hotelConfigService.saveConfig(config));
+    }
+
+    @GetMapping("businessHours/list")
+    @ApiOperation(value = "获取营业时段列表")
+    public ResponseEntity<?> getBusinessHoursList() {
+        return ResponseUtil.newInstance()
+                .add("list", hotelConfigService.getBusinessHoursList())
+                .ok("操作成功");
+    }
+
+    @PostMapping("businessHours/save")
+    @ApiOperation(value = "保存营业时段")
+    public ResponseEntity<?> saveBusinessHours(@RequestBody HotelBusinessHours hours) {
+        return ResponseUtil.newInstance().ok(hotelConfigService.saveBusinessHours(hours));
+    }
+
+    @PostMapping("businessHours/toggle")
+    @ApiOperation(value = "紧急暂停/恢复营业")
+    public ResponseEntity<?> toggleBusinessHours(@RequestParam String id, @RequestParam Integer enabled) {
+        return ResponseUtil.newInstance().ok(hotelConfigService.toggleBusinessHours(id, enabled));
+    }
+}

+ 29 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/domain/HotelBusinessHours.java

@@ -0,0 +1,29 @@
+package com.jeeplus.hotelorder.hotelconfig.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_business_hours")
+public class HotelBusinessHours extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 时段名称(如: 早餐、午餐、晚餐) */
+    private String name;
+
+    /** 开始时间 */
+    private String startTime;
+
+    /** 结束时间 */
+    private String endTime;
+
+    /** 是否启用 */
+    private Integer enabled;
+
+    /** 排序权重 */
+    private Integer sortOrder;
+}

+ 28 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/domain/HotelConfig.java

@@ -0,0 +1,28 @@
+package com.jeeplus.hotelorder.hotelconfig.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_config")
+public class HotelConfig extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 酒店名称 */
+    private String hotelName;
+
+    /** 主题配色 */
+    private String theme;
+
+    /** 配送费 */
+    private BigDecimal deliveryFee;
+
+    /** 预计配送时长(分钟) */
+    private Integer estimatedDeliveryMinutes;
+}

+ 9 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/HotelBusinessHoursMapper.java

@@ -0,0 +1,9 @@
+package com.jeeplus.hotelorder.hotelconfig.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelBusinessHours;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HotelBusinessHoursMapper extends BaseMapper<HotelBusinessHours> {
+}

+ 14 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/HotelConfigMapper.java

@@ -0,0 +1,14 @@
+package com.jeeplus.hotelorder.hotelconfig.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HotelConfigMapper extends BaseMapper<HotelConfig> {
+
+    /**
+     * 查询酒店配置(取第一条有效记录)
+     */
+    HotelConfig selectConfig();
+}

+ 16 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/mapper/xml/HotelConfigMapper.xml

@@ -0,0 +1,16 @@
+<?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.jeeplus.hotelorder.hotelconfig.mapper.HotelConfigMapper">
+
+    <!-- 查询酒店配置(取第一条有效记录) -->
+    <select id="selectConfig" resultType="com.jeeplus.hotelorder.hotelconfig.domain.HotelConfig">
+        SELECT id, hotel_name as hotelName, theme, delivery_fee as deliveryFee,
+               estimated_delivery_minutes as estimatedDeliveryMinutes,
+               create_time as createTime, update_time as updateTime,
+               del_flag as delFlag, tenant_id as tenantId
+        FROM hotel_config
+        WHERE del_flag = 0
+        LIMIT 1
+    </select>
+
+</mapper>

+ 94 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/hotelconfig/service/HotelConfigService.java

@@ -0,0 +1,94 @@
+package com.jeeplus.hotelorder.hotelconfig.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelBusinessHours;
+import com.jeeplus.hotelorder.hotelconfig.domain.HotelConfig;
+import com.jeeplus.hotelorder.hotelconfig.mapper.HotelBusinessHoursMapper;
+import com.jeeplus.hotelorder.hotelconfig.mapper.HotelConfigMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class HotelConfigService {
+
+    @Resource
+    private HotelConfigMapper hotelConfigMapper;
+
+    @Resource
+    private HotelBusinessHoursMapper hotelBusinessHoursMapper;
+
+    /**
+     * 获取酒店配置
+     */
+    public HotelConfig getConfig() {
+        QueryWrapper<HotelConfig> qw = new QueryWrapper<>();
+        qw.last("LIMIT 1");
+        return hotelConfigMapper.selectOne(qw);
+    }
+
+    /**
+     * 保存/更新酒店配置
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String saveConfig(HotelConfig config) {
+        HotelConfig existing = getConfig();
+        if (existing == null) {
+            config.setId(UUID.randomUUID().toString().replace("-", ""));
+            config.setCreateTime(new Date());
+            config.setUpdateTime(new Date());
+            config.setDelFlag(0);
+            hotelConfigMapper.insert(config);
+        } else {
+            config.setId(existing.getId());
+            config.setUpdateTime(new Date());
+            hotelConfigMapper.updateById(config);
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 获取营业时段列表
+     */
+    public List<HotelBusinessHours> getBusinessHoursList() {
+        QueryWrapper<HotelBusinessHours> qw = new QueryWrapper<>();
+        qw.orderByAsc("sort_order");
+        return hotelBusinessHoursMapper.selectList(qw);
+    }
+
+    /**
+     * 保存营业时段
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String saveBusinessHours(HotelBusinessHours hours) {
+        if (hours.getId() == null || hours.getId().isEmpty()) {
+            hours.setId(UUID.randomUUID().toString().replace("-", ""));
+            hours.setCreateTime(new Date());
+            hours.setUpdateTime(new Date());
+            hours.setDelFlag(0);
+            hotelBusinessHoursMapper.insert(hours);
+        } else {
+            hours.setUpdateTime(new Date());
+            hotelBusinessHoursMapper.updateById(hours);
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 紧急暂停/恢复营业
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String toggleBusinessHours(String id, Integer enabled) {
+        HotelBusinessHours hours = hotelBusinessHoursMapper.selectById(id);
+        if (hours != null) {
+            hours.setEnabled(enabled);
+            hours.setUpdateTime(new Date());
+            hotelBusinessHoursMapper.updateById(hours);
+        }
+        return "操作成功";
+    }
+}

+ 136 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/controller/HotelOrderController.java

@@ -0,0 +1,136 @@
+package com.jeeplus.hotelorder.order.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.common.utils.ResponseUtil;
+import com.jeeplus.hotelorder.dto.OrderCreateDTO;
+import com.jeeplus.hotelorder.dto.OrderDashboardDTO;
+import com.jeeplus.hotelorder.order.domain.HotelOrder;
+import com.jeeplus.hotelorder.order.service.HotelOrderService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping(value = "/order")
+@Api(tags = "订单管理")
+public class HotelOrderController {
+
+    @Resource
+    private HotelOrderService hotelOrderService;
+
+    @GetMapping("dashboard")
+    @ApiOperation(value = "订单看板统计")
+    public ResponseEntity<?> dashboard() {
+        OrderDashboardDTO dashboard = hotelOrderService.getDashboard();
+        return ResponseUtil.newInstance()
+                .add("pendingCount", dashboard.getPendingCount())
+                .add("preparingCount", dashboard.getPreparingCount())
+                .add("readyCount", dashboard.getReadyCount())
+                .add("deliveringCount", dashboard.getDeliveringCount())
+                .add("todayTotal", dashboard.getTodayTotal())
+                .add("todayRevenue", dashboard.getTodayRevenue())
+                .ok("操作成功");
+    }
+
+    @GetMapping("page")
+    @ApiOperation(value = "分页查询订单列表")
+    public ResponseEntity<?> findPage(
+            @RequestParam(defaultValue = "1") int current,
+            @RequestParam(defaultValue = "10") int size,
+            HotelOrder query) {
+        Page<HotelOrder> page = new Page<>(current, size);
+        IPage<HotelOrder> result = hotelOrderService.findPage(page, query);
+        return ResponseUtil.newInstance()
+                .add("records", result.getRecords())
+                .add("total", result.getTotal())
+                .add("current", result.getCurrent())
+                .add("pages", result.getPages())
+                .ok("操作成功");
+    }
+
+    @GetMapping("detail/{id}")
+    @ApiOperation(value = "查询订单详情")
+    public ResponseEntity<?> getDetail(@PathVariable String id) {
+        HotelOrder order = hotelOrderService.getDetail(id);
+        return ResponseUtil.newInstance()
+                .add("order", order)
+                .ok("操作成功");
+    }
+
+    @GetMapping("listByRoom/{roomNo}")
+    @ApiOperation(value = "按房间号查询订单列表")
+    public ResponseEntity<?> findByRoomNo(@PathVariable String roomNo) {
+        return ResponseUtil.newInstance()
+                .add("list", hotelOrderService.findByRoomNo(roomNo))
+                .ok("操作成功");
+    }
+
+    @PostMapping("create")
+    @ApiOperation(value = "创建订单(客户端下单)")
+    public ResponseEntity<?> createOrder(@RequestBody OrderCreateDTO dto) {
+        HotelOrder order = hotelOrderService.createOrder(dto);
+        return ResponseUtil.newInstance()
+                .add("order", order)
+                .ok("下单成功");
+    }
+
+    @PostMapping("accept/{id}")
+    @ApiOperation(value = "餐厅接单")
+    public ResponseEntity<?> acceptOrder(@PathVariable String id) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.acceptOrder(id));
+    }
+
+    @PostMapping("reject/{id}")
+    @ApiOperation(value = "餐厅拒单")
+    public ResponseEntity<?> rejectOrder(@PathVariable String id, @RequestParam(required = false) String reason) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.rejectOrder(id, reason));
+    }
+
+    @PostMapping("prepare/{id}")
+    @ApiOperation(value = "开始备餐(厨房)")
+    public ResponseEntity<?> prepareOrder(@PathVariable String id) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.prepareOrder(id));
+    }
+
+    @PostMapping("ready/{id}")
+    @ApiOperation(value = "出餐完成(厨房)")
+    public ResponseEntity<?> readyOrder(@PathVariable String id) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.readyOrder(id));
+    }
+
+    @PostMapping("deliver/{id}")
+    @ApiOperation(value = "开始配送(餐厅前台)")
+    public ResponseEntity<?> deliverOrder(@PathVariable String id) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.deliverOrder(id));
+    }
+
+    @PostMapping("complete/{id}")
+    @ApiOperation(value = "完成配送")
+    public ResponseEntity<?> completeOrder(@PathVariable String id) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.completeOrder(id));
+    }
+
+    @PostMapping("customerRefund/{id}")
+    @ApiOperation(value = "客户自助退单")
+    public ResponseEntity<?> customerRefund(@PathVariable String id, @RequestParam(required = false) String reason) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.customerRefund(id, reason));
+    }
+
+    @PostMapping("hotelRefund/{id}")
+    @ApiOperation(value = "酒店前台退单")
+    public ResponseEntity<?> hotelRefund(@PathVariable String id, @RequestParam(required = false) String reason) {
+        return ResponseUtil.newInstance().ok(hotelOrderService.hotelRefund(id, reason));
+    }
+
+    @GetMapping("todayCount")
+    @ApiOperation(value = "统计今日订单数")
+    public ResponseEntity<?> countTodayOrders() {
+        return ResponseUtil.newInstance()
+                .add("count", hotelOrderService.countTodayOrders())
+                .ok("操作成功");
+    }
+}

+ 96 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/domain/HotelOrder.java

@@ -0,0 +1,96 @@
+package com.jeeplus.hotelorder.order.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jeeplus.core.domain.BaseEntity;
+import com.jeeplus.core.query.Query;
+import com.jeeplus.core.query.QueryType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_order")
+public class HotelOrder extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 订单号 */
+    @Query(tableColumn = "a.order_no", type = QueryType.LIKE)
+    private String orderNo;
+
+    /** 房间号 */
+    @Query(tableColumn = "a.room_no", type = QueryType.EQ)
+    private String roomNo;
+
+    /** 房间类型 */
+    private String roomType;
+
+    /** 联系人姓名 */
+    @Query(tableColumn = "a.contact_name", type = QueryType.LIKE)
+    private String contactName;
+
+    /** 联系电话 */
+    private String contactPhone;
+
+    /** 订单状态 */
+    @Query(tableColumn = "a.status", type = QueryType.EQ)
+    private String status;
+
+    /** 订单总金额 */
+    private BigDecimal totalAmount;
+
+    /** 配送费 */
+    private BigDecimal deliveryFee;
+
+    /** 支付方式 */
+    private String payMethod;
+
+    /** 配送时间类型(ASAP-立即送达, CUSTOM-自定义) */
+    private String deliveryTimeType;
+
+    /** 自定义配送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
+    private Date customDeliveryTime;
+
+    /** 订单备注 */
+    private String note;
+
+    /** 接单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date acceptTime;
+
+    /** 出餐时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date readyTime;
+
+    /** 配送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date deliverTime;
+
+    /** 完成时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date completeTime;
+
+    /** 拒单原因 */
+    private String rejectReason;
+
+    /** 退单原因 */
+    private String refundReason;
+
+    /** 退单发起方(CUSTOMER/HOTEL_DESK) */
+    private String refundInitiator;
+
+    /** 退单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date refundTime;
+
+    /** 订单明细(非DB字段) */
+    @TableField(exist = false)
+    private List<HotelOrderItem> items;
+}

+ 43 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/domain/HotelOrderItem.java

@@ -0,0 +1,43 @@
+package com.jeeplus.hotelorder.order.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_order_item")
+public class HotelOrderItem extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 订单ID */
+    private String orderId;
+
+    /** 菜品ID */
+    private String dishId;
+
+    /** 菜品名称(冗余) */
+    private String dishName;
+
+    /** 规格描述(冗余) */
+    private String specDesc;
+
+    /** 加料描述(冗余) */
+    private String additionsDesc;
+
+    /** 数量 */
+    private Integer quantity;
+
+    /** 单价 */
+    private BigDecimal unitPrice;
+
+    /** 小计 */
+    private BigDecimal subtotal;
+
+    /** 备注 */
+    private String note;
+}

+ 22 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/HotelOrderItemMapper.java

@@ -0,0 +1,22 @@
+package com.jeeplus.hotelorder.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.order.domain.HotelOrderItem;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface HotelOrderItemMapper extends BaseMapper<HotelOrderItem> {
+
+    /**
+     * 根据订单ID查询明细列表
+     */
+    List<HotelOrderItem> selectByOrderId(@Param("orderId") String orderId);
+
+    /**
+     * 批量插入订单明细
+     */
+    int batchInsert(@Param("list") List<HotelOrderItem> list);
+}

+ 38 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/HotelOrderMapper.java

@@ -0,0 +1,38 @@
+package com.jeeplus.hotelorder.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.order.domain.HotelOrder;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+@Mapper
+public interface HotelOrderMapper extends BaseMapper<HotelOrder> {
+
+    /**
+     * 订单看板统计
+     */
+    Map<String, Object> selectDashboard();
+
+    /**
+     * 统计今日有效订单数
+     */
+    int countTodayOrders();
+
+    /**
+     * 统计今日营业额
+     */
+    BigDecimal sumTodayRevenue(@Param("today") String today);
+
+    /**
+     * 查询指定房间今日订单数
+     */
+    int countTodayByRoomNo(@Param("roomNo") String roomNo);
+
+    /**
+     * 查询当天最大订单号
+     */
+    String selectMaxOrderNo(@Param("prefix") String prefix);
+}

+ 36 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/xml/HotelOrderItemMapper.xml

@@ -0,0 +1,36 @@
+<?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.jeeplus.hotelorder.order.mapper.HotelOrderItemMapper">
+
+    <sql id="item_Column_List">
+        a.id, a.order_id as orderId, a.dish_id as dishId, a.dish_name as dishName,
+        a.spec_desc as specDesc, a.additions_desc as additionsDesc,
+        a.quantity, a.unit_price as unitPrice, a.subtotal, a.note,
+        a.create_time as createTime, a.create_by_id as createById,
+        a.update_time as updateTime, a.update_by_id as updateById,
+        a.del_flag as delFlag, a.tenant_id as tenantId
+    </sql>
+
+    <!-- 根据订单ID查询明细 -->
+    <select id="selectByOrderId" resultType="com.jeeplus.hotelorder.order.domain.HotelOrderItem">
+        SELECT <include refid="item_Column_List"/>
+        FROM hotel_order_item a
+        WHERE a.del_flag = 0 AND a.order_id = #{orderId}
+        ORDER BY a.id ASC
+    </select>
+
+    <!-- 批量插入订单明细 -->
+    <insert id="batchInsert">
+        INSERT INTO hotel_order_item
+        (id, order_id, dish_id, dish_name, spec_desc, additions_desc, quantity, unit_price, subtotal, note,
+         create_time, create_by_id, update_time, update_by_id, del_flag, tenant_id)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.id}, #{item.orderId}, #{item.dishId}, #{item.dishName},
+             #{item.specDesc}, #{item.additionsDesc}, #{item.quantity}, #{item.unitPrice},
+             #{item.subtotal}, #{item.note}, #{item.createTime}, #{item.createById},
+             #{item.updateTime}, #{item.updateById}, #{item.delFlag}, #{item.tenantId})
+        </foreach>
+    </insert>
+
+</mapper>

+ 98 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/mapper/xml/HotelOrderMapper.xml

@@ -0,0 +1,98 @@
+<?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.jeeplus.hotelorder.order.mapper.HotelOrderMapper">
+
+    <sql id="order_Column_List">
+        a.id, a.order_no as orderNo, a.room_no as roomNo, a.room_type as roomType,
+        a.contact_name as contactName, a.contact_phone as contactPhone,
+        a.status, a.total_amount as totalAmount, a.delivery_fee as deliveryFee,
+        a.pay_method as payMethod, a.delivery_time_type as deliveryTimeType,
+        a.custom_delivery_time as customDeliveryTime, a.note,
+        a.accept_time as acceptTime, a.ready_time as readyTime,
+        a.deliver_time as deliverTime, a.complete_time as completeTime,
+        a.reject_reason as rejectReason, a.refund_reason as refundReason,
+        a.refund_initiator as refundInitiator, a.refund_time as refundTime,
+        a.create_time as createTime, a.create_by_id as createById,
+        a.update_time as updateTime, a.update_by_id as updateById,
+        a.del_flag as delFlag, a.tenant_id as tenantId
+    </sql>
+
+    <!-- 订单列表(支持多条件筛选) -->
+    <select id="selectOrderPage" resultType="com.jeeplus.hotelorder.order.domain.HotelOrder">
+        SELECT <include refid="order_Column_List"/>
+        FROM hotel_order a
+        WHERE a.del_flag = 0
+        <if test="ew != null and ew.entity != null">
+            <if test="ew.entity.roomNo != null and ew.entity.roomNo != ''">
+                AND a.room_no = #{ew.entity.roomNo}
+            </if>
+            <if test="ew.entity.status != null and ew.entity.status != ''">
+                AND a.status = #{ew.entity.status}
+            </if>
+            <if test="ew.entity.contactName != null and ew.entity.contactName != ''">
+                AND a.contact_name LIKE CONCAT('%', #{ew.entity.contactName}, '%')
+            </if>
+            <if test="ew.entity.contactPhone != null and ew.entity.contactPhone != ''">
+                AND a.contact_phone = #{ew.entity.contactPhone}
+            </if>
+            <if test="ew.entity.payMethod != null and ew.entity.payMethod != ''">
+                AND a.pay_method = #{ew.entity.payMethod}
+            </if>
+        </if>
+        <if test="startDate != null and startDate != ''">
+            AND a.create_time &gt;= #{startDate}
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND a.create_time &lt;= #{endDate}
+        </if>
+        ORDER BY a.create_time DESC
+    </select>
+
+    <!-- 订单看板统计 -->
+    <select id="selectDashboard" resultType="java.util.HashMap">
+        SELECT
+            SUM(CASE WHEN status = 'PLACED' THEN 1 ELSE 0 END) as pendingCount,
+            SUM(CASE WHEN status = 'PREPARING' THEN 1 ELSE 0 END) as preparingCount,
+            SUM(CASE WHEN status IN ('READY','ACCEPTED') THEN 1 ELSE 0 END) as readyCount,
+            SUM(CASE WHEN status = 'DELIVERING' THEN 1 ELSE 0 END) as deliveringCount,
+            SUM(CASE WHEN DATE(create_time) = CURDATE() AND status NOT IN ('REJECTED','REFUNDED') THEN 1 ELSE 0 END) as todayTotal,
+            IFNULL(SUM(CASE WHEN DATE(create_time) = CURDATE() AND status NOT IN ('REJECTED','REFUNDED') THEN total_amount ELSE 0 END), 0) as todayRevenue
+        FROM hotel_order
+        WHERE del_flag = 0
+    </select>
+
+    <!-- 统计今日有效订单数 -->
+    <select id="countTodayOrders" resultType="int">
+        SELECT COUNT(*)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND DATE(create_time) = CURDATE()
+          AND status NOT IN ('REJECTED', 'REFUNDED')
+    </select>
+
+    <!-- 统计今日营业额 -->
+    <select id="sumTodayRevenue" resultType="java.math.BigDecimal">
+        SELECT IFNULL(SUM(total_amount), 0)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND DATE(create_time) = CURDATE()
+          AND status NOT IN ('REJECTED', 'REFUNDED')
+    </select>
+
+    <!-- 查询指定房间今日订单数 -->
+    <select id="countTodayByRoomNo" resultType="int">
+        SELECT COUNT(*)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND room_no = #{roomNo}
+          AND DATE(create_time) = CURDATE()
+    </select>
+
+    <!-- 生成订单号: 查当天最大序号 -->
+    <select id="selectMaxOrderNo" resultType="java.lang.String">
+        SELECT MAX(order_no)
+        FROM hotel_order
+        WHERE order_no LIKE CONCAT(#{prefix}, '%')
+    </select>
+
+</mapper>

+ 400 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/order/service/HotelOrderService.java

@@ -0,0 +1,400 @@
+package com.jeeplus.hotelorder.order.service;
+
+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.jeeplus.hotelorder.dish.domain.HotelDish;
+import com.jeeplus.hotelorder.dish.mapper.HotelDishMapper;
+import com.jeeplus.hotelorder.dto.OrderCreateDTO;
+import com.jeeplus.hotelorder.dto.OrderDashboardDTO;
+import com.jeeplus.hotelorder.order.domain.HotelOrder;
+import com.jeeplus.hotelorder.order.domain.HotelOrderItem;
+import com.jeeplus.hotelorder.order.mapper.HotelOrderItemMapper;
+import com.jeeplus.hotelorder.order.mapper.HotelOrderMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class HotelOrderService {
+
+    private static final Logger log = LoggerFactory.getLogger(HotelOrderService.class);
+
+    @Resource
+    private HotelOrderMapper hotelOrderMapper;
+
+    @Resource
+    private HotelOrderItemMapper hotelOrderItemMapper;
+
+    @Resource
+    private HotelDishMapper hotelDishMapper;
+
+    /**
+     * 订单看板统计
+     */
+    public OrderDashboardDTO getDashboard() {
+        OrderDashboardDTO dto = new OrderDashboardDTO();
+        String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+
+        // 各状态数量(今日)
+        dto.setPendingCount(countByStatusAndDate("PLACED", today));
+        dto.setPreparingCount(countByStatusAndDate("PREPARING", today));
+        dto.setReadyCount(countByStatusAndDate("READY", today));
+        dto.setDeliveringCount(countByStatusAndDate("DELIVERING", today));
+
+        // 今日订单总数(排除已拒单和已退单)
+        QueryWrapper<HotelOrder> totalQw = new QueryWrapper<>();
+        totalQw.likeRight("create_time", today)
+               .ne("status", "REJECTED")
+               .ne("status", "REFUNDED");
+        dto.setTodayTotal(Math.toIntExact(hotelOrderMapper.selectCount(totalQw)));
+
+        // 今日营业额
+        BigDecimal revenue = hotelOrderMapper.sumTodayRevenue(today);
+        dto.setTodayRevenue(revenue != null ? revenue.toPlainString() : "0.00");
+
+        return dto;
+    }
+
+    /**
+     * 按状态和日期统计订单数
+     */
+    private int countByStatusAndDate(String status, String date) {
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        qw.likeRight("create_time", date).eq("status", status);
+        return Math.toIntExact(hotelOrderMapper.selectCount(qw));
+    }
+
+    /**
+     * 分页查询订单列表
+     */
+    public IPage<HotelOrder> findPage(Page<HotelOrder> page, HotelOrder query) {
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        if (query != null) {
+            if (query.getRoomNo() != null && !query.getRoomNo().isEmpty()) {
+                qw.eq("room_no", query.getRoomNo());
+            }
+            if (query.getStatus() != null && !query.getStatus().isEmpty()) {
+                qw.eq("status", query.getStatus());
+            }
+            if (query.getContactName() != null && !query.getContactName().isEmpty()) {
+                qw.like("contact_name", query.getContactName());
+            }
+        }
+        qw.orderByDesc("create_time");
+        IPage<HotelOrder> result = hotelOrderMapper.selectPage(page, qw);
+        // 填充订单明细
+        for (HotelOrder order : result.getRecords()) {
+            fillOrderItems(order);
+        }
+        return result;
+    }
+
+    /**
+     * 查询订单详情
+     */
+    public HotelOrder getDetail(String id) {
+        HotelOrder order = hotelOrderMapper.selectById(id);
+        if (order != null) {
+            fillOrderItems(order);
+        }
+        return order;
+    }
+
+    /**
+     * 填充订单明细
+     */
+    private void fillOrderItems(HotelOrder order) {
+        QueryWrapper<HotelOrderItem> qw = new QueryWrapper<>();
+        qw.eq("order_id", order.getId());
+        order.setItems(hotelOrderItemMapper.selectList(qw));
+    }
+
+    /**
+     * 创建订单(客户端下单, 使用DTO接收参数)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public HotelOrder createOrder(OrderCreateDTO dto) {
+        // 校验
+        if (dto.getRoomNo() == null || dto.getRoomNo().isEmpty()) {
+            throw new RuntimeException("房间号不能为空");
+        }
+        if (dto.getItems() == null || dto.getItems().isEmpty()) {
+            throw new RuntimeException("订单明细不能为空");
+        }
+
+        // 构建订单实体
+        HotelOrder order = new HotelOrder();
+        order.setId(UUID.randomUUID().toString().replace("-", ""));
+        order.setOrderNo(generateOrderNo());
+        order.setRoomNo(dto.getRoomNo());
+        order.setContactName(dto.getContactName());
+        order.setContactPhone(dto.getContactPhone());
+        order.setDeliveryFee(dto.getDeliveryFee() != null ? dto.getDeliveryFee() : BigDecimal.ZERO);
+        order.setPayMethod(dto.getPayMethod());
+        order.setDeliveryTimeType(dto.getDeliveryTimeType());
+        order.setCustomDeliveryTime(dto.getCustomDeliveryTime());
+        order.setNote(dto.getNote());
+        order.setStatus("PLACED");
+        order.setCreateTime(new Date());
+        order.setUpdateTime(new Date());
+        order.setDelFlag(0);
+
+        // 计算总金额
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        for (HotelOrderItem item : dto.getItems()) {
+            BigDecimal subtotal = item.getUnitPrice().multiply(new BigDecimal(item.getQuantity()));
+            item.setSubtotal(subtotal);
+            totalAmount = totalAmount.add(subtotal);
+        }
+        totalAmount = totalAmount.add(order.getDeliveryFee());
+        order.setTotalAmount(totalAmount);
+
+        hotelOrderMapper.insert(order);
+        log.info("创建订单: {}, 房间: {}, 金额: {}", order.getOrderNo(), order.getRoomNo(), totalAmount);
+
+        // 保存订单明细
+        for (HotelOrderItem item : dto.getItems()) {
+            item.setId(UUID.randomUUID().toString().replace("-", ""));
+            item.setOrderId(order.getId());
+            item.setCreateTime(new Date());
+            item.setUpdateTime(new Date());
+            item.setDelFlag(0);
+            hotelOrderItemMapper.insert(item);
+        }
+
+        return order;
+    }
+
+    /**
+     * 生成订单号: RM + YYYYMMDD + 4位序号
+     */
+    private String generateOrderNo() {
+        String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
+        String prefix = "RM" + dateStr;
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        qw.likeRight("order_no", prefix).orderByDesc("order_no").last("LIMIT 1");
+        HotelOrder lastOrder = hotelOrderMapper.selectOne(qw);
+        int seq = 1;
+        if (lastOrder != null && lastOrder.getOrderNo() != null) {
+            String lastSeq = lastOrder.getOrderNo().substring(prefix.length());
+            try {
+                seq = Integer.parseInt(lastSeq) + 1;
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return prefix + String.format("%04d", seq);
+    }
+
+    /**
+     * 更新菜品销量
+     */
+    private void updateDishSales(String dishId, int quantity) {
+        HotelDish dish = hotelDishMapper.selectById(dishId);
+        if (dish != null) {
+            dish.setSales((dish.getSales() != null ? dish.getSales() : 0) + quantity);
+            hotelDishMapper.updateById(dish);
+        }
+    }
+
+    /**
+     * 餐厅接单
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String acceptOrder(String orderId) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"PLACED".equals(order.getStatus())) {
+            throw new RuntimeException("只有待接单的订单才能接单");
+        }
+        order.setStatus("ACCEPTED");
+        order.setAcceptTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        log.info("订单接单: {}", order.getOrderNo());
+        return "操作成功";
+    }
+
+    /**
+     * 餐厅拒单(自动退款)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String rejectOrder(String orderId, String reason) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"PLACED".equals(order.getStatus())) {
+            throw new RuntimeException("只有待接单的订单才能拒单");
+        }
+        order.setStatus("REJECTED");
+        order.setRejectReason(reason);
+        order.setRefundTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        log.info("订单拒单: {}", order.getOrderNo());
+        return "操作成功";
+    }
+
+    /**
+     * 开始备餐(厨房)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String prepareOrder(String orderId) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"ACCEPTED".equals(order.getStatus())) {
+            throw new RuntimeException("只有已接单的订单才能开始备餐");
+        }
+        order.setStatus("PREPARING");
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        return "操作成功";
+    }
+
+    /**
+     * 出餐完成(厨房)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String readyOrder(String orderId) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"PREPARING".equals(order.getStatus())) {
+            throw new RuntimeException("只有备餐中的订单才能出餐");
+        }
+        order.setStatus("READY");
+        order.setReadyTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        return "操作成功";
+    }
+
+    /**
+     * 开始配送(餐厅前台)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String deliverOrder(String orderId) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"READY".equals(order.getStatus())) {
+            throw new RuntimeException("只有待配送的订单才能开始配送");
+        }
+        order.setStatus("DELIVERING");
+        order.setDeliverTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        return "操作成功";
+    }
+
+    /**
+     * 完成配送
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String completeOrder(String orderId) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"DELIVERING".equals(order.getStatus())) {
+            throw new RuntimeException("只有配送中的订单才能完成");
+        }
+        order.setStatus("COMPLETED");
+        order.setCompleteTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        // 订单完成时累加菜品销量
+        incrementDishSales(orderId);
+        log.info("订单完成: {}", order.getOrderNo());
+        return "操作成功";
+    }
+
+    /**
+     * 订单完成时累加菜品销量
+     */
+    private void incrementDishSales(String orderId) {
+        QueryWrapper<HotelOrderItem> qw = new QueryWrapper<>();
+        qw.eq("order_id", orderId).eq("del_flag", 0);
+        List<HotelOrderItem> items = hotelOrderItemMapper.selectList(qw);
+        for (HotelOrderItem item : items) {
+            if (item.getDishId() != null) {
+                HotelDish dish = hotelDishMapper.selectById(item.getDishId());
+                if (dish != null) {
+                    dish.setSales((dish.getSales() != null ? dish.getSales() : 0) + item.getQuantity());
+                    hotelDishMapper.updateById(dish);
+                }
+            }
+        }
+    }
+
+    /**
+     * 客户自助退单(仅未接单时可退)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String customerRefund(String orderId, String reason) {
+        HotelOrder order = checkOrderExists(orderId);
+        if (!"PLACED".equals(order.getStatus())) {
+            return "订单已接单,请联系前台退单";
+        }
+        order.setStatus("REFUNDED");
+        order.setRefundReason(reason);
+        order.setRefundInitiator("CUSTOMER");
+        order.setRefundTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        log.info("客户退单: {}", order.getOrderNo());
+        return "退单成功";
+    }
+
+    /**
+     * 酒店前台退单
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String hotelRefund(String orderId, String reason) {
+        HotelOrder order = checkOrderExists(orderId);
+        if ("COMPLETED".equals(order.getStatus()) || "REFUNDED".equals(order.getStatus())) {
+            throw new RuntimeException("已完成或已退单的订单不能退单");
+        }
+        order.setStatus("REFUNDED");
+        order.setRefundReason(reason);
+        order.setRefundInitiator("HOTEL_DESK");
+        order.setRefundTime(new Date());
+        order.setUpdateTime(new Date());
+        hotelOrderMapper.updateById(order);
+        log.info("前台退单: {}", order.getOrderNo());
+        return "操作成功";
+    }
+
+    /**
+     * 按房间号查询订单列表
+     */
+    public List<HotelOrder> findByRoomNo(String roomNo) {
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        qw.eq("room_no", roomNo).orderByDesc("create_time");
+        List<HotelOrder> list = hotelOrderMapper.selectList(qw);
+        for (HotelOrder order : list) {
+            fillOrderItems(order);
+        }
+        return list;
+    }
+
+    /**
+     * 统计今日订单数
+     */
+    public int countTodayOrders() {
+        String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+        QueryWrapper<HotelOrder> qw = new QueryWrapper<>();
+        qw.likeRight("create_time", today)
+          .ne("status", "REJECTED")
+          .ne("status", "REFUNDED");
+        return Math.toIntExact(hotelOrderMapper.selectCount(qw));
+    }
+
+    /**
+     * 校验订单是否存在
+     */
+    private HotelOrder checkOrderExists(String orderId) {
+        HotelOrder order = hotelOrderMapper.selectById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+        return order;
+    }
+}

+ 149 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/controller/HotelRoomController.java

@@ -0,0 +1,149 @@
+package com.jeeplus.hotelorder.room.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.common.utils.ResponseUtil;
+import com.jeeplus.hotelorder.room.domain.HotelRoom;
+import com.jeeplus.hotelorder.room.service.HotelRoomService;
+import com.jeeplus.logging.annotation.ApiLog;
+import com.jeeplus.logging.constant.enums.LogTypeEnum;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+/**
+ * 酒店房间管理Controller
+ * @author jeeplus
+ */
+@Api("酒店房间管理")
+@RestController
+@RequestMapping(value = "/room")
+public class HotelRoomController {
+
+    private static Logger log = LoggerFactory.getLogger(HotelRoomController.class);
+
+    @Resource
+    private HotelRoomService hotelRoomService;
+
+    /**
+     * 分页查询房间列表
+     * @param hotelRoom 查询条件
+     * @param page 分页参数
+     * @return 分页结果
+     * @throws Exception 查询异常
+     */
+    @ApiLog("查询房间列表")
+    @PreAuthorize("hasAuthority('hotel:admin:room:list')")
+    @GetMapping("list")
+    public ResponseEntity<IPage<HotelRoom>> list(HotelRoom hotelRoom, Page<HotelRoom> page) throws Exception {
+        IPage<HotelRoom> result = hotelRoomService.findPage(page, hotelRoom);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 查询房间详情
+     * @param id 房间ID
+     * @return 房间信息
+     */
+    @ApiLog("查询房间详情")
+    @PreAuthorize("hasAuthority('hotel:admin:room:list')")
+    @GetMapping("queryById")
+    public ResponseEntity queryById(@RequestParam("id") String id) {
+        HotelRoom hotelRoom = hotelRoomService.queryById(id);
+        return ResponseEntity.ok(hotelRoom);
+    }
+
+    /**
+     * 新增/编辑房间
+     * 新增时自动生成二维码
+     * @param hotelRoom 房间信息
+     * @return 操作结果
+     * @throws Exception 业务异常
+     */
+    @ApiLog(value = "新增/编辑房间", type = LogTypeEnum.SAVE)
+    @PreAuthorize("hasAnyAuthority('hotel:admin:room:add','hotel:admin:room:edit')")
+    @PostMapping("save")
+    public ResponseEntity save(@Valid @RequestBody HotelRoom hotelRoom) throws Exception {
+        hotelRoomService.saveRoom(hotelRoom);
+        return ResponseEntity.ok("操作成功");
+    }
+
+    /**
+     * 切换房间状态
+     * @param id 房间ID
+     * @param status 目标状态(0-正常, 1-暂停, 2-停用)
+     * @return 操作结果
+     */
+    @ApiLog(value = "切换房间状态", type = LogTypeEnum.SAVE)
+    @PreAuthorize("hasAuthority('hotel:admin:room:edit')")
+    @PostMapping("toggleStatus")
+    public ResponseEntity toggleStatus(@RequestParam("id") String id, @RequestParam("status") String status) {
+        hotelRoomService.toggleStatus(id, status);
+        return ResponseEntity.ok("操作成功");
+    }
+
+    /**
+     * 补打二维码(内容不变,重新生成图片)
+     * @param id 房间ID
+     * @return 二维码Base64图片
+     * @throws Exception 生成异常
+     */
+    @ApiLog("补打二维码")
+    @PreAuthorize("hasAuthority('hotel:admin:room:list')")
+    @GetMapping("reprintQr")
+    public ResponseEntity reprintQr(@RequestParam("id") String id) throws Exception {
+        String base64Image = hotelRoomService.reprintQrCode(id);
+        return ResponseUtil.newInstance()
+                .add("qrCodeImage", base64Image)
+                .ok("补打成功");
+    }
+
+    /**
+     * 重新生成二维码(内容改变,旧码废弃)
+     * @param id 房间ID
+     * @return 新的二维码Base64图片
+     * @throws Exception 生成异常
+     */
+    @ApiLog(value = "重新生成二维码", type = LogTypeEnum.SAVE)
+    @PreAuthorize("hasAuthority('hotel:admin:room:edit')")
+    @PostMapping("regenerateQr")
+    public ResponseEntity regenerateQr(@RequestParam("id") String id) throws Exception {
+        String base64Image = hotelRoomService.regenerateQrCode(id);
+        return ResponseUtil.newInstance()
+                .add("qrCodeImage", base64Image)
+                .ok("重新生成成功");
+    }
+
+    /**
+     * 删除房间
+     * @param id 房间ID
+     * @return 操作结果
+     */
+    @ApiLog("删除房间")
+    @PreAuthorize("hasAuthority('hotel:admin:room:delete')")
+    @PostMapping("delete")
+    public ResponseEntity delete(@RequestParam("id") String id) {
+        hotelRoomService.deleteRoom(id);
+        return ResponseEntity.ok("删除成功");
+    }
+
+    /**
+     * 批量删除房间
+     * @param ids 房间ID,逗号分隔
+     * @return 操作结果
+     */
+    @ApiLog("批量删除房间")
+    @PreAuthorize("hasAuthority('hotel:admin:room:delete')")
+    @PostMapping("batchDelete")
+    public ResponseEntity batchDelete(@RequestParam("ids") String ids) {
+        hotelRoomService.batchDelete(ids);
+        return ResponseEntity.ok("批量删除成功");
+    }
+}

+ 98 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/controller/HotelRoomTypeController.java

@@ -0,0 +1,98 @@
+package com.jeeplus.hotelorder.room.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.hotelorder.room.domain.HotelRoomType;
+import com.jeeplus.hotelorder.room.service.HotelRoomTypeService;
+import com.jeeplus.logging.annotation.ApiLog;
+import com.jeeplus.logging.constant.enums.LogTypeEnum;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 房间类型管理Controller
+ * @author jeeplus
+ */
+@Api("房间类型管理")
+@RestController
+@RequestMapping(value = "/roomType")
+public class HotelRoomTypeController {
+
+    private static Logger log = LoggerFactory.getLogger(HotelRoomTypeController.class);
+
+    @Resource
+    private HotelRoomTypeService hotelRoomTypeService;
+
+    /**
+     * 分页查询房间类型
+     */
+    @ApiLog("查询房间类型列表")
+    @PreAuthorize("hasAuthority('hotel:admin:roomType:list')")
+    @GetMapping("list")
+    public ResponseEntity<IPage<HotelRoomType>> list(HotelRoomType hotelRoomType, Page<HotelRoomType> page) throws Exception {
+        IPage<HotelRoomType> result = hotelRoomTypeService.findPage(page, hotelRoomType);
+        return ResponseEntity.ok(result);
+    }
+
+    /**
+     * 查询所有启用的房间类型(下拉框用)
+     */
+    @ApiLog("查询所有房间类型")
+    @GetMapping("all")
+    public ResponseEntity<List<HotelRoomType>> all() {
+        List<HotelRoomType> list = hotelRoomTypeService.findAllEnabled();
+        return ResponseEntity.ok(list);
+    }
+
+    /**
+     * 查询房间类型详情
+     */
+    @ApiLog("查询房间类型详情")
+    @PreAuthorize("hasAuthority('hotel:admin:roomType:list')")
+    @GetMapping("queryById")
+    public ResponseEntity queryById(@RequestParam("id") String id) {
+        HotelRoomType hotelRoomType = hotelRoomTypeService.queryById(id);
+        return ResponseEntity.ok(hotelRoomType);
+    }
+
+    /**
+     * 新增/编辑房间类型
+     */
+    @ApiLog(value = "新增/编辑房间类型", type = LogTypeEnum.SAVE)
+    @PreAuthorize("hasAnyAuthority('hotel:admin:roomType:add','hotel:admin:roomType:edit')")
+    @PostMapping("save")
+    public ResponseEntity save(@RequestBody HotelRoomType hotelRoomType) {
+        hotelRoomTypeService.saveRoomType(hotelRoomType);
+        return ResponseEntity.ok("操作成功");
+    }
+
+    /**
+     * 切换状态(启用/禁用)
+     */
+    @ApiLog(value = "切换房间类型状态", type = LogTypeEnum.SAVE)
+    @PreAuthorize("hasAuthority('hotel:admin:roomType:edit')")
+    @PostMapping("toggleStatus")
+    public ResponseEntity toggleStatus(@RequestParam("id") String id) {
+        hotelRoomTypeService.toggleStatus(id);
+        return ResponseEntity.ok("操作成功");
+    }
+
+    /**
+     * 删除房间类型
+     */
+    @ApiLog("删除房间类型")
+    @PreAuthorize("hasAuthority('hotel:admin:roomType:delete')")
+    @PostMapping("delete")
+    public ResponseEntity delete(@RequestParam("id") String id) {
+        hotelRoomTypeService.deleteRoomType(id);
+        return ResponseEntity.ok("删除成功");
+    }
+}

+ 66 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/domain/HotelRoom.java

@@ -0,0 +1,66 @@
+package com.jeeplus.hotelorder.room.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import com.jeeplus.core.query.Query;
+import com.jeeplus.core.query.QueryType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Date;
+
+/**
+ * 酒店房间实体
+ * @author jeeplus
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_room")
+public class HotelRoom extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 房间号 */
+    @NotBlank(message = "房间号不能为空")
+    @Query(tableColumn = "room_no", type = QueryType.LIKE)
+    private String roomNo;
+
+    /** 房间类型ID */
+    @Query(tableColumn = "room_type", type = QueryType.EQ)
+    private String roomTypeId;
+
+    /** 房间类型名称(非DB字段,查询用) */
+    @TableField(exist = false)
+    private String roomTypeName;
+
+    /** 楼层 */
+    @Query(tableColumn = "floor", type = QueryType.EQ)
+    private Integer floor;
+
+    /**
+     * 房间状态: 0-正常, 1-暂停, 2-停用
+     */
+    @Query(tableColumn = "status", type = QueryType.EQ)
+    private String status;
+
+    /** 二维码完整URL */
+    private String qrCodeUrl;
+
+    /** 二维码图片存储路径 */
+    private String qrCodeImage;
+
+    /** 二维码签名值 */
+    private String qrCodeSign;
+
+    /** 房间短码(用于二维码URL) */
+    private String roomShortCode;
+
+    /** 二维码生成时间 */
+    private Date qrGeneratedTime;
+
+    /** 是否允许点餐: 1-允许, 0-不允许 */
+    @TableField(exist = false)
+    private Integer orderEnabled;
+}

+ 31 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/domain/HotelRoomType.java

@@ -0,0 +1,31 @@
+package com.jeeplus.hotelorder.room.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeeplus.core.domain.BaseEntity;
+import com.jeeplus.core.query.Query;
+import com.jeeplus.core.query.QueryType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 房间类型实体
+ * @author jeeplus
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName(value = "hotel_room_type")
+public class HotelRoomType extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 类型名称 */
+    @Query(tableColumn = "name", type = QueryType.LIKE)
+    private String name;
+
+    /** 排序号 */
+    private Integer sortOrder;
+
+    /** 状态: ENABLED-启用, DISABLED-禁用 */
+    @Query(tableColumn = "status", type = QueryType.EQ)
+    private String status;
+}

+ 14 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/HotelRoomMapper.java

@@ -0,0 +1,14 @@
+package com.jeeplus.hotelorder.room.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.room.domain.HotelRoom;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 酒店房间Mapper接口
+ * @author jeeplus
+ */
+@Mapper
+public interface HotelRoomMapper extends BaseMapper<HotelRoom> {
+
+}

+ 11 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/HotelRoomTypeMapper.java

@@ -0,0 +1,11 @@
+package com.jeeplus.hotelorder.room.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.hotelorder.room.domain.HotelRoomType;
+
+/**
+ * 房间类型Mapper
+ * @author jeeplus
+ */
+public interface HotelRoomTypeMapper extends BaseMapper<HotelRoomType> {
+}

+ 5 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/mapper/xml/HotelRoomMapper.xml

@@ -0,0 +1,5 @@
+<?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.jeeplus.hotelorder.room.mapper.HotelRoomMapper">
+
+</mapper>

+ 286 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/service/HotelRoomService.java

@@ -0,0 +1,286 @@
+package com.jeeplus.hotelorder.room.service;
+
+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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.jeeplus.core.query.QueryWrapperGenerator;
+import com.jeeplus.hotelorder.room.domain.HotelRoom;
+import com.jeeplus.hotelorder.room.domain.HotelRoomType;
+import com.jeeplus.hotelorder.room.mapper.HotelRoomMapper;
+import com.jeeplus.hotelorder.utils.QrCodeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.UUID;
+
+/**
+ * 酒店房间服务类
+ * @author jeeplus
+ */
+@Service
+public class HotelRoomService extends ServiceImpl<HotelRoomMapper, HotelRoom> {
+
+    private static Logger log = LoggerFactory.getLogger(HotelRoomService.class);
+
+    @Resource
+    private HotelRoomMapper hotelRoomMapper;
+
+    @Resource
+    private HotelRoomTypeService hotelRoomTypeService;
+
+    /** 二维码基础域名(生产环境配置到Nacos) */
+    @Value("${hotel.qr.base-url:https://hotel.example.com}")
+    private String qrBaseUrl;
+
+    /**
+     * 分页查询房间列表
+     * 利用@Query注解自动构建查询条件
+     * @param page 分页参数
+     * @param hotelRoom 查询条件
+     * @return 分页结果
+     * @throws Exception 查询异常
+     */
+    public IPage<HotelRoom> findPage(Page<HotelRoom> page, HotelRoom hotelRoom) throws Exception {
+        QueryWrapper<HotelRoom> queryWrapper = QueryWrapperGenerator.buildQueryCondition(hotelRoom, HotelRoom.class);
+        queryWrapper.eq("del_flag", 0);
+        queryWrapper.orderByAsc("floor").orderByAsc("room_no");
+        IPage<HotelRoom> result = hotelRoomMapper.selectPage(page, queryWrapper);
+        // 填充房间类型名称
+        fillRoomTypeName(result.getRecords());
+        return result;
+    }
+
+    /**
+     * 根据ID查询房间详情
+     * @param id 房间ID
+     * @return 房间信息
+     */
+    public HotelRoom queryById(String id) {
+        HotelRoom room = hotelRoomMapper.selectById(id);
+        if (room != null) {
+            fillRoomTypeName(java.util.Collections.singletonList(room));
+        }
+        return room;
+    }
+
+    /**
+     * 根据房间号查询房间(用于扫码入口)
+     * @param roomNo 房间号
+     * @return 房间信息
+     */
+    public HotelRoom getByRoomNo(String roomNo) {
+        QueryWrapper<HotelRoom> qw = new QueryWrapper<>();
+        qw.eq("room_no", roomNo);
+        qw.eq("del_flag", 0);
+        qw.last("LIMIT 1");
+        HotelRoom room = hotelRoomMapper.selectOne(qw);
+        if (room != null) {
+            fillRoomTypeName(java.util.Collections.singletonList(room));
+        }
+        return room;
+    }
+
+    /**
+     * 校验房间号是否已存在
+     * @param roomNo 房间号
+     * @param excludeId 排除的房间ID(编辑时传入)
+     * @return true-已存在, false-不存在
+     */
+    public boolean existsByRoomNo(String roomNo, String excludeId) {
+        QueryWrapper<HotelRoom> qw = new QueryWrapper<>();
+        qw.eq("room_no", roomNo);
+        qw.eq("del_flag", 0);
+        if (StringUtils.isNotBlank(excludeId)) {
+            qw.ne("id", excludeId);
+        }
+        return hotelRoomMapper.selectCount(qw) > 0;
+    }
+
+    /**
+     * 新增/编辑房间
+     * 新增时自动生成二维码,编辑时不改变二维码
+     * @param hotelRoom 房间信息
+     * @return 操作结果消息
+     * @throws Exception 业务异常
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String saveRoom(HotelRoom hotelRoom) throws Exception {
+        // 校验房间号唯一性
+        if (existsByRoomNo(hotelRoom.getRoomNo(), hotelRoom.getId())) {
+            throw new RuntimeException("房间号【" + hotelRoom.getRoomNo() + "】已存在");
+        }
+
+        if (StringUtils.isBlank(hotelRoom.getId())) {
+            // 新增房间
+            hotelRoom.setId(UUID.randomUUID().toString().replace("-", ""));
+            hotelRoom.setDelFlag(0);
+            if (StringUtils.isBlank(hotelRoom.getStatus())) {
+                hotelRoom.setStatus("0"); // 默认正常状态
+            }
+            // 生成房间短码
+            hotelRoom.setRoomShortCode(generateShortCode(hotelRoom.getRoomNo()));
+            // 自动生成二维码
+            generateQrCode(hotelRoom);
+            hotelRoomMapper.insert(hotelRoom);
+        } else {
+            // 编辑房间(不改变二维码)
+            hotelRoomMapper.updateById(hotelRoom);
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 切换房间状态(正常↔暂停停用)
+     * @param id 房间ID
+     * @param status 目标状态
+     * @return 操作结果消息
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String toggleStatus(String id, String status) {
+        HotelRoom room = hotelRoomMapper.selectById(id);
+        if (room == null) {
+            throw new RuntimeException("房间不存在");
+        }
+        room.setStatus(status);
+        hotelRoomMapper.updateById(room);
+        return "操作成功";
+    }
+
+    /**
+     * 补打二维码(内容不变,重新生成图片)
+     * @param id 房间ID
+     * @return 二维码Base64图片
+     * @throws Exception 生成异常
+     */
+    public String reprintQrCode(String id) throws Exception {
+        HotelRoom room = hotelRoomMapper.selectById(id);
+        if (room == null) {
+            throw new RuntimeException("房间不存在");
+        }
+        if (StringUtils.isBlank(room.getQrCodeUrl())) {
+            throw new RuntimeException("该房间尚未生成二维码");
+        }
+        // 使用已有的URL重新生成图片
+        String base64Image = QrCodeUtils.generateQrCodeBase64(room.getQrCodeUrl());
+        room.setQrCodeImage(base64Image);
+        hotelRoomMapper.updateById(room);
+        return base64Image;
+    }
+
+    /**
+     * 重新生成二维码(内容改变,旧码废弃)
+     * @param id 房间ID
+     * @return 新的二维码Base64图片
+     * @throws Exception 生成异常
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String regenerateQrCode(String id) throws Exception {
+        HotelRoom room = hotelRoomMapper.selectById(id);
+        if (room == null) {
+            throw new RuntimeException("房间不存在");
+        }
+        // 重新生成二维码
+        generateQrCode(room);
+        hotelRoomMapper.updateById(room);
+        return room.getQrCodeImage();
+    }
+
+    /**
+     * 删除房间(逻辑删除)
+     * @param id 房间ID
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteRoom(String id) {
+        hotelRoomMapper.deleteById(id);
+    }
+
+    /**
+     * 批量删除房间
+     * @param ids 房间ID,逗号分隔
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void batchDelete(String ids) {
+        String[] idArray = ids.split(",");
+        for (String id : idArray) {
+            hotelRoomMapper.deleteById(id.trim());
+        }
+    }
+
+    /**
+     * 为房间生成二维码
+     * 包含:构建URL → 生成签名 → 生成图片 → 设置到实体
+     * @param room 房间实体
+     * @throws Exception 生成异常
+     */
+    private void generateQrCode(HotelRoom room) throws Exception {
+        long timestamp = System.currentTimeMillis();
+        // 构建二维码URL
+        String qrUrl = QrCodeUtils.buildQrCodeUrl(
+                qrBaseUrl,
+                room.getRoomShortCode(),
+                room.getId(),
+                room.getRoomNo(),
+                timestamp
+        );
+        // 生成签名
+        String sign = QrCodeUtils.generateSign(room.getId(), room.getRoomNo(), timestamp);
+        // 生成二维码图片(Base64)
+        String base64Image = QrCodeUtils.generateQrCodeBase64(qrUrl);
+
+        room.setQrCodeUrl(qrUrl);
+        room.setQrCodeImage(base64Image);
+        room.setQrCodeSign(sign);
+        room.setQrGeneratedTime(new java.util.Date());
+    }
+
+    /**
+     * 根据房间号生成短码
+     * 使用房间号的Base62编码,保持URL简短
+     * @param roomNo 房间号
+     * @return 短码
+     */
+    private String generateShortCode(String roomNo) {
+        // 简单方案:直接使用房间号作为短码(如 "808" → "808")
+        // 如需更短可使用Base62编码
+        return roomNo;
+    }
+
+    /**
+     * 批量填充房间类型名称
+     */
+    private void fillRoomTypeName(java.util.List<HotelRoom> rooms) {
+        if (rooms == null || rooms.isEmpty()) {
+            return;
+        }
+        // 收集所有类型ID
+        java.util.Set<String> typeIds = new java.util.HashSet<>();
+        for (HotelRoom room : rooms) {
+            if (StringUtils.isNotBlank(room.getRoomTypeId())) {
+                typeIds.add(room.getRoomTypeId());
+            }
+        }
+        if (typeIds.isEmpty()) {
+            return;
+        }
+        // 批量查询类型
+        QueryWrapper<HotelRoomType> qw = new QueryWrapper<>();
+        qw.in("id", typeIds);
+        java.util.List<HotelRoomType> types = hotelRoomTypeService.list(qw);
+        java.util.Map<String, String> typeNameMap = new java.util.HashMap<>();
+        for (HotelRoomType type : types) {
+            typeNameMap.put(type.getId(), type.getName());
+        }
+        // 填充名称
+        for (HotelRoom room : rooms) {
+            if (StringUtils.isNotBlank(room.getRoomTypeId())) {
+                room.setRoomTypeName(typeNameMap.get(room.getRoomTypeId()));
+            }
+        }
+    }
+}

+ 102 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/room/service/HotelRoomTypeService.java

@@ -0,0 +1,102 @@
+package com.jeeplus.hotelorder.room.service;
+
+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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.jeeplus.core.query.QueryWrapperGenerator;
+import com.jeeplus.hotelorder.room.domain.HotelRoomType;
+import com.jeeplus.hotelorder.room.mapper.HotelRoomTypeMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 房间类型服务类
+ * @author jeeplus
+ */
+@Service
+public class HotelRoomTypeService extends ServiceImpl<HotelRoomTypeMapper, HotelRoomType> {
+
+    @Resource
+    private HotelRoomTypeMapper hotelRoomTypeMapper;
+
+    /**
+     * 分页查询房间类型
+     */
+    public IPage<HotelRoomType> findPage(Page<HotelRoomType> page, HotelRoomType hotelRoomType) throws Exception {
+        QueryWrapper<HotelRoomType> queryWrapper = QueryWrapperGenerator.buildQueryCondition(hotelRoomType, HotelRoomType.class);
+        queryWrapper.eq("del_flag", 0);
+        queryWrapper.orderByAsc("sort_order");
+        return hotelRoomTypeMapper.selectPage(page, queryWrapper);
+    }
+
+    /**
+     * 查询所有启用的房间类型(下拉框用)
+     */
+    public List<HotelRoomType> findAllEnabled() {
+        QueryWrapper<HotelRoomType> qw = new QueryWrapper<>();
+        qw.eq("status", "ENABLED").eq("del_flag", 0).orderByAsc("sort_order");
+        return hotelRoomTypeMapper.selectList(qw);
+    }
+
+    /**
+     * 根据ID查询
+     */
+    public HotelRoomType queryById(String id) {
+        return hotelRoomTypeMapper.selectById(id);
+    }
+
+    /**
+     * 保存(新增/编辑)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String saveRoomType(HotelRoomType hotelRoomType) {
+        if (StringUtils.isBlank(hotelRoomType.getId())) {
+            hotelRoomType.setId(UUID.randomUUID().toString().replace("-", ""));
+            hotelRoomType.setDelFlag(0);
+            if (StringUtils.isBlank(hotelRoomType.getStatus())) {
+                hotelRoomType.setStatus("ENABLED");
+            }
+            if (hotelRoomType.getSortOrder() == null) {
+                hotelRoomType.setSortOrder(0);
+            }
+            hotelRoomType.setCreateTime(new Date());
+            hotelRoomType.setUpdateTime(new Date());
+            hotelRoomTypeMapper.insert(hotelRoomType);
+        } else {
+            hotelRoomType.setUpdateTime(new Date());
+            hotelRoomTypeMapper.updateById(hotelRoomType);
+        }
+        return "操作成功";
+    }
+
+    /**
+     * 切换状态(启用/禁用)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String toggleStatus(String id) {
+        HotelRoomType roomType = hotelRoomTypeMapper.selectById(id);
+        if (roomType == null) {
+            throw new RuntimeException("房间类型不存在");
+        }
+        String newStatus = "ENABLED".equals(roomType.getStatus()) ? "DISABLED" : "ENABLED";
+        roomType.setStatus(newStatus);
+        roomType.setUpdateTime(new Date());
+        hotelRoomTypeMapper.updateById(roomType);
+        return "操作成功";
+    }
+
+    /**
+     * 删除
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteRoomType(String id) {
+        hotelRoomTypeMapper.deleteById(id);
+    }
+}

+ 99 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/java/com/jeeplus/hotelorder/utils/QrCodeUtils.java

@@ -0,0 +1,99 @@
+package com.jeeplus.hotelorder.utils;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.crypto.SecureUtil;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 二维码生成工具类
+ * 用于生成房间点餐二维码,包含签名校验机制
+ * @author jeeplus
+ */
+public class QrCodeUtils {
+
+    /** 二维码默认尺寸 */
+    private static final int DEFAULT_SIZE = 400;
+
+    /** 二维码签名密钥(生产环境应配置到Nacos) */
+    private static final String QR_SIGN_SECRET = "hotel_qr_secret_2026";
+
+    /**
+     * 生成二维码Base64图片
+     * @param content 二维码内容(URL)
+     * @return Base64编码的PNG图片
+     * @throws Exception 生成异常
+     */
+    public static String generateQrCodeBase64(String content) throws Exception {
+        return generateQrCodeBase64(content, DEFAULT_SIZE);
+    }
+
+    /**
+     * 生成二维码Base64图片(指定尺寸)
+     * @param content 二维码内容(URL)
+     * @param size 图片尺寸(像素)
+     * @return Base64编码的PNG图片
+     * @throws Exception 生成异常
+     */
+    public static String generateQrCodeBase64(String content, int size) throws Exception {
+        QRCodeWriter qrCodeWriter = new QRCodeWriter();
+        Map<EncodeHintType, Object> hints = new HashMap<>();
+        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
+        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+        hints.put(EncodeHintType.MARGIN, 4);
+
+        BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, size, size, hints);
+        ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
+        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
+        byte[] pngData = pngOutputStream.toByteArray();
+        return Base64.encode(pngData);
+    }
+
+    /**
+     * 生成二维码签名
+     * 签名算法:MD5(roomId + roomNo + timestamp + 固定密钥)
+     * @param roomId 房间ID
+     * @param roomNo 房间号
+     * @param timestamp 生成时间戳
+     * @return MD5签名值
+     */
+    public static String generateSign(String roomId, String roomNo, long timestamp) {
+        String raw = roomId + roomNo + timestamp + QR_SIGN_SECRET;
+        return SecureUtil.md5(raw);
+    }
+
+    /**
+     * 校验二维码签名
+     * @param roomId 房间ID
+     * @param roomNo 房间号
+     * @param timestamp 时间戳
+     * @param sign 待校验的签名
+     * @return 签名是否有效
+     */
+    public static boolean verifySign(String roomId, String roomNo, long timestamp, String sign) {
+        String expectedSign = generateSign(roomId, roomNo, timestamp);
+        return expectedSign.equals(sign);
+    }
+
+    /**
+     * 构建二维码URL内容
+     * @param baseUrl 基础域名(如 https://hotel.example.com)
+     * @param shortCode 房间短码
+     * @param roomId 房间ID
+     * @param roomNo 房间号
+     * @param timestamp 时间戳
+     * @return 完整的二维码URL
+     */
+    public static String buildQrCodeUrl(String baseUrl, String shortCode, String roomId, String roomNo, long timestamp) {
+        String sign = generateSign(roomId, roomNo, timestamp);
+        return baseUrl + "/h5/order?r=" + shortCode + "&sign=" + sign;
+    }
+}

+ 69 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/resources/bootstrap.yml

@@ -0,0 +1,69 @@
+# Tomcat
+server:
+  port: 9529
+  servlet:
+    context-path:
+
+# Spring
+spring:
+  main:
+    allow-bean-definition-overriding: true
+  application:
+    # 应用名称
+    name: jeeplus-hotel-order
+  boot:
+    admin:
+      client:
+        url: http://localhost:8989
+  profiles:
+    # 环境配置
+    active: pro
+  cloud:
+    nacos:
+      discovery:
+        # 服务注册地址
+        server-addr: 127.0.0.1:8848
+        # 命名空间
+        namespace: ${spring.profiles.active}
+        username: nacos
+        password: jsxgpassword
+      config:
+        # 配置中心地址
+        server-addr: 127.0.0.1:8848
+        # 配置文件格式
+        file-extension: yml
+        # 共享配置
+        shared-configs:
+          - application.${spring.cloud.nacos.config.file-extension}
+        # 命名空间
+        namespace: ${spring.profiles.active}
+        username: ${spring.cloud.nacos.discovery.username}
+        password: ${spring.cloud.nacos.discovery.password}
+      auth:
+        plugin:
+          nacos:
+            token:
+              secret:
+                key: cCtJSWRxbW1xT3FmZkh1RTBpdTltYkM5ZlNSaTRyQ1RiSzJzV1pweEYycz0=
+    sentinel:
+      transport:
+        dashboard: 127.0.0.1:8858
+
+#ribbon的超时时间
+ribbon:
+  ConnectTimeout: 60000 # 连接超时时间(ms)
+  ReadTimeout: 60000 # 通信超时时间(ms)
+
+hystrix:
+  command:
+    default:
+      execution:
+        isolation:
+          thread:
+            timeoutInMillisecond: 60000 # 熔断超时时长:60000ms
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: "*"

+ 19 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/dish/mapper/HotelDishCategoryMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.hotelorder.dish.mapper.HotelDishCategoryMapper">
+
+    <!-- 分页查询分类(带菜品数量统计) -->
+    <select id="selectCategoryWithCount" resultType="com.jeeplus.hotelorder.dish.domain.HotelDishCategory">
+        SELECT c.*, IFNULL(d.dish_count, 0) AS dish_count
+        FROM hotel_dish_category c
+        LEFT JOIN (
+            SELECT category_id, COUNT(*) AS dish_count
+            FROM hotel_dish
+            WHERE del_flag = 0
+            GROUP BY category_id
+        ) d ON c.id = d.category_id
+        WHERE c.del_flag = 0
+        ORDER BY c.sort_order ASC
+    </select>
+
+</mapper>

+ 57 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/dish/mapper/HotelDishMapper.xml

@@ -0,0 +1,57 @@
+<?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.jeeplus.hotelorder.dish.mapper.HotelDishMapper">
+
+    <!-- 客户端菜品列表(只查在售且未售罄) -->
+    <select id="selectClientList" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT a.*, c.name AS category_name
+        FROM hotel_dish a
+        LEFT JOIN hotel_dish_category c ON a.category_id = c.id AND c.del_flag = 0
+        WHERE a.del_flag = 0
+          AND a.status = 'ON_SALE'
+          AND (a.sold_out IS NULL OR a.sold_out = 0)
+        <if test="categoryId != null and categoryId != ''">
+            AND a.category_id = #{categoryId}
+        </if>
+        <if test="keyword != null and keyword != ''">
+            AND (a.name LIKE CONCAT('%', #{keyword}, '%') OR a.tags LIKE CONCAT('%', #{keyword}, '%'))
+        </if>
+        ORDER BY a.is_recommended DESC, a.sort_order ASC, a.sales DESC
+    </select>
+
+    <!-- 管理后台菜品分页 -->
+    <select id="selectAdminPage" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT a.*, c.name AS category_name
+        FROM hotel_dish a
+        LEFT JOIN hotel_dish_category c ON a.category_id = c.id AND c.del_flag = 0
+        WHERE a.del_flag = 0
+        <if test="categoryId != null and categoryId != ''">
+            AND a.category_id = #{categoryId}
+        </if>
+        <if test="status != null and status != ''">
+            AND a.status = #{status}
+        </if>
+        <if test="name != null and name != ''">
+            AND a.name LIKE CONCAT('%', #{name}, '%')
+        </if>
+        ORDER BY a.sort_order ASC, a.sales DESC
+    </select>
+
+    <!-- 销量排行 -->
+    <select id="selectSalesRanking" resultType="com.jeeplus.hotelorder.dish.domain.HotelDish">
+        SELECT * FROM hotel_dish
+        WHERE del_flag = 0 AND status = 'ON_SALE'
+        ORDER BY sales DESC
+        LIMIT #{limit}
+    </select>
+
+    <!-- 批量更新销量 -->
+    <update id="batchUpdateSales">
+        <foreach collection="items" item="item" separator=";">
+            UPDATE hotel_dish
+            SET sales = IFNULL(sales, 0) + #{item.quantity}, update_time = NOW()
+            WHERE id = #{item.dishId} AND del_flag = 0
+        </foreach>
+    </update>
+
+</mapper>

+ 53 - 0
jeeplus-modules/jeeplus-hotel-order/src/main/resources/com/jeeplus/hotelorder/order/mapper/HotelOrderMapper.xml

@@ -0,0 +1,53 @@
+<?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.jeeplus.hotelorder.order.mapper.HotelOrderMapper">
+
+    <!-- 订单看板统计 -->
+    <select id="selectDashboard" resultType="java.util.Map">
+        SELECT
+            COUNT(CASE WHEN status = 'PLACED' THEN 1 END) AS pendingCount,
+            COUNT(CASE WHEN status = 'PREPARING' THEN 1 END) AS preparingCount,
+            COUNT(CASE WHEN status = 'READY' THEN 1 END) AS readyCount,
+            COUNT(CASE WHEN status = 'DELIVERING' THEN 1 END) AS deliveringCount,
+            COUNT(CASE WHEN status NOT IN ('REJECTED','REFUNDED') THEN 1 END) AS todayTotal,
+            IFNULL(SUM(CASE WHEN status = 'COMPLETED' THEN total_amount ELSE 0 END), 0) AS todayRevenue
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND DATE(create_time) = CURDATE()
+    </select>
+
+    <!-- 统计今日有效订单数 -->
+    <select id="countTodayOrders" resultType="int">
+        SELECT COUNT(*)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND DATE(create_time) = CURDATE()
+          AND status NOT IN ('REJECTED', 'REFUNDED')
+    </select>
+
+    <!-- 统计今日营业额 -->
+    <select id="sumTodayRevenue" resultType="java.math.BigDecimal">
+        SELECT IFNULL(SUM(total_amount), 0)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND DATE(create_time) = #{today}
+          AND status = 'COMPLETED'
+    </select>
+
+    <!-- 查询指定房间今日订单数 -->
+    <select id="countTodayByRoomNo" resultType="int">
+        SELECT COUNT(*)
+        FROM hotel_order
+        WHERE del_flag = 0
+          AND room_no = #{roomNo}
+          AND DATE(create_time) = CURDATE()
+    </select>
+
+    <!-- 查询当天最大订单号 -->
+    <select id="selectMaxOrderNo" resultType="java.lang.String">
+        SELECT MAX(order_no)
+        FROM hotel_order
+        WHERE order_no LIKE CONCAT(#{prefix}, '%')
+    </select>
+
+</mapper>

+ 1 - 0
jeeplus-modules/pom.xml

@@ -30,6 +30,7 @@
         <module>jeeplus-ccpm</module>
         <module>jeeplus-consult</module>
         <module>jeeplus-psi-management</module> <!-- 你的新模块文件夹名 -->
+        <module>jeeplus-hotel-order</module>
     </modules>
 
 </project>