Explorar o código

feat: 商家端点餐桌号管理相关功能及微信小程序二维码生成

penghao hai 2 meses
pai
achega
93a56a00a7

+ 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;
+}

+ 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;
+}

+ 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> {
+}

+ 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;
+    }
+}

+ 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());
+        }
+    }
+
+}

+ 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);
+}

+ 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);
+    }
+}