5 Комити 8e7fca9150 ... 4786bd98ef

Аутор SHA1 Порука Датум
  penghao 4786bd98ef feat: 新增点餐管理餐食费查询、编辑保存 пре 2 месеци
  penghao b786921565 feat: 添加价目表菜品分类、标签和描述字段支持 пре 2 месеци
  penghao 30547fcd76 feat: 商家端点餐管理菜品分类相关功能开发 пре 2 месеци
  penghao bad6e483f4 feat: 漏提的桌号相关dto和vo пре 2 месеци
  penghao c25c3a7bba feat: 商家端点餐桌号管理相关功能及微信小程序二维码生成 пре 2 месеци
36 измењених фајлова са 2157 додато и 96 уклоњено
  1. 24 4
      alien-entity/src/main/java/shop/alien/entity/store/StoreCuisine.java
  2. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCuisineCategory.java
  3. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java
  4. 74 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java
  5. 78 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTableLog.java
  6. 21 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/BatchQueryTableStatusDTO.java
  7. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CuisineComboDto.java
  8. 18 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CuisineTypeResponseDto.java
  9. 32 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategoryDTO.java
  10. 24 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategorySortDTO.java
  11. 0 36
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffFitnessCourseGroup.java
  12. 0 37
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffFitnessCourseItem.java
  13. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreTableChangeDTO.java
  14. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreTableDTO.java
  15. 22 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/TablewareFeeDto.java
  16. 6 4
      alien-entity/src/main/java/shop/alien/entity/store/vo/PriceListVo.java
  17. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreTableStatusVO.java
  18. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCuisineCategoryMapper.java
  19. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTableLogMapper.java
  20. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTableMapper.java
  21. 10 3
      alien-entity/src/main/resources/mapper/StoreCuisineMapper.xml
  22. 62 0
      alien-store/src/main/java/shop/alien/store/config/WeChatMiniProgramConfig.java
  23. 147 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java
  24. 45 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java
  25. 192 0
      alien-store/src/main/java/shop/alien/store/controller/StoreTableController.java
  26. 58 0
      alien-store/src/main/java/shop/alien/store/service/StoreCuisineCategoryService.java
  27. 8 0
      alien-store/src/main/java/shop/alien/store/service/StoreCuisineService.java
  28. 9 0
      alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java
  29. 72 0
      alien-store/src/main/java/shop/alien/store/service/StoreTableService.java
  30. 47 0
      alien-store/src/main/java/shop/alien/store/service/WeChatMiniProgramQrCodeService.java
  31. 200 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineCategoryServiceImpl.java
  32. 108 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineServiceImpl.java
  33. 18 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  34. 0 12
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java
  35. 370 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreTableServiceImpl.java
  36. 300 0
      alien-store/src/main/java/shop/alien/store/service/impl/WeChatMiniProgramQrCodeServiceImpl.java

+ 24 - 4
alien-entity/src/main/java/shop/alien/entity/store/StoreCuisine.java

@@ -30,6 +30,14 @@ public class StoreCuisine {
     @TableField("store_id")
     private Integer storeId;
 
+    @ApiModelProperty(value = "美食类型: 1-单品,2-套餐")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+    @TableField("category_ids")
+    private String categoryIds;
+
     @ApiModelProperty(value = "菜名")
     @TableField("name")
     private String name;
@@ -38,6 +46,18 @@ public class StoreCuisine {
     @TableField("total_price")
     private BigDecimal totalPrice;
 
+    @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+    @TableField("is_homepage_display")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    @TableField("tags")
+    private String tags;
+
+    @ApiModelProperty(value = "菜品短评")
+    @TableField("dish_review")
+    private String dishReview;
+
     @ApiModelProperty(value = "图片列表,最多 9 张 URL")
     @TableField("images")
     private String images;
@@ -54,6 +74,10 @@ public class StoreCuisine {
     @TableField("detail_content")
     private String detailContent;
 
+    @ApiModelProperty(value = "菜品描述")
+    @TableField("description")
+    private String description;
+
     @ApiModelProperty(value = "补充说明")
     @TableField("extra_note")
     private String extraNote;
@@ -86,10 +110,6 @@ public class StoreCuisine {
     @TableField("rejection_reason")
     private String rejectionReason;
 
-    @ApiModelProperty(value = "美食类型: 1-单品,2-套餐")
-    @TableField("cuisine_type")
-    private Integer cuisineType;
-
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic

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

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

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java

@@ -355,4 +355,8 @@ public class StoreInfo {
     @TableField("store_tickets")
     private Integer storeTickets;
 
+    @ApiModelProperty(value = "餐具费(限2位正整数)")
+    @TableField("tableware_fee")
+    private Integer tablewareFee;
+
 }

+ 74 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java

@@ -0,0 +1,74 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 桌号表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_table")
+@ApiModel(value = "StoreTable对象", description = "桌号表")
+public class StoreTable {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号")
+    @TableField("table_number")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "当前进行中的订单ID(订单结账或取消后清空)")
+    @TableField("current_order_id")
+    private Integer currentOrderId;
+
+    @ApiModelProperty(value = "二维码URL")
+    @TableField("qrcode_url")
+    private String qrcodeUrl;
+
+    @ApiModelProperty(value = "状态(0:空闲, 1:就餐中, 2:其他)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

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

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

+ 21 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/BatchQueryTableStatusDTO.java

@@ -0,0 +1,21 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 批量查询桌号状态DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "BatchQueryTableStatusDTO对象", description = "批量查询桌号状态")
+public class BatchQueryTableStatusDTO {
+
+    @ApiModelProperty(value = "桌号ID列表", required = true)
+    private List<Integer> tableIds;
+}

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

@@ -28,6 +28,10 @@ public class CuisineComboDto {
     @TableField("cuisine_type")
     private Integer cuisineType;
 
+    @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+    @TableField("category_ids")
+    private String categoryIds;
+
     @ApiModelProperty(value = "商户id")
     @TableField("store_id")
     private Integer storeId;
@@ -40,6 +44,18 @@ public class CuisineComboDto {
     @TableField("total_price")
     private BigDecimal totalPrice;
 
+    @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+    @TableField("is_homepage_display")
+    private Integer isHomepageDisplay;
+
+    @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+    @TableField("tags")
+    private String tags;
+
+    @ApiModelProperty(value = "菜品短评")
+    @TableField("dish_review")
+    private String dishReview;
+
     @ApiModelProperty(value = "图片列表,最多 9 张 URL")
     @TableField("images")
     private String images;
@@ -52,6 +68,10 @@ public class CuisineComboDto {
     @TableField("detail_content")
     private String detailContent;
 
+    @ApiModelProperty(value = "菜品描述")
+    @TableField("description")
+    private String description;
+
     @ApiModelProperty(value = "补充说明")
     @TableField("extra_note")
     private String extraNote;

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CuisineTypeResponseDto.java

@@ -38,6 +38,21 @@ public class CuisineTypeResponseDto {
         @ApiModelProperty(value = "总价")
         private BigDecimal totalPrice;
 
+        @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+        private String categoryIds;
+
+        @ApiModelProperty(value = "菜品分类名称(JSON数组,如:[\"热菜\",\"凉菜\"])")
+        private String categoryNames;
+
+        @ApiModelProperty(value = "首页展示(0:否, 1:是)")
+        private Integer isHomepageDisplay;
+
+        @ApiModelProperty(value = "菜品标签(JSON数组,如:[\"招牌菜\",\"推荐\"])")
+        private String tags;
+
+        @ApiModelProperty(value = "菜品短评")
+        private String dishReview;
+
         @ApiModelProperty(value = "图片列表,最多 9 张 URL,逗号分隔")
         private String images;
 
@@ -50,6 +65,9 @@ public class CuisineTypeResponseDto {
         @ApiModelProperty(value = "图文详情-文字")
         private String detailContent;
 
+        @ApiModelProperty(value = "菜品描述")
+        private String description;
+
         @ApiModelProperty(value = "补充说明")
         private String extraNote;
 

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

@@ -0,0 +1,32 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 菜品分类管理DTO
+ * 用于批量创建和编辑菜品分类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreCuisineCategoryDTO对象", description = "菜品分类管理DTO")
+public class StoreCuisineCategoryDTO {
+
+    @ApiModelProperty(value = "分类ID", notes = "编辑分类时必填")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", notes = "批量创建分类时必填")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类名称", notes = "编辑分类时必填")
+    private String categoryName;
+
+    @ApiModelProperty(value = "分类名称列表", notes = "批量创建分类时必填,多个分类名称用英文逗号分隔,如:热菜,水果,甜品")
+    private String categoryNames;
+
+}

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategorySortDTO.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 菜品分类排序DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreCuisineCategorySortDTO对象", description = "菜品分类排序DTO")
+public class StoreCuisineCategorySortDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类排序列表", required = true, notes = "按顺序排列的分类ID列表")
+    private List<Integer> categoryIds;
+}

+ 0 - 36
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffFitnessCourseGroup.java

@@ -1,36 +0,0 @@
-package shop.alien.entity.store.dto;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 运动健身-课程类型分组(用于前端“课程类型下多项目”的绑定;不直接映射数据库表)
- */
-@Data
-@JsonInclude
-@ApiModel(value = "StoreStaffFitnessCourseGroup对象", description = "运动健身-课程类型分组(分组结构用)")
-public class StoreStaffFitnessCourseGroup implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 课程类型编码:
-     * - 推荐传字典 dictId(更稳定)
-     * - 兼容传字典 dictDetail(名称)
-     */
-    @ApiModelProperty(value = "课程类型(推荐 dictId;兼容 dictDetail)")
-    private String courseType;
-
-    @ApiModelProperty(value = "课程类型名称(可选,便于回填展示)")
-    private String courseTypeName;
-
-    @ApiModelProperty(value = "项目列表")
-    private List<StoreStaffFitnessCourseItem> items;
-}
-
-

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

@@ -1,37 +0,0 @@
-package shop.alien.entity.store.dto;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-
-/**
- * 运动健身-课程项目(用于前端分组绑定;不直接映射数据库表)
- */
-@Data
-@JsonInclude
-@ApiModel(value = "StoreStaffFitnessCourseItem对象", description = "运动健身-课程项目(分组结构用)")
-public class StoreStaffFitnessCourseItem implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @ApiModelProperty(value = "项目名称")
-    private String courseName;
-
-    @ApiModelProperty(value = "价格类型(0-固定价 1-价格区间)")
-    private Integer coursePriceType;
-
-    @ApiModelProperty(value = "固定价(coursePriceType=0)")
-    private BigDecimal coursePrice;
-
-    @ApiModelProperty(value = "最低价(coursePriceType=1)")
-    private BigDecimal courseMinPrice;
-
-    @ApiModelProperty(value = "最高价(coursePriceType=1)")
-    private BigDecimal courseMaxPrice;
-}
-
-

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreTableChangeDTO.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 换桌DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreTableChangeDTO对象", description = "换桌")
+public class StoreTableChangeDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    private Integer orderId;
+
+    @ApiModelProperty(value = "原桌号ID", required = true)
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "目标桌号ID", required = true)
+    private Integer toTableId;
+
+    @ApiModelProperty(value = "换桌原因")
+    private String changeReason;
+}

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

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 桌号管理DTO
+ * 用于批量创建桌号和编辑桌号
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreTableDTO对象", description = "桌号管理DTO")
+public class StoreTableDTO {
+
+    @ApiModelProperty(value = "桌号ID", notes = "编辑桌号时必填")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", notes = "批量创建桌号时必填")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号名称", notes = "编辑桌号时必填")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "桌号列表", notes = "批量创建桌号时必填,多个桌号用英文逗号分隔,如:A01,A02,A03")
+    private String tableNumbers;
+}

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

@@ -0,0 +1,22 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 餐具费DTO
+ *
+ * @author auto-generated
+ * @since 2025-01-27
+ */
+@Data
+@ApiModel(value = "TablewareFeeDto", description = "餐具费DTO")
+public class TablewareFeeDto {
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "餐具费(限2位正整数)")
+    private Integer tablewareFee;
+}

+ 6 - 4
alien-entity/src/main/java/shop/alien/entity/store/vo/PriceListVo.java

@@ -1,18 +1,14 @@
 package shop.alien.entity.store.vo;
 
 import com.baomidou.mybatisplus.annotation.*;
-import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
-import shop.alien.entity.store.StoreCuisine;
-import shop.alien.entity.store.StorePrice;
 
 import java.math.BigDecimal;
 import java.util.Date;
-import java.util.List;
 
 /**
  * 价目表通用VO
@@ -74,6 +70,12 @@ public class PriceListVo {
     @ApiModelProperty(value = "美食类型: 1-单品,2-套餐")
     private Integer cuisineType;
 
+    @ApiModelProperty(value = "菜品分类ids(JSON数组,如:[1,2,3])")
+    private String categoryIds;
+
+    @ApiModelProperty(value = "菜品分类名称(JSON数组,如:[\"热菜\",\"凉菜\"])")
+    private String categoryNames;
+
     @ApiModelProperty(value = "拒绝原因")
     private String rejectionReason;
 

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreTableStatusVO.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 桌号状态VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreTableStatusVO对象", description = "桌号状态")
+public class StoreTableStatusVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "状态(0:空闲, 1:就餐中, 2:其他)")
+    private Integer status;
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreCuisineCategoryMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreCuisineCategory;
+
+/**
+ * 菜品分类表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreCuisineCategoryMapper extends BaseMapper<StoreCuisineCategory> {
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreTableLogMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreTableLog;
+
+/**
+ * 桌号换桌记录表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreTableLogMapper extends BaseMapper<StoreTableLog> {
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreTableMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreTable;
+
+/**
+ * 桌号表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreTableMapper extends BaseMapper<StoreTable> {
+}

+ 10 - 3
alien-entity/src/main/resources/mapper/StoreCuisineMapper.xml

@@ -8,12 +8,18 @@
     <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreCuisine">
         <id column="id" property="id" />
         <result column="store_id" property="storeId" />
+        <result column="cuisine_type" property="cuisineType" />
+        <result column="category_ids" property="categoryIds" />
         <result column="name" property="name" />
         <result column="total_price" property="totalPrice" />
+        <result column="is_homepage_display" property="isHomepageDisplay" />
+        <result column="tags" property="tags" />
+        <result column="dish_review" property="dishReview" />
         <result column="images" property="images" />
         <result column="image_content" property="imageContent" />
         <result column="raw_json" property="rawJson" />
         <result column="detail_content" property="detailContent" />
+        <result column="description" property="description" />
         <result column="extra_note" property="extraNote" />
         <result column="need_reserve" property="needReserve" />
         <result column="reserve_rule" property="reserveRule" />
@@ -31,9 +37,10 @@
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, store_id, name, total_price, images, image_content, raw_json, detail_content,
-        extra_note, need_reserve, reserve_rule, people_limit, usage_rule, status, shelf_status,
-        rejection_reason, delete_flag, created_user_id, updated_user_id, created_time, updated_time
+        id, store_id, cuisine_type, category_ids, name, total_price, is_homepage_display, tags, dish_review,
+        images, image_content, raw_json, detail_content, description, extra_note, need_reserve, reserve_rule,
+        people_limit, usage_rule, status, shelf_status, rejection_reason, delete_flag, created_user_id,
+        updated_user_id, created_time, updated_time
     </sql>
 
 </mapper>

+ 62 - 0
alien-store/src/main/java/shop/alien/store/config/WeChatMiniProgramConfig.java

@@ -0,0 +1,62 @@
+package shop.alien.store.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+/**
+ * 微信小程序配置
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "wechat.miniprogram")
+public class WeChatMiniProgramConfig {
+
+    /**
+     * 小程序AppID
+     */
+    private String appId;
+
+    /**
+     * 小程序AppSecret
+     */
+    private String appSecret;
+
+    /**
+     * 小程序页面路径
+     */
+    private String pagePath;
+
+    /**
+     * 环境版本:release-正式版,trial-体验版,develop-开发版
+     */
+    private String envVersion;
+
+    /**
+     * 二维码配置
+     */
+    private QrCodeConfig qrcode = new QrCodeConfig();
+
+    @Data
+    public static class QrCodeConfig {
+        /**
+         * 是否使用OSS存储
+         */
+        private boolean useOss = true;
+
+        /**
+         * OSS Bucket名称
+         */
+        private String ossBucket;
+
+        /**
+         * 本地存储路径(不使用OSS时)
+         */
+        private String localPath;
+    }
+}

+ 147 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java

@@ -0,0 +1,147 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.dto.StoreCuisineCategoryDTO;
+import shop.alien.entity.store.dto.StoreCuisineCategorySortDTO;
+import shop.alien.store.service.StoreCuisineCategoryService;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 菜品分类管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"商家端点餐管理-菜品分类管理"})
+@ApiSort(14)
+@CrossOrigin
+@RestController
+@RequestMapping("/storeCuisineCategory")
+@RequiredArgsConstructor
+public class StoreCuisineCategoryController {
+
+    private final StoreCuisineCategoryService storeCuisineCategoryService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询菜品分类列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getCategoryList")
+    public R<List<StoreCuisineCategory>> getCategoryList(@RequestParam Integer storeId) {
+        log.info("StoreCuisineCategoryController.getCategoryList?storeId={}", storeId);
+        return R.data(storeCuisineCategoryService.getCategoryList(storeId));
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("批量创建菜品分类")
+    @PostMapping("/batchCreateCategories")
+    public R<Boolean> batchCreateCategories(@RequestBody StoreCuisineCategoryDTO dto) {
+        log.info("StoreCuisineCategoryController.batchCreateCategories?dto={}", dto);
+        
+        if (dto.getStoreId() == null || !StringUtils.hasText(dto.getCategoryNames())) {
+            return R.fail("门店ID和分类名称列表不能为空");
+        }
+
+        // 解析分类名称列表
+        List<String> categoryNameList = Arrays.stream(dto.getCategoryNames().split(","))
+                .map(String::trim)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (categoryNameList.isEmpty()) {
+            return R.fail("分类名称列表不能为空");
+        }
+
+        try {
+            boolean result = storeCuisineCategoryService.batchCreateCategories(dto.getStoreId(), categoryNameList);
+            if (result) {
+                return R.success("批量创建菜品分类成功");
+            } else {
+                return R.fail("批量创建菜品分类失败");
+            }
+        } catch (Exception e) {
+            log.error("批量创建菜品分类失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("编辑菜品分类")
+    @PostMapping("/updateCategory")
+    public R<Boolean> updateCategory(@RequestBody StoreCuisineCategoryDTO dto) {
+        log.info("StoreCuisineCategoryController.updateCategory?dto={}", dto);
+        
+        if (dto.getId() == null || !StringUtils.hasText(dto.getCategoryName())) {
+            return R.fail("分类ID和分类名称不能为空");
+        }
+        
+        try {
+            boolean result = storeCuisineCategoryService.updateCategory(dto.getId(), dto.getCategoryName());
+            if (result) {
+                return R.success("更新菜品分类成功");
+            } else {
+                return R.fail("更新菜品分类失败");
+            }
+        } catch (Exception e) {
+            log.error("更新菜品分类失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除菜品分类")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "分类ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/deleteCategory")
+    public R<Boolean> deleteCategory(@RequestParam Integer id) {
+        log.info("StoreCuisineCategoryController.deleteCategory?id={}", id);
+        
+        try {
+            boolean result = storeCuisineCategoryService.deleteCategory(id);
+            if (result) {
+                return R.success("删除菜品分类成功");
+            } else {
+                return R.fail("删除菜品分类失败");
+            }
+        } catch (Exception e) {
+            log.error("删除菜品分类失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("更新菜品分类排序")
+    @PostMapping("/updateCategorySort")
+    public R<Boolean> updateCategorySort(@RequestBody StoreCuisineCategorySortDTO dto) {
+        log.info("StoreCuisineCategoryController.updateCategorySort?dto={}", dto);
+        
+        if (dto.getStoreId() == null || dto.getCategoryIds() == null || dto.getCategoryIds().isEmpty()) {
+            return R.fail("门店ID和分类ID列表不能为空");
+        }
+        
+        try {
+            boolean result = storeCuisineCategoryService.updateCategorySort(dto.getStoreId(), dto.getCategoryIds());
+            if (result) {
+                return R.success("更新菜品分类排序成功");
+            } else {
+                return R.fail("更新菜品分类排序失败");
+            }
+        } catch (Exception e) {
+            log.error("更新菜品分类排序失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 45 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java

@@ -11,12 +11,15 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StorePrice;
 import shop.alien.entity.store.dto.CuisineComboDto;
 import shop.alien.entity.store.dto.CuisineTypeResponseDto;
+import shop.alien.entity.store.dto.TablewareFeeDto;
 import shop.alien.entity.store.vo.PriceListVo;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreCuisineService;
+import shop.alien.store.service.StoreInfoService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.util.ai.AiGetPriceUtil;
 
@@ -44,6 +47,8 @@ public class StoreCuisineController {
 
     private final StorePriceService storePriceService;
 
+    private final StoreInfoService storeInfoService;
+
     @ApiOperation("新增美食套餐或单品")
     @ApiOperationSupport(order = 1)
     @PostMapping("/addCuisineCombo")
@@ -181,6 +186,8 @@ public class StoreCuisineController {
                     PriceListVo vo = new PriceListVo();
 
                     BeanUtils.copyProperties(cuisine, vo);
+                    // 设置分类名称
+                    vo.setCategoryNames(storeCuisineService.getCategoryNames(cuisine.getCategoryIds()));
                     priceListVo.add(vo);
                 }
             }
@@ -246,6 +253,44 @@ public class StoreCuisineController {
         String price = priceObj.toString();
         return R.data(price);
     }
+
+    @ApiOperation("保存或更新餐具费")
+    @ApiOperationSupport(order = 8)
+    @PostMapping("/saveTablewareFee")
+    public R<String> saveTablewareFee(@RequestBody TablewareFeeDto tablewareFeeDto) {
+        log.info("StoreCuisineController.saveTablewareFee?storeId={},tablewareFee={}", 
+                tablewareFeeDto.getStoreId(), tablewareFeeDto.getTablewareFee());
+        
+        if (tablewareFeeDto.getStoreId() == null) {
+            return R.fail("门店ID不能为空");
+        }
+        
+        if (storeInfoService.saveOrUpdateTablewareFee(tablewareFeeDto.getStoreId(), tablewareFeeDto.getTablewareFee())) {
+            return R.success("保存成功");
+        }
+        return R.fail("保存失败");
+    }
+
+    @ApiOperation("查询餐具费")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getTablewareFee")
+    public R<Integer> getTablewareFee(@RequestParam("storeId") Integer storeId) {
+        log.info("StoreCuisineController.getTablewareFee?storeId={}", storeId);
+        
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        
+        StoreInfo storeInfo = storeInfoService.getById(storeId);
+        if (storeInfo == null) {
+            return R.fail("门店不存在");
+        }
+        
+        return R.data(storeInfo.getTablewareFee());
+    }
 }
 
 

+ 192 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreTableController.java

@@ -0,0 +1,192 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.BatchQueryTableStatusDTO;
+import shop.alien.entity.store.dto.StoreTableChangeDTO;
+import shop.alien.entity.store.dto.StoreTableDTO;
+import shop.alien.entity.store.vo.StoreTableStatusVO;
+import shop.alien.store.service.StoreTableService;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 桌号管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"商家端点餐管理-桌号管理"})
+@ApiSort(13)
+@CrossOrigin
+@RestController
+@RequestMapping("/storeTable")
+@RequiredArgsConstructor
+public class StoreTableController {
+
+    private final StoreTableService storeTableService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("分页查询桌号列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页数", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "页容", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "status", value = "状态(0:空闲, 1:就餐中, 2:其他)", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getTablePage")
+    public R<IPage<StoreTable>> getTablePage(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer status) {
+        log.info("StoreTableController.getTablePage?pageNum={}&pageSize={}&storeId={}&status={}", pageNum, pageSize, storeId, status);
+        return R.data(storeTableService.getTablePage(pageNum, pageSize, storeId, status));
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("查询桌号详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "桌号ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getTableDetail")
+    public R<StoreTable> getTableDetail(@RequestParam Integer id) {
+        log.info("StoreTableController.getTableDetail?id={}", id);
+        StoreTable table = storeTableService.getById(id);
+        if (table == null) {
+            return R.fail("桌号不存在");
+        }
+        return R.data(table);
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("批量创建桌号")
+    @PostMapping("/batchCreateTables")
+    public R<Boolean> batchCreateTables(@RequestBody StoreTableDTO dto) {
+        log.info("StoreTableController.batchCreateTables?dto={}", dto);
+        
+        if (dto.getStoreId() == null || !StringUtils.hasText(dto.getTableNumbers())) {
+            return R.fail("门店ID和桌号列表不能为空");
+        }
+
+        // 解析桌号列表
+        List<String> tableNumberList = Arrays.stream(dto.getTableNumbers().split(","))
+                .map(String::trim)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (tableNumberList.isEmpty()) {
+            return R.fail("桌号列表不能为空");
+        }
+
+        try {
+            boolean result = storeTableService.batchCreateTables(dto.getStoreId(), tableNumberList);
+            if (result) {
+                return R.success("批量创建桌号成功");
+            } else {
+                return R.fail("批量创建桌号失败");
+            }
+        } catch (Exception e) {
+            log.error("批量创建桌号失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("编辑桌号")
+    @PostMapping("/updateTable")
+    public R<Boolean> updateTable(@RequestBody StoreTableDTO dto) {
+        log.info("StoreTableController.updateTable?dto={}", dto);
+        
+        if (dto.getId() == null || !StringUtils.hasText(dto.getTableNumber())) {
+            return R.fail("桌号ID和桌号名称不能为空");
+        }
+        
+        try {
+            boolean result = storeTableService.updateTable(dto);
+            if (result) {
+                return R.success("更新桌号成功");
+            } else {
+                return R.fail("更新桌号失败");
+            }
+        } catch (Exception e) {
+            log.error("更新桌号失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除桌号")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "桌号ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/deleteTable")
+    public R<Boolean> deleteTable(@RequestParam Integer id) {
+        log.info("StoreTableController.deleteTable?id={}", id);
+        
+        try {
+            boolean result = storeTableService.deleteTable(id);
+            if (result) {
+                return R.success("删除桌号成功");
+            } else {
+                return R.fail("删除桌号失败");
+            }
+        } catch (Exception e) {
+            log.error("删除桌号失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("换桌")
+    @PostMapping("/changeTable")
+    public R<Boolean> changeTable(@RequestBody StoreTableChangeDTO dto) {
+        log.info("StoreTableController.changeTable?dto={}", dto);
+        
+        if (dto.getOrderId() == null || dto.getFromTableId() == null || dto.getToTableId() == null) {
+            return R.fail("订单ID、原桌号ID和目标桌号ID不能为空");
+        }
+        
+        try {
+            boolean result = storeTableService.changeTable(dto);
+            if (result) {
+                return R.success("换桌成功");
+            } else {
+                return R.fail("换桌失败");
+            }
+        } catch (Exception e) {
+            log.error("换桌失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation("批量查询桌号状态")
+    @PostMapping("/batchQueryTableStatus")
+    public R<List<StoreTableStatusVO>> batchQueryTableStatus(@RequestBody BatchQueryTableStatusDTO dto) {
+        log.info("StoreTableController.batchQueryTableStatus?dto={}", dto);
+        
+        if (dto.getTableIds() == null || dto.getTableIds().isEmpty()) {
+            return R.fail("桌号ID列表不能为空");
+        }
+        
+        try {
+            List<StoreTableStatusVO> tables = storeTableService.batchQueryTableStatus(dto.getTableIds());
+            return R.data(tables);
+        } catch (Exception e) {
+            log.error("批量查询桌号状态失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,58 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreCuisineCategory;
+
+import java.util.List;
+
+/**
+ * 菜品分类表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreCuisineCategoryService extends IService<StoreCuisineCategory> {
+
+    /**
+     * 查询菜品分类列表(按排序字段排序)
+     *
+     * @param storeId 门店ID
+     * @return List<StoreCuisineCategory>
+     */
+    List<StoreCuisineCategory> getCategoryList(Integer storeId);
+
+    /**
+     * 批量创建菜品分类
+     *
+     * @param storeId        门店ID
+     * @param categoryNames  分类名称列表
+     * @return boolean
+     */
+    boolean batchCreateCategories(Integer storeId, List<String> categoryNames);
+
+    /**
+     * 更新菜品分类
+     *
+     * @param id           分类ID
+     * @param categoryName 分类名称
+     * @return boolean
+     */
+    boolean updateCategory(Integer id, String categoryName);
+
+    /**
+     * 删除菜品分类
+     *
+     * @param id 分类ID
+     * @return boolean
+     */
+    boolean deleteCategory(Integer id);
+
+    /**
+     * 更新菜品分类排序
+     *
+     * @param storeId     门店ID
+     * @param categoryIds 分类ID列表(按顺序排列)
+     * @return boolean
+     */
+    boolean updateCategorySort(Integer storeId, List<Integer> categoryIds);
+}

+ 8 - 0
alien-store/src/main/java/shop/alien/store/service/StoreCuisineService.java

@@ -42,6 +42,14 @@ public interface StoreCuisineService extends IService<StoreCuisine> {
      * 上下架操作:1-上架,2-下架
      */
     boolean changeShelfStatus(Integer id, Integer shelfStatus);
+
+    /**
+     * 根据分类ID字符串(JSON数组格式)查询分类名称
+     *
+     * @param categoryIds 分类ID字符串(JSON数组格式,如:"[1,2,3]")
+     * @return 分类名称JSON数组字符串(如:"[\"热菜\",\"凉菜\"]"),如果categoryIds为空则返回null
+     */
+    String getCategoryNames(String categoryIds);
 }
 
 

+ 9 - 0
alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java

@@ -213,6 +213,15 @@ public interface StoreInfoService extends IService<StoreInfo> {
     String exportExcelExpirationTime(String id, String expiredState) throws IOException;
 
     /**
+     * 保存或更新餐具费
+     *
+     * @param storeId 门店ID
+     * @param tablewareFee 餐具费(限2位正整数)
+     * @return boolean 是否成功
+     */
+    boolean saveOrUpdateTablewareFee(Integer storeId, Integer tablewareFee);
+
+    /**
      * 八大类用户端筛选
      */
     IPage<StoreInfo> getScreening(ScreeningOfEightMajorCategoriesVO screeningOfEightMajorCategoriesVO);

+ 72 - 0
alien-store/src/main/java/shop/alien/store/service/StoreTableService.java

@@ -0,0 +1,72 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreTableChangeDTO;
+import shop.alien.entity.store.dto.StoreTableDTO;
+import shop.alien.entity.store.vo.StoreTableStatusVO;
+
+import java.util.List;
+
+/**
+ * 桌号表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreTableService extends IService<StoreTable> {
+
+    /**
+     * 分页查询桌号列表
+     *
+     * @param pageNum  页数
+     * @param pageSize 页容
+     * @param storeId  门店ID
+     * @param status   状态(0:空闲, 1:就餐中, 2:已预订)
+     * @return IPage<StoreTable>
+     */
+    IPage<StoreTable> getTablePage(Integer pageNum, Integer pageSize, Integer storeId, Integer status);
+
+    /**
+     * 批量创建桌号
+     *
+     * @param storeId     门店ID
+     * @param tableNumbers 桌号列表
+     * @return boolean
+     */
+    boolean batchCreateTables(Integer storeId, List<String> tableNumbers);
+
+    /**
+     * 更新桌号
+     *
+     * @param dto 桌号管理DTO
+     * @return boolean
+     */
+    boolean updateTable(StoreTableDTO dto);
+
+    /**
+     * 删除桌号
+     *
+     * @param id 桌号ID
+     * @return boolean
+     */
+    boolean deleteTable(Integer id);
+
+    /**
+     * 换桌
+     *
+     * @param dto 换桌DTO
+     * @return boolean
+     */
+    boolean changeTable(StoreTableChangeDTO dto);
+
+    /**
+     * 批量查询桌号状态
+     *
+     * @param tableIds 桌号ID列表
+     * @return List<StoreTableStatusVO>
+     */
+    List<StoreTableStatusVO> batchQueryTableStatus(List<Integer> tableIds);
+
+}

+ 47 - 0
alien-store/src/main/java/shop/alien/store/service/WeChatMiniProgramQrCodeService.java

@@ -0,0 +1,47 @@
+package shop.alien.store.service;
+
+/**
+ * 微信小程序二维码服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface WeChatMiniProgramQrCodeService {
+
+    /**
+     * 获取微信Access Token
+     *
+     * @return access_token
+     */
+    String getAccessToken();
+
+    /**
+     * 生成小程序二维码
+     *
+     * @param scene     场景参数(最大32字符)
+     * @param page      小程序页面路径(可选,为空使用默认配置)
+     * @param width     二维码宽度(默认430,范围280-1280)
+     * @return 二维码图片字节数组
+     */
+    byte[] generateQrCode(String scene, String page, Integer width);
+
+    /**
+     * 生成小程序二维码并上传到OSS
+     *
+     * @param scene     场景参数(最大32字符)
+     * @param page      小程序页面路径(可选,为空使用默认配置)
+     * @param width     二维码宽度(默认430,范围280-1280)
+     * @param ossPath   OSS存储路径
+     * @return 二维码URL
+     */
+    String generateQrCodeAndUpload(String scene, String page, Integer width, String ossPath);
+
+    /**
+     * 为桌号生成小程序二维码
+     *
+     * @param tableId   桌号ID
+     * @param storeId   门店ID
+     * @return 二维码URL
+     */
+    String generateTableQrCode(Integer tableId, Integer storeId);
+}

+ 200 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineCategoryServiceImpl.java

@@ -0,0 +1,200 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.mapper.StoreCuisineCategoryMapper;
+import shop.alien.store.service.StoreCuisineCategoryService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * 菜品分类表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreCuisineCategoryServiceImpl extends ServiceImpl<StoreCuisineCategoryMapper, StoreCuisineCategory> implements StoreCuisineCategoryService {
+
+    @Override
+    public List<StoreCuisineCategory> getCategoryList(Integer storeId) {
+        log.info("StoreCuisineCategoryServiceImpl.getCategoryList?storeId={}", storeId);
+        
+        LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .eq(StoreCuisineCategory::getStatus, 1) // 只查询启用的分类
+                .orderByAsc(StoreCuisineCategory::getSort) // 按排序字段升序
+                .orderByDesc(StoreCuisineCategory::getCreatedTime); // 如果排序字段相同,按创建时间倒序
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public boolean batchCreateCategories(Integer storeId, List<String> categoryNames) {
+        log.info("StoreCuisineCategoryServiceImpl.batchCreateCategories?storeId={}&categoryNames={}", storeId, categoryNames);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        if (storeId == null || categoryNames == null || categoryNames.isEmpty()) {
+            log.warn("批量创建菜品分类失败:参数不完整");
+            return false;
+        }
+
+        // 检查分类名称是否已存在
+        LambdaQueryWrapper<StoreCuisineCategory> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .in(StoreCuisineCategory::getCategoryName, categoryNames);
+        List<StoreCuisineCategory> existingCategories = this.list(checkWrapper);
+        
+        if (!existingCategories.isEmpty()) {
+            List<String> existingNames = existingCategories.stream()
+                    .map(StoreCuisineCategory::getCategoryName)
+                    .collect(Collectors.toList());
+            log.warn("批量创建菜品分类失败:分类名称已存在,{}", existingNames);
+            throw new RuntimeException("分类名称已存在:" + String.join(",", existingNames));
+        }
+
+        // 查询当前最大的排序值
+        LambdaQueryWrapper<StoreCuisineCategory> maxSortWrapper = new LambdaQueryWrapper<>();
+        maxSortWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .orderByDesc(StoreCuisineCategory::getSort)
+                .last("LIMIT 1");
+        StoreCuisineCategory maxSortCategory = this.getOne(maxSortWrapper);
+        int maxSort = (maxSortCategory != null && maxSortCategory.getSort() != null) ? maxSortCategory.getSort() : 0;
+
+        // 使用 AtomicInteger 解决 lambda 表达式中的变量问题
+        AtomicInteger sortCounter = new AtomicInteger(maxSort);
+
+        // 批量创建
+        List<StoreCuisineCategory> categories = categoryNames.stream()
+                .map(categoryName -> {
+                    StoreCuisineCategory category = new StoreCuisineCategory();
+                    category.setStoreId(storeId);
+                    category.setCategoryName(categoryName);
+                    category.setStatus(1); // 默认启用
+                    category.setSort(sortCounter.incrementAndGet()); // 设置排序值
+                    category.setCreatedUserId(userId);
+                    return category;
+                })
+                .collect(Collectors.toList());
+
+        return this.saveBatch(categories);
+    }
+
+    @Override
+    public boolean updateCategory(Integer id, String categoryName) {
+        log.info("StoreCuisineCategoryServiceImpl.updateCategory?id={}&categoryName={}", id, categoryName);
+        
+        StoreCuisineCategory category = this.getById(id);
+        if (category == null) {
+            log.warn("更新菜品分类失败:分类不存在,id={}", id);
+            return false;
+        }
+
+        // 如果修改了分类名称,检查新分类名称是否已存在
+        if (!categoryName.equals(category.getCategoryName())) {
+            LambdaQueryWrapper<StoreCuisineCategory> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreCuisineCategory::getStoreId, category.getStoreId())
+                    .eq(StoreCuisineCategory::getCategoryName, categoryName)
+                    .ne(StoreCuisineCategory::getId, id);
+            StoreCuisineCategory existingCategory = this.getOne(wrapper);
+            if (existingCategory != null) {
+                log.warn("更新菜品分类失败:分类名称已存在,categoryName={}", categoryName);
+                throw new RuntimeException("分类名称已存在:" + categoryName);
+            }
+        }
+
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+
+        LambdaUpdateWrapper<StoreCuisineCategory> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreCuisineCategory::getId, id)
+                .set(StoreCuisineCategory::getCategoryName, categoryName);
+        if (userId != null) {
+            updateWrapper.set(StoreCuisineCategory::getUpdatedUserId, userId);
+        }
+
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean deleteCategory(Integer id) {
+        log.info("StoreCuisineCategoryServiceImpl.deleteCategory?id={}", id);
+        
+        StoreCuisineCategory category = this.getById(id);
+        if (category == null) {
+            log.warn("删除菜品分类失败:分类不存在,id={}", id);
+            return false;
+        }
+
+        // 逻辑删除
+        return this.removeById(id);
+    }
+
+    @Override
+    public boolean updateCategorySort(Integer storeId, List<Integer> categoryIds) {
+        log.info("StoreCuisineCategoryServiceImpl.updateCategorySort?storeId={}&categoryIds={}", storeId, categoryIds);
+        
+        if (storeId == null || categoryIds == null || categoryIds.isEmpty()) {
+            log.warn("更新菜品分类排序失败:参数不完整");
+            return false;
+        }
+
+        // 验证所有分类ID是否属于该门店
+        LambdaQueryWrapper<StoreCuisineCategory> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreCuisineCategory::getStoreId, storeId)
+                .in(StoreCuisineCategory::getId, categoryIds);
+        long count = this.count(checkWrapper);
+        if (count != categoryIds.size()) {
+            log.warn("更新菜品分类排序失败:部分分类ID不属于该门店");
+            throw new RuntimeException("部分分类ID不属于该门店");
+        }
+
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+
+        // 批量更新排序值
+        for (int i = 0; i < categoryIds.size(); i++) {
+            LambdaUpdateWrapper<StoreCuisineCategory> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreCuisineCategory::getId, categoryIds.get(i))
+                    .set(StoreCuisineCategory::getSort, i + 1);
+            if (userId != null) {
+                updateWrapper.set(StoreCuisineCategory::getUpdatedUserId, userId);
+            }
+            this.update(updateWrapper);
+        }
+
+        return true;
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 108 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineServiceImpl.java

@@ -16,9 +16,11 @@ import shop.alien.entity.store.dto.CategoryGroupDto;
 import shop.alien.entity.store.dto.CuisineComboDto;
 import shop.alien.entity.store.dto.CuisineItemDto;
 import shop.alien.entity.store.dto.CuisineTypeResponseDto;
+import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.mapper.StoreCuisineMapper;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.store.service.StoreCuisineComboService;
+import shop.alien.store.service.StoreCuisineCategoryService;
 import shop.alien.store.service.StoreCuisineService;
 
 import java.io.IOException;
@@ -44,6 +46,8 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
 
     private final StoreInfoMapper storeInfoMapper;
 
+    private final StoreCuisineCategoryService storeCuisineCategoryService;
+
     /**
      * 新增美食(单品或套餐)
      */
@@ -59,6 +63,8 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
         // 1. 保存主表信息到 store_cuisine
         StoreCuisine cuisine = new StoreCuisine();
         BeanUtils.copyProperties(cuisineComboDto, cuisine);
+        // 处理分类ID:将分类ID数组转换为JSON字符串格式存储
+        cuisine.setCategoryIds(formatCategoryIds(cuisineComboDto.getCategoryIds()));
         save(cuisine);
 
         // 单品:只写主表,不写中间表
@@ -90,6 +96,8 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
         StoreCuisine cuisine = new StoreCuisine();
         BeanUtils.copyProperties(cuisineComboDto, cuisine);
         cuisine.setId(comboId);
+        // 处理分类ID:将分类ID数组转换为JSON字符串格式存储
+        cuisine.setCategoryIds(formatCategoryIds(cuisineComboDto.getCategoryIds()));
         updateById(cuisine);
 
         // 单品:只更新主表即可
@@ -152,9 +160,16 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
         data.setStoreId(base.getStoreId());
         data.setName(base.getName());
         data.setTotalPrice(base.getTotalPrice());
+        data.setCategoryIds(base.getCategoryIds());
+        // 根据分类ID查询分类名称
+        data.setCategoryNames(getCategoryNames(base.getCategoryIds()));
+        data.setIsHomepageDisplay(base.getIsHomepageDisplay());
+        data.setTags(base.getTags());
+        data.setDishReview(base.getDishReview());
         data.setImages(base.getImages());
         data.setImageContent(base.getImageContent());
         data.setDetailContent(base.getDetailContent());
+        data.setDescription(base.getDescription());
         data.setExtraNote(base.getExtraNote());
         data.setNeedReserve(base.getNeedReserve());
         data.setReserveRule(base.getReserveRule());
@@ -280,6 +295,99 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
         }
         return comboList;
     }
+
+    /**
+     * 格式化分类ID为JSON数组字符串
+     * 如果传入的是有效的JSON数组字符串,直接返回;如果是空或null,返回null
+     *
+     * @param categoryIds 分类ID字符串(JSON数组格式,如:"[1,2,3]")
+     * @return 格式化后的JSON数组字符串,如果为空则返回null
+     */
+    private String formatCategoryIds(String categoryIds) {
+        if (StringUtils.isBlank(categoryIds)) {
+            return null;
+        }
+        
+        // 去除首尾空格
+        String trimmed = categoryIds.trim();
+        
+        // 如果已经是有效的JSON数组格式(以[开头,]结尾),直接返回
+        if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
+            try {
+                // 验证是否为有效的JSON数组
+                objectMapper.readValue(trimmed, new TypeReference<List<Integer>>() {});
+                return trimmed;
+            } catch (IOException e) {
+                throw new IllegalArgumentException("分类ID格式不正确,应为JSON数组格式,如:[1,2,3]", e);
+            }
+        }
+        
+        // 如果不是JSON格式,尝试解析为逗号分隔的字符串并转换为JSON数组
+        try {
+            String[] ids = trimmed.split(",");
+            List<Integer> idList = new ArrayList<>();
+            for (String id : ids) {
+                String idStr = id.trim();
+                if (StringUtils.isNotBlank(idStr)) {
+                    idList.add(Integer.parseInt(idStr));
+                }
+            }
+            if (idList.isEmpty()) {
+                return null;
+            }
+            return objectMapper.writeValueAsString(idList);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("分类ID格式不正确,应为数字数组", e);
+        } catch (IOException e) {
+            throw new RuntimeException("转换分类ID为JSON格式失败", e);
+        }
+    }
+
+    /**
+     * 根据分类ID字符串(JSON数组格式)查询分类名称
+     *
+     * @param categoryIds 分类ID字符串(JSON数组格式,如:"[1,2,3]")
+     * @return 分类名称JSON数组字符串(如:"[\"热菜\",\"凉菜\"]"),如果categoryIds为空则返回null
+     */
+    @Override
+    public String getCategoryNames(String categoryIds) {
+        if (StringUtils.isBlank(categoryIds)) {
+            return null;
+        }
+
+        try {
+            // 解析分类ID数组
+            List<Integer> idList = objectMapper.readValue(categoryIds, new TypeReference<List<Integer>>() {});
+            if (idList == null || idList.isEmpty()) {
+                return null;
+            }
+
+            // 批量查询分类信息
+            List<StoreCuisineCategory> categories = new ArrayList<>(storeCuisineCategoryService.listByIds(idList));
+            if (categories == null || categories.isEmpty()) {
+                return "[]";
+            }
+
+            // 构建ID到名称的映射
+            Map<Integer, String> idToNameMap = categories.stream()
+                    .collect(Collectors.toMap(StoreCuisineCategory::getId, StoreCuisineCategory::getCategoryName));
+
+            // 按照原始ID顺序构建名称列表
+            List<String> nameList = new ArrayList<>();
+            for (Integer id : idList) {
+                String name = idToNameMap.get(id);
+                if (name != null) {
+                    nameList.add(name);
+                }
+            }
+
+            // 转换为JSON数组字符串
+            return objectMapper.writeValueAsString(nameList);
+        } catch (IOException e) {
+            // 解析分类ID或构建分类名称失败时,返回null,不抛出异常
+            return null;
+        }
+    }
 }
 
 

+ 18 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -2782,6 +2782,24 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     }
 
     @Override
+    public boolean saveOrUpdateTablewareFee(Integer storeId, Integer tablewareFee) {
+        if (storeId == null) {
+            throw new IllegalArgumentException("门店ID不能为空");
+        }
+        
+        // 验证餐具费:限2位正整数(0-99)
+        if (tablewareFee != null && (tablewareFee < 0 || tablewareFee > 99)) {
+            throw new IllegalArgumentException("餐具费必须是0-99之间的整数");
+        }
+        
+        LambdaUpdateWrapper<StoreInfo> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreInfo::getId, storeId);
+        wrapper.set(StoreInfo::getTablewareFee, tablewareFee);
+        int result = storeInfoMapper.update(null, wrapper);
+        return result > 0;
+    }
+
+    @Override
     public int saveOrUpdateStoreInfo(StoreInfoDto storeInfodto) {
         if (storeInfodto.getId() != null) {
             // 校验当前店铺存在未完成的订单及正在销售的商品

+ 0 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java

@@ -14,8 +14,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreStaffConfigListQueryDto;
-import shop.alien.entity.store.dto.StoreStaffFitnessCourseGroup;
-import shop.alien.entity.store.dto.StoreStaffFitnessCourseItem;
 import shop.alien.entity.store.excelVo.StoreStaffConfigExcelVo;
 import shop.alien.entity.store.excelVo.util.ExcelGenerator;
 import shop.alien.entity.result.R;
@@ -24,7 +22,6 @@ import shop.alien.entity.store.StoreStaffFitnessCertification;
 import shop.alien.entity.store.StoreStaffFitnessCourse;
 import shop.alien.entity.store.StoreStaffFitnessExperience;
 import shop.alien.entity.store.vo.PerformanceScheduleVo;
-import shop.alien.entity.store.StoreComment;
 import shop.alien.entity.store.StoreStaffReview;
 import shop.alien.entity.store.StoreStaffTitle;
 import shop.alien.entity.store.LifeLikeRecord;
@@ -41,8 +38,6 @@ import shop.alien.store.service.StoreStaffFitnessCertificationService;
 import shop.alien.store.service.StoreStaffFitnessCourseService;
 import shop.alien.store.service.StoreStaffFitnessExperienceService;
 import shop.alien.store.util.CommonConstant;
-import shop.alien.store.util.ai.AiContentModerationUtil;
-import shop.alien.store.util.ai.AiVideoModerationUtil;
 import shop.alien.util.ali.AliOSSUtil;
 
 import java.io.File;
@@ -50,7 +45,6 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -80,16 +74,10 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
 
     private final StoreStaffFitnessCertificationService storeStaffFitnessCertificationService;
 
-    private final AiContentModerationUtil aiContentModerationUtil;
-
-    private final AiVideoModerationUtil aiVideoModerationUtil;
-
     private final BarPerformanceMapper barPerformanceMapper;
 
     private final StoreStaffTitleMapper storeStaffTitleMapper;
 
-    private final StoreCommentMapper storeCommentMapper;
-
     private final StoreStaffReviewMapper storeStaffReviewMapper;
 
     private final StoreStaffFitnessExperienceService storeStaffFitnessExperienceService;

+ 370 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreTableServiceImpl.java

@@ -0,0 +1,370 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.StoreTableLog;
+import shop.alien.entity.store.dto.StoreTableChangeDTO;
+import shop.alien.entity.store.dto.StoreTableDTO;
+import shop.alien.entity.store.vo.StoreTableStatusVO;
+import shop.alien.mapper.StoreTableLogMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.StoreTableService;
+import shop.alien.store.service.WeChatMiniProgramQrCodeService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 桌号表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreTableServiceImpl extends ServiceImpl<StoreTableMapper, StoreTable> implements StoreTableService {
+
+    private final StoreTableLogMapper storeTableLogMapper;
+    private final WeChatMiniProgramQrCodeService weChatMiniProgramQrCodeService;
+    private final BaseRedisService baseRedisService;
+
+    @Override
+    public IPage<StoreTable> getTablePage(Integer pageNum, Integer pageSize, Integer storeId, Integer status) {
+        log.info("StoreTableServiceImpl.getTablePage?pageNum={}&pageSize={}&storeId={}&status={}", pageNum, pageSize, storeId, status);
+        LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreTable::getStoreId, storeId)
+                .eq(status != null, StoreTable::getStatus, status)
+                .orderByAsc(StoreTable::getTableNumber);
+        return this.page(new Page<>(pageNum, pageSize), wrapper);
+    }
+
+    @Override
+    public boolean batchCreateTables(Integer storeId, List<String> tableNumbers) {
+        log.info("StoreTableServiceImpl.batchCreateTables?storeId={}&tableNumbers={}", storeId, tableNumbers);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        if (storeId == null || tableNumbers == null || tableNumbers.isEmpty()) {
+            log.warn("批量创建桌号失败:参数不完整");
+            return false;
+        }
+
+        // 检查桌号是否已存在
+        LambdaQueryWrapper<StoreTable> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreTable::getStoreId, storeId)
+                .in(StoreTable::getTableNumber, tableNumbers);
+        List<StoreTable> existingTables = this.list(checkWrapper);
+        
+        if (!existingTables.isEmpty()) {
+            List<String> existingNumbers = existingTables.stream()
+                    .map(StoreTable::getTableNumber)
+                    .collect(Collectors.toList());
+            log.warn("批量创建桌号失败:桌号已存在,{}", existingNumbers);
+            throw new RuntimeException("桌号已存在:" + String.join(",", existingNumbers));
+        }
+
+        // 批量创建
+        List<StoreTable> tables = tableNumbers.stream()
+                .map(tableNumber -> {
+                    StoreTable table = new StoreTable();
+                    table.setStoreId(storeId);
+                    table.setTableNumber(tableNumber);
+                    table.setStatus(0); // 默认空闲
+                    table.setCreatedUserId(userId);
+                    return table;
+                })
+                .collect(Collectors.toList());
+
+        boolean result = this.saveBatch(tables);
+        
+        // 批量创建成功后,异步生成小程序二维码
+        if (result) {
+            asyncGenerateQrCodesForTables(storeId, tableNumbers);
+        }
+        
+        return result;
+    }
+
+    /**
+     * 异步为新创建的桌号生成二维码
+     */
+    @Async
+    public void asyncGenerateQrCodesForTables(Integer storeId, List<String> tableNumbers) {
+        log.info("开始异步生成桌号二维码, storeId={}, tableNumbers={}", storeId, tableNumbers);
+        try {
+            // 查询刚创建的桌号
+            LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreTable::getStoreId, storeId)
+                    .in(StoreTable::getTableNumber, tableNumbers)
+                    .isNull(StoreTable::getQrcodeUrl);
+            List<StoreTable> tables = this.list(wrapper);
+            
+            for (StoreTable table : tables) {
+                try {
+                    String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(table.getId(), storeId);
+                    if (qrCodeUrl != null && !qrCodeUrl.isEmpty()) {
+                        // 更新二维码URL
+                        LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+                        updateWrapper.eq(StoreTable::getId, table.getId())
+                                .set(StoreTable::getQrcodeUrl, qrCodeUrl);
+                        this.update(updateWrapper);
+                        log.info("桌号二维码生成成功, tableId={}, qrCodeUrl={}", table.getId(), qrCodeUrl);
+                    }
+                } catch (Exception e) {
+                    log.error("生成桌号二维码失败, tableId={}, error={}", table.getId(), e.getMessage(), e);
+                }
+            }
+            log.info("异步生成桌号二维码完成, storeId={}, count={}", storeId, tables.size());
+        } catch (Exception e) {
+            log.error("异步生成桌号二维码异常, storeId={}, error={}", storeId, e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean updateTable(StoreTableDTO dto) {
+        log.info("StoreTableServiceImpl.updateTable?dto={}", dto);
+        
+        StoreTable table = this.getById(dto.getId());
+        if (table == null) {
+            log.warn("更新桌号失败:桌号不存在,id={}", dto.getId());
+            return false;
+        }
+
+        // 如果修改了桌号,检查新桌号是否已存在
+        if (!dto.getTableNumber().equals(table.getTableNumber())) {
+            LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreTable::getStoreId, table.getStoreId())
+                    .eq(StoreTable::getTableNumber, dto.getTableNumber())
+                    .ne(StoreTable::getId, dto.getId());
+            StoreTable existingTable = this.getOne(wrapper);
+            if (existingTable != null) {
+                log.warn("更新桌号失败:桌号已存在,tableNumber={}", dto.getTableNumber());
+                throw new RuntimeException("桌号已存在:" + dto.getTableNumber());
+            }
+        }
+
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+
+        LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreTable::getId, dto.getId())
+                .set(StoreTable::getTableNumber, dto.getTableNumber());
+        if (userId != null) {
+            updateWrapper.set(StoreTable::getUpdatedUserId, userId);
+        }
+
+        boolean result = this.update(updateWrapper);
+        
+        // 更新成功后,异步更新小程序二维码
+        if (result) {
+            asyncUpdateQrCodeForTable(dto.getId(), table.getStoreId());
+        }
+        
+        return result;
+    }
+
+    /**
+     * 异步更新桌号的小程序二维码
+     */
+    @Async
+    public void asyncUpdateQrCodeForTable(Integer tableId, Integer storeId) {
+        log.info("开始异步更新桌号二维码, tableId={}, storeId={}", tableId, storeId);
+        try {
+            String qrCodeUrl = weChatMiniProgramQrCodeService.generateTableQrCode(tableId, storeId);
+            if (qrCodeUrl != null && !qrCodeUrl.isEmpty()) {
+                // 更新二维码URL
+                LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.eq(StoreTable::getId, tableId)
+                        .set(StoreTable::getQrcodeUrl, qrCodeUrl);
+                this.update(updateWrapper);
+                log.info("桌号二维码更新成功, tableId={}, qrCodeUrl={}", tableId, qrCodeUrl);
+            }
+        } catch (Exception e) {
+            log.error("更新桌号二维码失败, tableId={}, error={}", tableId, e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean deleteTable(Integer id) {
+        log.info("StoreTableServiceImpl.deleteTable?id={}", id);
+        
+        StoreTable table = this.getById(id);
+        if (table == null) {
+            log.warn("删除桌号失败:桌号不存在,id={}", id);
+            return false;
+        }
+
+        // 如果桌号正在使用中,不允许删除
+        if (table.getStatus() != null && table.getStatus() == 1) {
+            log.warn("删除桌号失败:桌号正在使用中,id={}", id);
+            throw new RuntimeException("桌号正在使用中,无法删除");
+        }
+
+        return this.removeById(id);
+    }
+
+    @Override
+    public boolean changeTable(StoreTableChangeDTO dto) {
+        log.info("StoreTableServiceImpl.changeTable?dto={}", dto);
+        
+        // 查询原桌号
+        StoreTable fromTable = this.getById(dto.getFromTableId());
+        if (fromTable == null) {
+            log.warn("换桌失败:原桌号不存在,fromTableId={}", dto.getFromTableId());
+            throw new RuntimeException("原桌号不存在");
+        }
+
+        // 检查原桌号的订单ID是否匹配
+        if (fromTable.getCurrentOrderId() == null || !fromTable.getCurrentOrderId().equals(dto.getOrderId())) {
+            log.warn("换桌失败:原桌号的订单ID不匹配,fromTableId={}, currentOrderId={}, orderId={}", 
+                    dto.getFromTableId(), fromTable.getCurrentOrderId(), dto.getOrderId());
+            throw new RuntimeException("原桌号的订单ID不匹配");
+        }
+
+        // 检查门店ID是否一致(先检查,避免后续不必要的操作)
+        if (dto.getFromTableId().equals(dto.getToTableId())) {
+            log.warn("换桌失败:原桌号和目标桌号不能相同");
+            throw new RuntimeException("原桌号和目标桌号不能相同");
+        }
+
+        // 使用分布式锁锁定目标桌号,防止并发冲突
+        String lockKey = "table:change:" + dto.getToTableId();
+        String lockIdentifier = baseRedisService.lock(lockKey, 10000, 5000); // 锁10秒,获取锁超时5秒
+        
+        if (lockIdentifier == null) {
+            log.warn("换桌失败:获取目标桌号锁失败,可能正在被其他操作占用,toTableId={}", dto.getToTableId());
+            throw new RuntimeException("目标桌号正在被占用,请稍后重试");
+        }
+
+        try {
+            // 重新查询目标桌号(在锁内查询,确保获取最新状态)
+            StoreTable toTable = this.getById(dto.getToTableId());
+            if (toTable == null) {
+                log.warn("换桌失败:目标桌号不存在,toTableId={}", dto.getToTableId());
+                throw new RuntimeException("目标桌号不存在");
+            }
+
+            // 检查门店ID是否一致
+            if (!fromTable.getStoreId().equals(toTable.getStoreId())) {
+                log.warn("换桌失败:桌号不在同一门店,fromTableId={}, toTableId={}", dto.getFromTableId(), dto.getToTableId());
+                throw new RuntimeException("桌号不在同一门店");
+            }
+
+            // 从JWT获取当前登录用户ID
+            Integer userId = getCurrentUserId();
+
+            // 更新原桌号:状态为空闲,清空订单ID
+            LambdaUpdateWrapper<StoreTable> fromUpdateWrapper = new LambdaUpdateWrapper<>();
+            fromUpdateWrapper.eq(StoreTable::getId, dto.getFromTableId())
+                    .eq(StoreTable::getCurrentOrderId, dto.getOrderId()) // 确保订单ID匹配
+                    .set(StoreTable::getStatus, 0)
+                    .set(StoreTable::getCurrentOrderId, null);
+            boolean fromUpdateResult = this.update(fromUpdateWrapper);
+            
+            if (!fromUpdateResult) {
+                log.warn("换桌失败:更新原桌号失败,可能订单状态已变化,fromTableId={}, orderId={}", 
+                        dto.getFromTableId(), dto.getOrderId());
+                throw new RuntimeException("原桌号的订单状态已变化,请刷新后重试");
+            }
+
+            // 使用条件更新目标桌号:只有在空闲状态且无订单时才能更新
+            // 这样可以确保即使有其他用户扫码,也能保证原子性
+            LambdaUpdateWrapper<StoreTable> toUpdateWrapper = new LambdaUpdateWrapper<>();
+            toUpdateWrapper.eq(StoreTable::getId, dto.getToTableId())
+                    .eq(StoreTable::getStatus, 0) // 必须是空闲状态
+                    .isNull(StoreTable::getCurrentOrderId) // 必须没有订单
+                    .set(StoreTable::getStatus, 1)
+                    .set(StoreTable::getCurrentOrderId, dto.getOrderId());
+            
+            boolean toUpdateResult = this.update(toUpdateWrapper);
+            
+            if (!toUpdateResult) {
+                log.warn("换桌失败:目标桌号已被占用(可能被其他用户扫码),toTableId={}", dto.getToTableId());
+                // 回滚原桌号的更新
+                LambdaUpdateWrapper<StoreTable> rollbackWrapper = new LambdaUpdateWrapper<>();
+                rollbackWrapper.eq(StoreTable::getId, dto.getFromTableId())
+                        .set(StoreTable::getStatus, 1)
+                        .set(StoreTable::getCurrentOrderId, dto.getOrderId());
+                this.update(rollbackWrapper);
+                throw new RuntimeException("目标桌号已被占用,请选择其他桌号");
+            }
+
+            // 创建换桌记录(只有在成功更新后才记录)
+            StoreTableLog tableLog = new StoreTableLog();
+            tableLog.setStoreId(fromTable.getStoreId());
+            tableLog.setOrderId(dto.getOrderId());
+            tableLog.setFromTableId(dto.getFromTableId());
+            tableLog.setFromTableNumber(fromTable.getTableNumber());
+            tableLog.setToTableId(dto.getToTableId());
+            tableLog.setToTableNumber(toTable.getTableNumber());
+            tableLog.setChangeReason(dto.getChangeReason());
+            tableLog.setCreatedUserId(userId);
+            storeTableLogMapper.insert(tableLog);
+
+            log.info("换桌成功:订单{}从桌号{}换到桌号{}", dto.getOrderId(), dto.getFromTableId(), dto.getToTableId());
+            return true;
+            
+        } finally {
+            // 释放分布式锁
+            baseRedisService.unlock(lockKey, lockIdentifier);
+        }
+    }
+
+    @Override
+    public List<StoreTableStatusVO> batchQueryTableStatus(List<Integer> tableIds) {
+        log.info("StoreTableServiceImpl.batchQueryTableStatus?tableIds={}", tableIds);
+        
+        if (tableIds == null || tableIds.isEmpty()) {
+            log.warn("批量查询桌号状态失败:桌号ID列表为空");
+            return Collections.emptyList();
+        }
+        
+        LambdaQueryWrapper<StoreTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.in(StoreTable::getId, tableIds)
+                .select(StoreTable::getId, StoreTable::getStatus);
+        List<StoreTable> tables = this.list(wrapper);
+        
+        // 转换为VO
+        return tables.stream()
+                .map(table -> {
+                    StoreTableStatusVO vo = new StoreTableStatusVO();
+                    vo.setId(table.getId());
+                    vo.setStatus(table.getStatus());
+                    return vo;
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 300 - 0
alien-store/src/main/java/shop/alien/store/service/impl/WeChatMiniProgramQrCodeServiceImpl.java

@@ -0,0 +1,300 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.stereotype.Service;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.config.WeChatMiniProgramConfig;
+import shop.alien.store.service.WeChatMiniProgramQrCodeService;
+import shop.alien.util.ali.AliOSSUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 微信小程序二维码服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WeChatMiniProgramQrCodeServiceImpl implements WeChatMiniProgramQrCodeService {
+
+    private final WeChatMiniProgramConfig config;
+    private final BaseRedisService redisService;
+    private final AliOSSUtil aliOSSUtil;
+
+    /**
+     * Redis缓存Key前缀
+     */
+    private static final String ACCESS_TOKEN_CACHE_KEY = "wechat:miniprogram:access_token";
+
+    /**
+     * Access Token缓存时间(秒),比微信的7200秒稍短
+     */
+    private static final long ACCESS_TOKEN_CACHE_SECONDS = 7000L;
+
+    /**
+     * 微信获取Access Token接口
+     */
+    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+
+    /**
+     * 微信生成小程序码接口(无数量限制)
+     */
+    private static final String QR_CODE_UNLIMITED_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
+
+    private final OkHttpClient httpClient = new OkHttpClient.Builder()
+            .connectTimeout(30, TimeUnit.SECONDS)
+            .readTimeout(30, TimeUnit.SECONDS)
+            .writeTimeout(30, TimeUnit.SECONDS)
+            .build();
+
+    @Override
+    public String getAccessToken() {
+        // 先从Redis缓存获取
+        String cachedToken = redisService.getString(ACCESS_TOKEN_CACHE_KEY);
+        if (cachedToken != null && !cachedToken.isEmpty()) {
+            log.debug("从Redis缓存获取Access Token成功");
+            return cachedToken;
+        }
+
+        // 缓存不存在,调用微信API获取
+        String url = String.format(ACCESS_TOKEN_URL, config.getAppId(), config.getAppSecret());
+        
+        Request request = new Request.Builder()
+                .url(url)
+                .get()
+                .build();
+
+        try (Response response = httpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                log.error("获取Access Token失败,HTTP状态码: {}", response.code());
+                throw new RuntimeException("获取Access Token失败,HTTP状态码: " + response.code());
+            }
+
+            String responseBody = response.body() != null ? response.body().string() : "";
+            JSONObject jsonObject = JSON.parseObject(responseBody);
+
+            if (jsonObject.containsKey("errcode") && jsonObject.getIntValue("errcode") != 0) {
+                log.error("获取Access Token失败,错误码: {}, 错误信息: {}", 
+                        jsonObject.getIntValue("errcode"), jsonObject.getString("errmsg"));
+                throw new RuntimeException("获取Access Token失败: " + jsonObject.getString("errmsg"));
+            }
+
+            String accessToken = jsonObject.getString("access_token");
+            if (accessToken == null || accessToken.isEmpty()) {
+                log.error("获取Access Token失败,返回数据异常: {}", responseBody);
+                throw new RuntimeException("获取Access Token失败,返回数据异常");
+            }
+
+            // 缓存到Redis
+            redisService.setString(ACCESS_TOKEN_CACHE_KEY, accessToken, ACCESS_TOKEN_CACHE_SECONDS);
+            log.info("获取Access Token成功并缓存到Redis");
+
+            return accessToken;
+        } catch (IOException e) {
+            log.error("获取Access Token网络请求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("获取Access Token网络请求失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public byte[] generateQrCode(String scene, String page, Integer width) {
+        log.info("开始生成小程序二维码, scene={}, page={}, width={}", scene, page, width);
+
+        // 参数校验
+        if (scene == null || scene.isEmpty()) {
+            throw new IllegalArgumentException("scene参数不能为空");
+        }
+        if (scene.length() > 32) {
+            log.warn("scene参数超过32字符,将被截断: {}", scene);
+            scene = scene.substring(0, 32);
+        }
+
+        // 获取Access Token
+        String accessToken = getAccessToken();
+        String url = String.format(QR_CODE_UNLIMITED_URL, accessToken);
+
+        // 构建请求参数
+        Map<String, Object> params = new HashMap<>();
+        params.put("scene", scene);
+        params.put("env_version", config.getEnvVersion()); // develop-开发版
+        
+        // 页面路径
+        String pagePath = (page != null && !page.isEmpty()) ? page : config.getPagePath();
+        if (pagePath != null && !pagePath.isEmpty()) {
+            params.put("page", pagePath);
+        }
+        
+        // 二维码宽度
+        if (width != null && width >= 280 && width <= 1280) {
+            params.put("width", width);
+        } else {
+            params.put("width", 430); // 默认宽度
+        }
+
+        // 自动配置线条颜色
+        params.put("auto_color", true);
+        // 是否需要透明底色
+        params.put("is_hyaline", false);
+        // 开发版或体验版时,跳过页面路径校验
+        if ("develop".equals(config.getEnvVersion()) || "trial".equals(config.getEnvVersion())) {
+            params.put("check_path", false);
+        }
+
+        String jsonBody = JSON.toJSONString(params);
+        log.info("请求微信小程序码接口, URL={}, params={}", url, jsonBody);
+
+        RequestBody requestBody = RequestBody.create(
+                MediaType.parse("application/json; charset=utf-8"), jsonBody);
+
+        Request request = new Request.Builder()
+                .url(url)
+                .post(requestBody)
+                .build();
+
+        try (Response response = httpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                log.error("生成小程序二维码失败,HTTP状态码: {}", response.code());
+                throw new RuntimeException("生成小程序二维码失败,HTTP状态码: " + response.code());
+            }
+
+            // 获取Content-Type
+            String contentType = response.header("Content-Type");
+            byte[] bodyBytes = response.body() != null ? response.body().bytes() : new byte[0];
+
+            // 判断返回类型
+            if (contentType != null && contentType.contains("application/json")) {
+                // 返回JSON表示出错了
+                String errorJson = new String(bodyBytes);
+                JSONObject jsonObject = JSON.parseObject(errorJson);
+                log.error("生成小程序二维码失败,错误码: {}, 错误信息: {}", 
+                        jsonObject.getIntValue("errcode"), jsonObject.getString("errmsg"));
+                
+                // 如果是access_token过期,清除缓存并重试一次
+                if (jsonObject.getIntValue("errcode") == 40001 || 
+                    jsonObject.getIntValue("errcode") == 42001) {
+                    log.info("Access Token已过期,清除缓存并重试");
+                    redisService.delete(ACCESS_TOKEN_CACHE_KEY);
+                    return retryGenerateQrCode(scene, pagePath, width);
+                }
+                
+                throw new RuntimeException("生成小程序二维码失败: " + jsonObject.getString("errmsg"));
+            }
+
+            // 返回图片数据
+            if (bodyBytes.length == 0) {
+                throw new RuntimeException("生成小程序二维码失败: 返回数据为空");
+            }
+
+            log.info("生成小程序二维码成功, 图片大小: {} bytes", bodyBytes.length);
+            return bodyBytes;
+        } catch (IOException e) {
+            log.error("生成小程序二维码网络请求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("生成小程序二维码网络请求失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 重试生成二维码(用于access_token过期的情况)
+     */
+    private byte[] retryGenerateQrCode(String scene, String page, Integer width) {
+        String accessToken = getAccessToken();
+        String url = String.format(QR_CODE_UNLIMITED_URL, accessToken);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("scene", scene);
+        params.put("env_version", config.getEnvVersion());
+        if (page != null && !page.isEmpty()) {
+            params.put("page", page);
+        }
+        params.put("width", width != null ? width : 430);
+        params.put("auto_color", true);
+        params.put("is_hyaline", false);
+        // 开发版或体验版时,跳过页面路径校验
+        if ("develop".equals(config.getEnvVersion()) || "trial".equals(config.getEnvVersion())) {
+            params.put("check_path", false);
+        }
+
+        RequestBody requestBody = RequestBody.create(
+                MediaType.parse("application/json; charset=utf-8"), JSON.toJSONString(params));
+
+        Request request = new Request.Builder()
+                .url(url)
+                .post(requestBody)
+                .build();
+
+        try (Response response = httpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new RuntimeException("重试生成小程序二维码失败,HTTP状态码: " + response.code());
+            }
+
+            String contentType = response.header("Content-Type");
+            byte[] bodyBytes = response.body() != null ? response.body().bytes() : new byte[0];
+
+            if (contentType != null && contentType.contains("application/json")) {
+                String errorJson = new String(bodyBytes);
+                JSONObject jsonObject = JSON.parseObject(errorJson);
+                throw new RuntimeException("重试生成小程序二维码失败: " + jsonObject.getString("errmsg"));
+            }
+
+            return bodyBytes;
+        } catch (IOException e) {
+            throw new RuntimeException("重试生成小程序二维码网络请求失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public String generateQrCodeAndUpload(String scene, String page, Integer width, String ossPath) {
+        log.info("生成小程序二维码并上传到OSS, scene={}, page={}, width={}, ossPath={}", 
+                scene, page, width, ossPath);
+
+        // 生成二维码
+        byte[] qrCodeBytes = generateQrCode(scene, page, width);
+
+        // 上传到OSS
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes);
+        String qrCodeUrl = aliOSSUtil.uploadFile(inputStream, ossPath);
+
+        if (qrCodeUrl == null || qrCodeUrl.isEmpty()) {
+            log.error("上传二维码到OSS失败, ossPath={}", ossPath);
+            throw new RuntimeException("上传二维码到OSS失败");
+        }
+
+        log.info("上传二维码到OSS成功, qrCodeUrl={}", qrCodeUrl);
+        return qrCodeUrl;
+    }
+
+    @Override
+    public String generateTableQrCode(Integer tableId, Integer storeId) {
+        log.info("为桌号生成小程序二维码, tableId={}, storeId={}", tableId, storeId);
+
+        if (tableId == null || storeId == null) {
+            throw new IllegalArgumentException("tableId和storeId不能为空");
+        }
+
+        // 构建scene参数(格式:t=tableId&s=storeId,使用简写确保不超过32字符)
+        String scene = String.format("t=%d&s=%d", tableId, storeId);
+        
+        // 如果scene超过32字符,使用更简洁的格式
+        if (scene.length() > 32) {
+            scene = String.format("%d-%d", tableId, storeId);
+        }
+
+        // 构建OSS存储路径
+        String ossPath = String.format("qrcode/table/%d_%d_%d.png", 
+                storeId, tableId, System.currentTimeMillis());
+
+        // 生成二维码并上传
+        return generateQrCodeAndUpload(scene, null, 430, ossPath);
+    }
+}