Jelajahi Sumber

Merge remote-tracking branch 'origin/sit-OrderFood' into sit-OrderFood

刘云鑫 2 bulan lalu
induk
melakukan
aac2edd8a0
90 mengubah file dengan 7868 tambahan dan 459 penghapusan
  1. 2 1
      alien-dining/src/main/java/shop/alien/dining/config/SwaggerConfig.java
  2. 114 0
      alien-dining/src/main/java/shop/alien/dining/controller/DiningUserController.java
  3. 34 0
      alien-dining/src/main/java/shop/alien/dining/dto/ChangePhoneDto.java
  4. 56 0
      alien-dining/src/main/java/shop/alien/dining/dto/UserProfileUpdateDto.java
  5. 26 0
      alien-dining/src/main/java/shop/alien/dining/dto/WeChatLoginDto.java
  6. 32 0
      alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java
  7. 49 0
      alien-dining/src/main/java/shop/alien/dining/service/DiningUserService.java
  8. 320 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java
  9. 345 0
      alien-dining/src/main/java/shop/alien/dining/util/WeChatMiniProgramUtil.java
  10. 73 0
      alien-dining/src/main/java/shop/alien/dining/vo/DiningUserVo.java
  11. 4 0
      alien-entity/pom.xml
  12. 95 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java
  13. 88 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCouponUsage.java
  14. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCuisineCategory.java
  15. 141 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrder.java
  16. 108 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderDetail.java
  17. 76 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderLock.java
  18. 86 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java
  19. 78 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTableLog.java
  20. 35 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddCartItemDTO.java
  21. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddDishDTO.java
  22. 21 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/BatchQueryTableStatusDTO.java
  23. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartDTO.java
  24. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java
  25. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/ChangeTableDTO.java
  26. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CreateOrderDTO.java
  27. 32 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategoryDTO.java
  28. 24 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCuisineCategorySortDTO.java
  29. 0 36
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffFitnessCourseGroup.java
  30. 0 37
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffFitnessCourseItem.java
  31. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreTableChangeDTO.java
  32. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreTableDTO.java
  33. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/UpdateOrderCouponDTO.java
  34. 40 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java
  35. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineComboItemVO.java
  36. 52 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineDetailVO.java
  37. 49 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineListVO.java
  38. 31 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/DiningPageInfoVO.java
  39. 12 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserDynamicsVo.java
  40. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderConfirmVO.java
  41. 68 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSettlementVO.java
  42. 34 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSuccessVO.java
  43. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreStaffReviewDetailVo.java
  44. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreTableStatusVO.java
  45. 2 2
      alien-entity/src/main/java/shop/alien/mapper/LifeUserDynamicsMapper.java
  46. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCartMapper.java
  47. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCouponUsageMapper.java
  48. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCuisineCategoryMapper.java
  49. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderDetailMapper.java
  50. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderLockMapper.java
  51. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderMapper.java
  52. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTableLogMapper.java
  53. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTableMapper.java
  54. 6 0
      alien-entity/src/main/resources/mapper/StoreStaffReviewMapper.xml
  55. 6 9
      alien-job/src/main/java/shop/alien/job/store/BadReviewAppealJob.java
  56. 62 0
      alien-store/src/main/java/shop/alien/store/config/WeChatMiniProgramConfig.java
  57. 255 0
      alien-store/src/main/java/shop/alien/store/controller/DiningController.java
  58. 1 1
      alien-store/src/main/java/shop/alien/store/controller/StoreBannerController.java
  59. 7 3
      alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java
  60. 147 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineCategoryController.java
  61. 322 0
      alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java
  62. 8 4
      alien-store/src/main/java/shop/alien/store/controller/StoreStaffReviewController.java
  63. 192 0
      alien-store/src/main/java/shop/alien/store/controller/StoreTableController.java
  64. 88 0
      alien-store/src/main/java/shop/alien/store/service/CartService.java
  65. 134 0
      alien-store/src/main/java/shop/alien/store/service/DiningService.java
  66. 33 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java
  67. 35 0
      alien-store/src/main/java/shop/alien/store/service/SseService.java
  68. 2 1
      alien-store/src/main/java/shop/alien/store/service/StoreClockInService.java
  69. 58 0
      alien-store/src/main/java/shop/alien/store/service/StoreCuisineCategoryService.java
  70. 95 0
      alien-store/src/main/java/shop/alien/store/service/StoreOrderService.java
  71. 3 1
      alien-store/src/main/java/shop/alien/store/service/StoreStaffReviewService.java
  72. 72 0
      alien-store/src/main/java/shop/alien/store/service/StoreTableService.java
  73. 47 0
      alien-store/src/main/java/shop/alien/store/service/WeChatMiniProgramQrCodeService.java
  74. 549 0
      alien-store/src/main/java/shop/alien/store/service/impl/CartServiceImpl.java
  75. 7 3
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  76. 569 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningServiceImpl.java
  77. 152 0
      alien-store/src/main/java/shop/alien/store/service/impl/SseServiceImpl.java
  78. 28 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBannerServiceImpl.java
  79. 10 9
      alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java
  80. 200 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineCategoryServiceImpl.java
  81. 85 34
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java
  82. 583 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOrderServiceImpl.java
  83. 175 287
      alien-store/src/main/java/shop/alien/store/service/impl/StorePlatformMenuServiceImpl.java
  84. 0 12
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java
  85. 72 7
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java
  86. 370 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreTableServiceImpl.java
  87. 3 12
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java
  88. 300 0
      alien-store/src/main/java/shop/alien/store/service/impl/WeChatMiniProgramQrCodeServiceImpl.java
  89. 195 0
      订单系统完整表结构.sql
  90. 286 0
      订单系统表结构说明.md

+ 2 - 1
alien-dining/src/main/java/shop/alien/dining/config/SwaggerConfig.java

@@ -33,7 +33,8 @@ public class SwaggerConfig {
                 .apiInfo(apiInfo())
                 .groupName("微信点餐Api服务")
                 .select()
-                .apis(RequestHandlerSelectors.any())
+                // 只扫描点餐模块的 Controller 包,避免展示系统默认的错误控制器等无关接口
+                .apis(RequestHandlerSelectors.basePackage("shop.alien.dining.controller"))
                 .paths(PathSelectors.any())
                 .build();
     }

+ 114 - 0
alien-dining/src/main/java/shop/alien/dining/controller/DiningUserController.java

@@ -0,0 +1,114 @@
+package shop.alien.dining.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.dto.WeChatLoginDto;
+import shop.alien.dining.service.DiningUserService;
+import shop.alien.dining.vo.DiningUserVo;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+
+/**
+ * 点餐用户控制器
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Api(tags = {"微信点餐-登录(用户端)"})
+@ApiSort(2)
+@CrossOrigin
+@RestController
+@RequestMapping("/dining/user")
+@RequiredArgsConstructor
+public class DiningUserController {
+
+    private final DiningUserService diningUserService;
+
+    @ApiOperation(value = "微信小程序登录", notes = "仅通过 wx.getPhoneNumber() 的 phoneCode 登录,无需 wx.login。后端调用 phonenumber.getPhoneNumber 换取手机号。")
+    @PostMapping("/wechatLogin")
+    public R<DiningUserVo> wechatLogin(@RequestBody WeChatLoginDto loginDto, HttpServletRequest request) {
+        log.info("微信小程序登录: phoneCode={}, storeId={}",
+                loginDto.getPhoneCode(),
+                loginDto.getStoreId());
+
+        if (loginDto.getPhoneCode() == null || loginDto.getPhoneCode().trim().isEmpty()) {
+            return R.fail("phoneCode不能为空,请通过 wx.getPhoneNumber() 获取");
+        }
+
+        String macIp = getClientIp(request);
+        DiningUserVo userVo = diningUserService.wechatLogin(
+                loginDto.getPhoneCode(),
+                loginDto.getStoreId(),
+                macIp);
+        if (userVo == null) {
+            return R.fail("登录失败,请先授权手机号(phoneCode 有效期5分钟,仅能使用一次)");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "获取用户信息", notes = "根据用户ID获取用户详细信息")
+    @GetMapping("/getUserInfo")
+    public R<DiningUserVo> getUserInfo(@RequestParam("userId") Long userId) {
+        log.info("获取用户信息: userId={}", userId);
+
+        if (userId == null) {
+            return R.fail("用户ID不能为空");
+        }
+        DiningUserVo userVo = diningUserService.getUserInfo(userId);
+        if (userVo == null) {
+            return R.fail("用户不存在或状态异常");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "更新用户个人信息", notes = "登录后完善资料,支持更新昵称、头像、性别、生日等")
+    @PostMapping("/updateProfile")
+    public R<DiningUserVo> updateProfile(@Valid @RequestBody UserProfileUpdateDto dto) {
+        log.info("更新用户个人信息: userId={}", dto.getUserId());
+
+        DiningUserVo userVo = diningUserService.updateProfile(dto);
+        if (userVo == null) {
+            return R.fail("更新失败,用户不存在");
+        }
+        return R.data(userVo);
+    }
+
+    @ApiOperation(value = "更换手机号", notes = "校验验证码后更新 user_phone,成功后需重新登录")
+    @PostMapping("/changePhone")
+    public R<DiningUserVo> changePhone(@Valid @RequestBody ChangePhoneDto dto) {
+        log.info("更换手机号: userId={}, newPhone={}", dto.getUserId(), dto.getNewPhone());
+
+        DiningUserVo userVo = diningUserService.changePhone(dto);
+        if (userVo == null) {
+            return R.fail("更换失败,请检查验证码或用户状态");
+        }
+        return R.data(userVo);
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        return ip;
+    }
+}

+ 34 - 0
alien-dining/src/main/java/shop/alien/dining/dto/ChangePhoneDto.java

@@ -0,0 +1,34 @@
+package shop.alien.dining.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+/**
+ * 更换手机号请求
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("更换手机号请求")
+public class ChangePhoneDto {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "新手机号", required = true)
+    @NotBlank(message = "新手机号不能为空")
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
+    private String newPhone;
+
+    @ApiModelProperty(value = "验证码", required = true)
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+}

+ 56 - 0
alien-dining/src/main/java/shop/alien/dining/dto/UserProfileUpdateDto.java

@@ -0,0 +1,56 @@
+package shop.alien.dining.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Date;
+
+/**
+ * 用户个人信息更新DTO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("用户个人信息更新请求")
+public class UserProfileUpdateDto {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "头像URL")
+    private String avatarUrl;
+
+    @ApiModelProperty(value = "性别(男/女)")
+    private String gender;
+
+    @ApiModelProperty(value = "生日", example = "1990-01-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date birthday;
+
+    @ApiModelProperty(value = "真实姓名")
+    private String realName;
+
+    @ApiModelProperty(value = "省份")
+    private String province;
+
+    @ApiModelProperty(value = "城市")
+    private String city;
+
+    @ApiModelProperty(value = "区县")
+    private String district;
+
+    @ApiModelProperty(value = "详细地址")
+    private String address;
+
+    @ApiModelProperty(value = "个人简介")
+    private String jianjie;
+}

+ 26 - 0
alien-dining/src/main/java/shop/alien/dining/dto/WeChatLoginDto.java

@@ -0,0 +1,26 @@
+package shop.alien.dining.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 微信小程序登录DTO(新方式:仅 getPhoneNumber,无需 wx.login)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("微信小程序登录请求")
+public class WeChatLoginDto {
+
+    @ApiModelProperty(value = "手机号凭证 code(通过 wx.getPhoneNumber() 获取,必填)", required = true)
+    @NotBlank(message = "phoneCode不能为空")
+    private String phoneCode;
+
+    @ApiModelProperty(value = "店铺ID", required = false)
+    private Long storeId;
+}

+ 32 - 0
alien-dining/src/main/java/shop/alien/dining/feign/AlienStoreFeign.java

@@ -0,0 +1,32 @@
+package shop.alien.dining.feign;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import shop.alien.entity.result.R;
+
+/**
+ * 点餐模块调用 alien-store(发码、校验等)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@FeignClient(name = "alienStoreFeign", url = "${feign.alienStore.url:}")
+public interface AlienStoreFeign {
+
+    /**
+     * 校验短信验证码(复用 store 阿里云短信)
+     *
+     * @param phone        手机号
+     * @param appType      0:用户 1:商家 2:商家web
+     * @param businessType 3:修改手机号
+     * @param code         验证码
+     */
+    @GetMapping("ali/checkSmsCode")
+    R checkSmsCode(
+            @RequestParam("phone") String phone,
+            @RequestParam("appType") Integer appType,
+            @RequestParam("businessType") Integer businessType,
+            @RequestParam("code") Integer code);
+}

+ 49 - 0
alien-dining/src/main/java/shop/alien/dining/service/DiningUserService.java

@@ -0,0 +1,49 @@
+package shop.alien.dining.service;
+
+import shop.alien.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.vo.DiningUserVo;
+
+/**
+ * 点餐用户服务接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+public interface DiningUserService {
+
+    /**
+     * 微信小程序登录(新方式:仅 phoneCode,无需 wx.login)
+     *
+     * @param phoneCode 手机号凭证(通过 wx.getPhoneNumber() 获取,必填)
+     * @param storeId   店铺ID(可选)
+     * @param macIp     客户端IP地址
+     * @return 用户信息(包含 token)
+     */
+    DiningUserVo wechatLogin(String phoneCode, Long storeId, String macIp);
+
+    /**
+     * 更新用户个人信息
+     *
+     * @param dto 用户信息更新DTO
+     * @return 更新后的用户信息
+     */
+    DiningUserVo updateProfile(UserProfileUpdateDto dto);
+
+    /**
+     * 获取用户信息
+     *
+     * @param userId 用户ID
+     * @return 用户信息
+     */
+    DiningUserVo getUserInfo(Long userId);
+
+    /**
+     * 更换手机号:Feign 调 store 校验验证码后更新 user_phone
+     *
+     * @param dto 更换手机号请求
+     * @return 更新后的用户信息,失败返回 null
+     */
+    DiningUserVo changePhone(ChangePhoneDto dto);
+}

+ 320 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java

@@ -0,0 +1,320 @@
+package shop.alien.dining.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.dining.config.BaseRedisService;
+import shop.alien.dining.dto.ChangePhoneDto;
+import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.feign.AlienStoreFeign;
+import shop.alien.dining.service.DiningUserService;
+import shop.alien.dining.util.WeChatMiniProgramUtil;
+import shop.alien.entity.result.R;
+import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.vo.LifeUserVo;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 点餐用户服务实现类(新方式:仅 phoneCode,无需 wx.login)
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningUserServiceImpl implements DiningUserService {
+
+    private final LifeUserMapper lifeUserMapper;
+    private final BaseRedisService baseRedisService;
+    private final WeChatMiniProgramUtil weChatMiniProgramUtil;
+    private final AlienStoreFeign alienStoreFeign;
+
+    @Value("${jwt.expiration-time}")
+    private String effectiveTime;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo wechatLogin(String phoneCode, Long storeId, String macIp) {
+        // 1. 通过 phonenumber.getPhoneNumber 换取手机号(必填)
+        String phone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
+        if (StringUtils.isBlank(phone)) {
+            log.warn("登录失败:无法通过 phoneCode 获取手机号(请先授权手机号,code 有效期5分钟且仅能使用一次)");
+            return null;
+        }
+        log.info("成功获取手机号: {}", phone.substring(0, Math.min(7, phone.length())) + "****");
+
+        // 2. 记录店铺ID(如果提供)
+        if (storeId != null) {
+            log.info("登录请求包含店铺ID: storeId={}", storeId);
+        }
+
+        // 3. 根据手机号查询用户
+        LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LifeUser::getUserPhone, phone);
+        LifeUser user = lifeUserMapper.selectOne(queryWrapper);
+
+        // 4. 用户不存在则创建
+        if (user == null) {
+            user = new LifeUser();
+            user.setUserPhone(phone);
+            user.setUserName(phone);
+            user.setRealName(phone);
+            user.setCreatedTime(new Date());
+            int ret = lifeUserMapper.insert(user);
+            if (ret != 1) {
+                log.error("创建用户失败");
+                return null;
+            }
+            queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(LifeUser::getUserPhone, phone);
+            user = lifeUserMapper.selectOne(queryWrapper);
+            log.info("创建新用户: phone={}, userId={}", phone, user.getId());
+        } else {
+            log.info("用户已存在,直接登录: phone={}, userId={}", phone, user.getId());
+        }
+
+        // 5. 检查用户状态
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("用户已被封禁: userId={}", user.getId());
+            return null;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("用户已注销: userId={}", user.getId());
+            return null;
+        }
+
+        // 6. 生成 token(不含 openid,新方式不做 code2Session)
+        Map<String, String> tokenMap = new HashMap<>();
+        tokenMap.put("phone", user.getUserPhone());
+        tokenMap.put("userName", user.getUserName() != null ? user.getUserName() : "用户");
+        tokenMap.put("userId", user.getId().toString());
+        tokenMap.put("userType", "user");
+        String token = generateToken(user.getUserPhone(), user.getUserName() != null ? user.getUserName() : "用户", tokenMap);
+
+        // 7. 存入 Redis
+        baseRedisService.setString("user_" + user.getUserPhone(), token);
+
+        // 8. 构建返回
+        LifeUserVo userVo = new LifeUserVo();
+        BeanUtils.copyProperties(user, userVo);
+        userVo.setToken(token);
+
+        DiningUserVo diningUserVo = new DiningUserVo();
+        diningUserVo.setId(user.getId().longValue());
+        diningUserVo.setPhone(user.getUserPhone());
+        diningUserVo.setNickName(user.getUserName());
+        diningUserVo.setAvatarUrl(user.getUserImage());
+        diningUserVo.setStatus(0);
+        diningUserVo.setCreatedTime(user.getCreatedTime());
+        diningUserVo.setToken(token);
+
+        return diningUserVo;
+    }
+
+    private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
+        int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
+        String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
+        long effectiveTimeIntLong = 0L;
+        switch (effectiveTimeUnit) {
+            case "s":
+                effectiveTimeIntLong = effectiveTimeInt * 1000L;
+                break;
+            case "m":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 1000L;
+                break;
+            case "h":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 60L * 1000L;
+                break;
+            case "d":
+                effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
+                break;
+            default:
+                effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
+        }
+        return JwtUtil.createJWT("user_" + userId, userName, JSONObject.toJSONString(tokenMap), effectiveTimeIntLong);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo updateProfile(UserProfileUpdateDto dto) {
+        // 1. 查询用户是否存在
+        LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
+        if (user == null) {
+            log.warn("更新个人信息失败:用户不存在, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 2. 更新用户信息(只更新非空字段)
+        if (StringUtils.isNotBlank(dto.getNickName())) {
+            user.setUserName(dto.getNickName());
+        }
+        if (StringUtils.isNotBlank(dto.getAvatarUrl())) {
+            user.setUserImage(dto.getAvatarUrl());
+        }
+        if (StringUtils.isNotBlank(dto.getGender())) {
+            user.setUserSex(dto.getGender());
+        }
+        if (dto.getBirthday() != null) {
+            user.setUserBirthday(dto.getBirthday());
+        }
+        if (StringUtils.isNotBlank(dto.getRealName())) {
+            user.setRealName(dto.getRealName());
+        }
+        if (StringUtils.isNotBlank(dto.getProvince())) {
+            user.setProvince(dto.getProvince());
+        }
+        if (StringUtils.isNotBlank(dto.getCity())) {
+            user.setCity(dto.getCity());
+        }
+        if (StringUtils.isNotBlank(dto.getDistrict())) {
+            user.setDistrict(dto.getDistrict());
+        }
+        if (StringUtils.isNotBlank(dto.getAddress())) {
+            user.setAddress(dto.getAddress());
+        }
+        if (StringUtils.isNotBlank(dto.getJianjie())) {
+            user.setJianjie(dto.getJianjie());
+        }
+
+        // 3. 设置更新时间
+        user.setUpdatedTime(new Date());
+
+        // 4. 执行更新
+        int result = lifeUserMapper.updateById(user);
+        if (result != 1) {
+            log.error("更新用户信息失败, userId={}", dto.getUserId());
+            return null;
+        }
+        log.info("用户信息更新成功, userId={}", dto.getUserId());
+
+        // 5. 返回更新后的用户信息
+        return buildDiningUserVo(user);
+    }
+
+    @Override
+    public DiningUserVo getUserInfo(Long userId) {
+        // 1. 查询用户
+        LifeUser user = lifeUserMapper.selectById(userId.intValue());
+        if (user == null) {
+            log.warn("获取用户信息失败:用户不存在, userId={}", userId);
+            return null;
+        }
+
+        // 2. 检查用户状态
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("用户已被封禁: userId={}", userId);
+            return null;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("用户已注销: userId={}", userId);
+            return null;
+        }
+
+        // 3. 返回用户信息
+        return buildDiningUserVo(user);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DiningUserVo changePhone(ChangePhoneDto dto) {
+        String newPhone = dto.getNewPhone().trim();
+        String codeStr = dto.getCode().trim();
+
+        // 1. 校验验证码(Feign 调 store ali/checkSmsCode,appType=0 用户端,businessType=3 修改手机号)
+        int codeInt;
+        try {
+            codeInt = Integer.parseInt(codeStr);
+        } catch (NumberFormatException e) {
+            log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
+            return null;
+        }
+        R checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
+        if (!R.isSuccess(checkRes)) {
+            log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
+            return null;
+        }
+
+        // 2. 查询用户
+        LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
+        if (user == null) {
+            log.warn("更换手机号失败:用户不存在, userId={}", dto.getUserId());
+            return null;
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            log.warn("更换手机号失败:用户已被封禁, userId={}", dto.getUserId());
+            return null;
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            log.warn("更换手机号失败:用户已注销, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 3. 新手机号与当前相同则无需更新
+        if (newPhone.equals(user.getUserPhone())) {
+            log.info("新手机号与当前相同,无需更换, userId={}", dto.getUserId());
+            return buildDiningUserVo(user);
+        }
+
+        // 4. 新手机号是否已被其他用户使用
+        LambdaQueryWrapper<LifeUser> q = new LambdaQueryWrapper<>();
+        q.eq(LifeUser::getUserPhone, newPhone);
+        LifeUser existing = lifeUserMapper.selectOne(q);
+        if (existing != null && !existing.getId().equals(user.getId())) {
+            log.warn("更换手机号失败:新手机号已被其他用户使用, newPhone={}", newPhone);
+            return null;
+        }
+
+        // 5. 更新 user_phone
+        String oldPhone = user.getUserPhone();
+        user.setUserPhone(newPhone);
+        user.setUpdatedTime(new Date());
+        int n = lifeUserMapper.updateById(user);
+        if (n != 1) {
+            log.error("更换手机号失败:更新数据库异常, userId={}", dto.getUserId());
+            return null;
+        }
+
+        // 6. 清除旧手机号对应 token,强制重新登录
+        baseRedisService.delete("user_" + oldPhone);
+        log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}", dto.getUserId(), oldPhone, newPhone);
+
+        return buildDiningUserVo(user);
+    }
+
+    /**
+     * 构建 DiningUserVo
+     */
+    private DiningUserVo buildDiningUserVo(LifeUser user) {
+        DiningUserVo diningUserVo = new DiningUserVo();
+        diningUserVo.setId(user.getId().longValue());
+        diningUserVo.setPhone(user.getUserPhone());
+        diningUserVo.setNickName(user.getUserName());
+        diningUserVo.setAvatarUrl(user.getUserImage());
+        diningUserVo.setStatus(0);
+        diningUserVo.setCreatedTime(user.getCreatedTime());
+        // 补充更多字段
+        diningUserVo.setGender(user.getUserSex());
+        diningUserVo.setBirthday(user.getUserBirthday());
+        diningUserVo.setRealName(user.getRealName());
+        diningUserVo.setProvince(user.getProvince());
+        diningUserVo.setCity(user.getCity());
+        diningUserVo.setDistrict(user.getDistrict());
+        diningUserVo.setAddress(user.getAddress());
+        diningUserVo.setJianjie(user.getJianjie());
+        return diningUserVo;
+    }
+}

+ 345 - 0
alien-dining/src/main/java/shop/alien/dining/util/WeChatMiniProgramUtil.java

@@ -0,0 +1,345 @@
+package shop.alien.dining.util;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.util.encryption.StandardAesUtil;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 微信小程序工具类
+ * 用于调用微信小程序相关接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+@Component
+public class WeChatMiniProgramUtil {
+
+    @Value("${wechat.miniprogram.appId}")
+    private String appId;
+
+    @Value("${wechat.miniprogram.appSecret}")
+    private String appSecret;
+
+    /**
+     * 微信小程序登录接口地址
+     */
+    private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
+
+    /**
+     * 微信小程序手机号获取接口地址(新版本)
+     */
+    private static final String CODE2VERIFYINFO_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
+
+    /**
+     * 通过 code 获取 openid 和 session_key
+     *
+     * @param code 小程序端通过 wx.login() 获取的 code
+     * @return WeChatSessionInfo 包含 openid、session_key、unionid 等信息
+     */
+    public WeChatSessionInfo code2Session(String code) {
+        String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+                CODE2SESSION_URL, appId, appSecret, code);
+
+        HttpClient httpClient = HttpClients.createDefault();
+        HttpGet httpGet = new HttpGet(url);
+
+        try {
+            HttpResponse response = httpClient.execute(httpGet);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            log.info("微信 code2session 响应: {}", responseBody);
+
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+
+            // 检查是否有错误
+            if (jsonObject.containsKey("errcode")) {
+                Integer errcode = jsonObject.getInteger("errcode");
+                String errmsg = jsonObject.getString("errmsg");
+                
+                // 将错误信息保存到返回对象中,方便上层处理
+                WeChatSessionInfo errorInfo = new WeChatSessionInfo();
+                errorInfo.setErrcode(errcode);
+                errorInfo.setErrmsg(errmsg);
+                
+                // 根据错误码记录详细的错误信息
+                String errorDetail = getErrorDetail(errcode, errmsg);
+                log.error("微信 code2session 失败: errcode={}, errmsg={}, 详情={}", errcode, errmsg, errorDetail);
+                return errorInfo;
+            }
+
+            // 解析返回数据
+            WeChatSessionInfo sessionInfo = new WeChatSessionInfo();
+            sessionInfo.setOpenid(jsonObject.getString("openid"));
+            sessionInfo.setSessionKey(jsonObject.getString("session_key"));
+            sessionInfo.setUnionid(jsonObject.getString("unionid"));
+
+            return sessionInfo;
+        } catch (IOException e) {
+            log.error("调用微信 code2session 接口异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取微信错误码的详细说明
+     */
+    private String getErrorDetail(Integer errcode, String errmsg) {
+        if (errcode == null) {
+            return "未知错误";
+        }
+        
+        switch (errcode) {
+            case 40029:
+                return "code无效(可能原因:1. code已使用过,每个code只能使用一次;2. code已过期,有效期5分钟;3. code格式错误;4. appId或appSecret配置错误)";
+            case 45011:
+                return "频率限制,每个用户每分钟最多调用5次";
+            case 40226:
+                return "高风险等级用户,需要用户进行验证";
+            case 40013:
+                return "appId无效,请检查Nacos配置中的wechat.miniprogram.appId";
+            case 40125:
+                return "appSecret无效,请检查Nacos配置中的wechat.miniprogram.appSecret";
+            case -1:
+                return "系统繁忙,请稍后重试";
+            default:
+                return String.format("微信接口错误,错误码:%d,错误信息:%s", errcode, errmsg);
+        }
+    }
+
+    /**
+     * 解密微信小程序加密数据(手机号等)
+     * 使用 AES-128-CBC 算法,使用 session_key 作为密钥
+     *
+     * @param encryptedData 加密数据(Base64编码)
+     * @param sessionKey    session_key(Base64编码)
+     * @param iv           初始向量(Base64编码)
+     * @return 解密后的JSON字符串,包含手机号等信息
+     */
+    public String decryptData(String encryptedData, String sessionKey, String iv) {
+        try {
+            if (encryptedData == null || encryptedData.isEmpty()) {
+                log.warn("加密数据为空,无法解密");
+                return null;
+            }
+            if (sessionKey == null || sessionKey.isEmpty()) {
+                log.warn("session_key为空,无法解密");
+                return null;
+            }
+            if (iv == null || iv.isEmpty()) {
+                log.warn("初始向量为空,无法解密");
+                return null;
+            }
+
+            // Base64解码
+            byte[] dataByte = Base64.decodeBase64(encryptedData);
+            byte[] keyByte = Base64.decodeBase64(sessionKey);
+            byte[] ivByte = Base64.decodeBase64(iv);
+
+            // AES-128-CBC 解密
+            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
+            IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
+            cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec);
+
+            byte[] decrypted = cipher.doFinal(dataByte);
+            String result = new String(decrypted, StandardCharsets.UTF_8);
+            
+            log.info("微信加密数据解密成功");
+            return result;
+        } catch (Exception e) {
+            log.error("微信加密数据解密失败: encryptedData={}, error={}", 
+                    encryptedData != null ? encryptedData.substring(0, Math.min(20, encryptedData.length())) : "null", 
+                    e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 从解密后的数据中提取手机号(旧版本,已废弃)
+     *
+     * @param decryptedData 解密后的JSON字符串
+     * @return 手机号
+     */
+    @Deprecated
+    public String extractPhoneNumber(String decryptedData) {
+        try {
+            if (decryptedData == null || decryptedData.isEmpty()) {
+                return null;
+            }
+            
+            JSONObject jsonObject = JSONObject.parseObject(decryptedData);
+            String phoneNumber = jsonObject.getString("phoneNumber");
+            
+            log.info("从解密数据中提取手机号: {}", phoneNumber != null ? phoneNumber.substring(0, Math.min(7, phoneNumber.length())) + "****" : "null");
+            return phoneNumber;
+        } catch (Exception e) {
+            log.error("提取手机号失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 通过手机号凭证 code 获取手机号(新版本API)
+     * 使用 code2Verifyinfo 接口
+     *
+     * @param phoneCode 手机号凭证 code(通过wx.getPhoneNumber()获取)
+     * @return 手机号,如果获取失败返回null
+     */
+    public String getPhoneNumberByCode(String phoneCode) {
+        if (phoneCode == null || phoneCode.isEmpty()) {
+            log.warn("手机号凭证code为空");
+            return null;
+        }
+
+        try {
+            // 获取 access_token
+            String accessToken = getAccessToken();
+            if (accessToken == null || accessToken.isEmpty()) {
+                log.error("获取access_token失败,无法调用手机号接口");
+                return null;
+            }
+
+            // 构建请求URL
+            String url = String.format("%s?access_token=%s", CODE2VERIFYINFO_URL, accessToken);
+            
+            // 构建请求体
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("code", phoneCode);
+
+            // 使用HttpClient发送POST请求
+            HttpClient httpClient = HttpClients.createDefault();
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setHeader("Content-Type", "application/json");
+            httpPost.setEntity(new StringEntity(requestBody.toJSONString(), "UTF-8"));
+
+            HttpResponse response = httpClient.execute(httpPost);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            log.info("微信 code2Verifyinfo 响应: {}", responseBody);
+
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+
+            // 检查是否有错误(errcode 为 0 表示成功,非 0 才视为失败)
+            if (jsonObject.containsKey("errcode")) {
+                Integer errcode = jsonObject.getInteger("errcode");
+                if (errcode != null && errcode != 0) {
+                    String errmsg = jsonObject.getString("errmsg");
+                    log.error("微信 code2Verifyinfo 失败: errcode={}, errmsg={}", errcode, errmsg);
+                    return null;
+                }
+            }
+
+            // 解析返回数据
+            JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
+            if (phoneInfo != null) {
+                String phoneNumber = phoneInfo.getString("phoneNumber");
+                log.info("成功获取手机号");
+                return phoneNumber;
+            }
+
+            return null;
+        } catch (Exception e) {
+            log.error("调用微信 code2Verifyinfo 接口异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取微信 access_token
+     * TODO: 应该实现token缓存机制,避免频繁请求(access_token有效期为7200秒)
+     */
+    private String getAccessToken() {
+        try {
+            String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                    appId, appSecret);
+            
+            HttpClient httpClient = HttpClients.createDefault();
+            HttpGet httpGet = new HttpGet(url);
+            HttpResponse response = httpClient.execute(httpGet);
+            String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+            
+            JSONObject jsonObject = JSONObject.parseObject(responseBody);
+            if (jsonObject.containsKey("access_token")) {
+                String accessToken = jsonObject.getString("access_token");
+                log.debug("成功获取access_token");
+                return accessToken;
+            } else {
+                Integer errcode = jsonObject.getInteger("errcode");
+                String errmsg = jsonObject.getString("errmsg");
+                log.error("获取access_token失败: errcode={}, errmsg={}", errcode, errmsg);
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("获取access_token异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 微信会话信息
+     */
+    public static class WeChatSessionInfo {
+        private String openid;
+        private String sessionKey;
+        private String unionid;
+        private Integer errcode;
+        private String errmsg;
+
+        public String getOpenid() {
+            return openid;
+        }
+
+        public void setOpenid(String openid) {
+            this.openid = openid;
+        }
+
+        public String getSessionKey() {
+            return sessionKey;
+        }
+
+        public void setSessionKey(String sessionKey) {
+            this.sessionKey = sessionKey;
+        }
+
+        public String getUnionid() {
+            return unionid;
+        }
+
+        public void setUnionid(String unionid) {
+            this.unionid = unionid;
+        }
+
+        public Integer getErrcode() {
+            return errcode;
+        }
+
+        public void setErrcode(Integer errcode) {
+            this.errcode = errcode;
+        }
+
+        public String getErrmsg() {
+            return errmsg;
+        }
+
+        public void setErrmsg(String errmsg) {
+            this.errmsg = errmsg;
+        }
+    }
+}
+

+ 73 - 0
alien-dining/src/main/java/shop/alien/dining/vo/DiningUserVo.java

@@ -0,0 +1,73 @@
+package shop.alien.dining.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 点餐用户VO
+ * 基于 LifeUser 表
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("点餐用户信息")
+public class DiningUserVo {
+
+    @ApiModelProperty("用户ID")
+    private Long id;
+
+    @ApiModelProperty("用户昵称")
+    private String nickName;
+
+    @ApiModelProperty("用户头像")
+    private String avatarUrl;
+
+    @ApiModelProperty("手机号")
+    private String phone;
+
+    @ApiModelProperty("性别")
+    private String gender;
+
+    @ApiModelProperty("生日")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date birthday;
+
+    @ApiModelProperty("真实姓名")
+    private String realName;
+
+    @ApiModelProperty("省份")
+    private String province;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("区县")
+    private String district;
+
+    @ApiModelProperty("详细地址")
+    private String address;
+
+    @ApiModelProperty("个人简介")
+    private String jianjie;
+
+    @ApiModelProperty("用户状态:0-正常,1-禁用")
+    private Integer status;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty("最后登录时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastLoginTime;
+
+    @ApiModelProperty("JWT Token")
+    private String token;
+}
+

+ 4 - 0
alien-entity/pom.xml

@@ -92,6 +92,10 @@
             <artifactId>alien-util</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 95 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java

@@ -0,0 +1,95 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 购物车表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_cart")
+@ApiModel(value = "StoreCart对象", description = "购物车表")
+public class StoreCart {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    @TableField("quantity")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "小计金额")
+    @TableField("subtotal_amount")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    @TableField("add_user_id")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    @TableField("add_user_phone")
+    private String addUserPhone;
+
+    @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;
+}

+ 88 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreCouponUsage.java

@@ -0,0 +1,88 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 优惠券使用记录表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_coupon_usage")
+@ApiModel(value = "StoreCouponUsage对象", description = "优惠券使用记录表")
+public class StoreCouponUsage {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "订单ID(下单时关联)")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "优惠券ID")
+    @TableField("coupon_id")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    @TableField("coupon_name")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    @TableField("discount_amount")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "使用状态(0:已标记使用, 1:已下单, 2:已支付, 3:已取消)")
+    @TableField("usage_status")
+    private Integer usageStatus;
+
+    @ApiModelProperty(value = "换桌前的桌号ID(如果是换桌迁移)")
+    @TableField("from_table_id")
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "换桌迁移时间")
+    @TableField("migrate_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date migrateTime;
+
+    @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;
+}

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

+ 141 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOrder.java

@@ -0,0 +1,141 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_order")
+@ApiModel(value = "StoreOrder对象", description = "订单表")
+public class StoreOrder {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号ID")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    @TableField("table_number")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    @TableField("diner_count")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "支付用户ID")
+    @TableField("pay_user_id")
+    private Integer payUserId;
+
+    @ApiModelProperty(value = "支付用户手机号")
+    @TableField("pay_user_phone")
+    private String payUserPhone;
+
+    @ApiModelProperty(value = "联系电话")
+    @TableField("contact_phone")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "餐具费")
+    @TableField("tableware_fee")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    @TableField("order_status")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "订单总金额")
+    @TableField("total_amount")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "优惠券ID")
+    @TableField("coupon_id")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "当前使用的优惠券ID(用于换桌场景)")
+    @TableField("current_coupon_id")
+    private Integer currentCouponId;
+
+    @ApiModelProperty(value = "优惠金额")
+    @TableField("discount_amount")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "锁定用户ID(下单或结算时锁定)")
+    @TableField("lock_user_id")
+    private Integer lockUserId;
+
+    @ApiModelProperty(value = "锁定过期时间")
+    @TableField("lock_expire_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lockExpireTime;
+
+    @ApiModelProperty(value = "实付金额")
+    @TableField("pay_amount")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "支付方式(1:微信, 2:支付宝, 3:现金)")
+    @TableField("pay_type")
+    private Integer payType;
+
+    @ApiModelProperty(value = "支付状态(0:未支付, 1:已支付, 2:已退款)")
+    @TableField("pay_status")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date payTime;
+
+    @ApiModelProperty(value = "支付交易号")
+    @TableField("pay_trade_no")
+    private String payTradeNo;
+
+    @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;
+}

+ 108 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOrderDetail.java

@@ -0,0 +1,108 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单明细表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_order_detail")
+@ApiModel(value = "StoreOrderDetail对象", description = "订单明细表")
+public class StoreOrderDetail {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单ID")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    @TableField("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "菜品ID")
+    @TableField("cuisine_id")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    @TableField("cuisine_name")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    @TableField("cuisine_type")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    @TableField("cuisine_image")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    @TableField("unit_price")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    @TableField("quantity")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "小计金额")
+    @TableField("subtotal_amount")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    @TableField("add_user_id")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    @TableField("add_user_phone")
+    private String addUserPhone;
+
+    @ApiModelProperty(value = "是否加餐(0:初始下单, 1:加餐)")
+    @TableField("is_add_dish")
+    private Integer isAddDish;
+
+    @ApiModelProperty(value = "加餐时间")
+    @TableField("add_dish_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date addDishTime;
+
+    @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;
+}

+ 76 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOrderLock.java

@@ -0,0 +1,76 @@
+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_order_lock")
+@ApiModel(value = "StoreOrderLock对象", description = "订单锁定记录表")
+public class StoreOrderLock {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "桌号ID(下单锁定)")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "订单ID(结算锁定)")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "锁定类型(1:下单锁定, 2:结算锁定)")
+    @TableField("lock_type")
+    private Integer lockType;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    @TableField("lock_user_id")
+    private Integer lockUserId;
+
+    @ApiModelProperty(value = "锁定用户手机号")
+    @TableField("lock_user_phone")
+    private String lockUserPhone;
+
+    @ApiModelProperty(value = "锁定过期时间")
+    @TableField("lock_expire_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lockExpireTime;
+
+    @ApiModelProperty(value = "锁定状态(0:已释放, 1:锁定中)")
+    @TableField("lock_status")
+    private Integer lockStatus;
+
+    @ApiModelProperty(value = "释放时间")
+    @TableField("release_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date releaseTime;
+
+    @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 = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

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

@@ -0,0 +1,86 @@
+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 = "当前使用的优惠券ID")
+    @TableField("current_coupon_id")
+    private Integer currentCouponId;
+
+    @ApiModelProperty(value = "购物车商品数量(缓存)")
+    @TableField("cart_item_count")
+    private Integer cartItemCount;
+
+    @ApiModelProperty(value = "购物车总金额(缓存)")
+    @TableField("cart_total_amount")
+    private java.math.BigDecimal cartTotalAmount;
+
+    @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;
+}

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AddCartItemDTO.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Positive;
+
+/**
+ * 添加购物车商品DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AddCartItemDTO对象", description = "添加购物车商品")
+public class AddCartItemDTO {
+
+    @ApiModelProperty(value = "桌号ID", required = true)
+    @NotNull(message = "桌号ID不能为空")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "菜品ID", required = true)
+    @NotNull(message = "菜品ID不能为空")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "数量", required = true)
+    @NotNull(message = "数量不能为空")
+    @Positive(message = "数量必须大于0")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AddDishDTO.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 加餐DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AddDishDTO对象", description = "加餐")
+public class AddDishDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    @NotNull(message = "订单ID不能为空")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "菜品ID", required = true)
+    @NotNull(message = "菜品ID不能为空")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "数量", required = true)
+    @NotNull(message = "数量不能为空")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

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

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

@@ -0,0 +1,37 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 购物车DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CartDTO对象", description = "购物车")
+public class CartDTO {
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "购物车商品列表")
+    private List<CartItemDTO> items;
+
+    @ApiModelProperty(value = "总金额")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "商品总数量")
+    private Integer totalQuantity;
+}

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 购物车商品项DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CartItemDTO对象", description = "购物车商品项")
+public class CartItemDTO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "小计金额")
+    private BigDecimal subtotalAmount;
+
+    @ApiModelProperty(value = "添加该菜品的用户ID")
+    private Integer addUserId;
+
+    @ApiModelProperty(value = "添加该菜品的用户手机号")
+    private String addUserPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+}

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

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 换桌DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "ChangeTableDTO对象", description = "换桌")
+public class ChangeTableDTO {
+
+    @ApiModelProperty(value = "原桌号ID", required = true)
+    @NotNull(message = "原桌号ID不能为空")
+    private Integer fromTableId;
+
+    @ApiModelProperty(value = "目标桌号ID", required = true)
+    @NotNull(message = "目标桌号ID不能为空")
+    private Integer toTableId;
+
+    @ApiModelProperty(value = "换桌原因")
+    private String changeReason;
+}

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

@@ -0,0 +1,37 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 创建订单DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CreateOrderDTO对象", description = "创建订单")
+public class CreateOrderDTO {
+
+    @ApiModelProperty(value = "桌号ID", required = true)
+    @NotNull(message = "桌号ID不能为空")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注(限30字)")
+    private String remark;
+
+    @ApiModelProperty(value = "是否立即支付(0:否,创建订单但不支付; 1:是,创建订单并支付)")
+    private Integer immediatePay;
+}

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

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/UpdateOrderCouponDTO.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 更新订单优惠券DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "UpdateOrderCouponDTO对象", description = "更新订单优惠券")
+public class UpdateOrderCouponDTO {
+
+    @ApiModelProperty(value = "订单ID", required = true)
+    @NotNull(message = "订单ID不能为空")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "优惠券ID(可为空,表示不使用优惠券)")
+    private Integer couponId;
+}

+ 40 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java

@@ -0,0 +1,40 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 可领取优惠券VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "AvailableCouponVO对象", description = "可领取优惠券")
+public class AvailableCouponVO {
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String name;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal nominalValue;
+
+    @ApiModelProperty(value = "使用标准(最低消费)")
+    private BigDecimal minimumSpendingAmount;
+
+    @ApiModelProperty(value = "到期时间")
+    private LocalDate endDate;
+
+    @ApiModelProperty(value = "是否已领取")
+    private Boolean isReceived;
+
+    @ApiModelProperty(value = "是否可用(库存>0且未过期)")
+    private Boolean isAvailable;
+}

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineComboItemVO.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 套餐包含的菜品项VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineComboItemVO对象", description = "套餐包含的菜品项")
+public class CuisineComboItemVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "数量")
+    private Integer quantity;
+
+    @ApiModelProperty(value = "类别")
+    private String category;
+}

+ 52 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineDetailVO.java

@@ -0,0 +1,52 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 菜品详情VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineDetailVO对象", description = "菜品详情")
+public class CuisineDetailVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String name;
+
+    @ApiModelProperty(value = "菜品图片列表")
+    private List<String> images;
+
+    @ApiModelProperty(value = "价格")
+    private BigDecimal price;
+
+    @ApiModelProperty(value = "月售数量")
+    private Integer monthlySales;
+
+    @ApiModelProperty(value = "标签(多个标签用逗号分隔)")
+    private String tags;
+
+    @ApiModelProperty(value = "短评")
+    private String shortComment;
+
+    @ApiModelProperty(value = "菜品介绍")
+    private String detailContent;
+
+    @ApiModelProperty(value = "购物车中的数量")
+    private Integer cartQuantity;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "套餐包含的菜品列表(仅套餐有)")
+    private List<CuisineComboItemVO> comboItems;
+}

+ 49 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineListVO.java

@@ -0,0 +1,49 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 菜品列表VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "CuisineListVO对象", description = "菜品列表")
+public class CuisineListVO {
+
+    @ApiModelProperty(value = "菜品ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "菜品名称")
+    private String name;
+
+    @ApiModelProperty(value = "菜品图片(首张)")
+    private String firstImage;
+
+    @ApiModelProperty(value = "价格")
+    private BigDecimal price;
+
+    @ApiModelProperty(value = "短评")
+    private String shortComment;
+
+    @ApiModelProperty(value = "标签(多个标签用逗号分隔)")
+    private String tags;
+
+    @ApiModelProperty(value = "月售数量")
+    private Integer monthlySales;
+
+    @ApiModelProperty(value = "购物车中的数量")
+    private Integer cartQuantity;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐)")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "上下架状态:1-上架,2-下架")
+    private Integer shelfStatus;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/DiningPageInfoVO.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 点餐页面信息VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "DiningPageInfoVO对象", description = "点餐页面信息")
+public class DiningPageInfoVO {
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号ID")
+    private Integer tableId;
+}

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserDynamicsVo.java

@@ -68,4 +68,16 @@ public class LifeUserDynamicsVo extends LifeUserDynamics {
 
     @ApiModelProperty(value = "商家名称")
     private String storeName;
+
+    @ApiModelProperty(value = "评分")
+    private Double scoreAvg;
+
+     @ApiModelProperty(value = "行业")
+    private String businessSection;
+
+    @ApiModelProperty(value = "评价数量")
+    private String ratingCount;
+
+    @ApiModelProperty(value = "评价类型")
+    private String businessTypeName;
 }

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderConfirmVO.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 订单确认页面VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "OrderConfirmVO对象", description = "订单确认页面")
+public class OrderConfirmVO {
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注(限30字)")
+    private String remark;
+
+    @ApiModelProperty(value = "购物车商品列表")
+    private List<CartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "餐具费单价")
+    private BigDecimal tablewareUnitPrice;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额(菜品总价+餐具费-优惠券)")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "可用优惠券列表")
+    private List<AvailableCouponVO> availableCoupons;
+
+    @ApiModelProperty(value = "是否已锁定(有人正在下单)")
+    private Boolean isLocked;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    private Integer lockUserId;
+}

+ 68 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSettlementVO.java

@@ -0,0 +1,68 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.CartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 订单结算确认页面VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "OrderSettlementVO对象", description = "订单结算确认页面")
+public class OrderSettlementVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品清单")
+    private List<CartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "是否已锁定(有人正在结算)")
+    private Boolean isLocked;
+
+    @ApiModelProperty(value = "锁定用户ID")
+    private Integer lockUserId;
+}

+ 34 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSuccessVO.java

@@ -0,0 +1,34 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 下单成功页面VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "OrderSuccessVO对象", description = "下单成功页面")
+public class OrderSuccessVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    private Integer orderStatus;
+}

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreStaffReviewDetailVo.java

@@ -25,5 +25,30 @@ public class StoreStaffReviewDetailVo {
 
     @ApiModelProperty(value = "人员信息")
     private StoreStaffConfig staffInfo;
+
+    @ApiModelProperty(value = "店铺信息")
+    private StoreInfoForStaffReviewVo storeInfo;
+    
+    /**
+     * 店铺信息VO(用于员工评价详情)
+     */
+    @Data
+    @ApiModel(value = "StoreInfoForStaffReviewVo对象", description = "店铺信息VO(用于员工评价详情)")
+    public static class StoreInfoForStaffReviewVo {
+        @ApiModelProperty(value = "店铺ID")
+        private Integer storeId;
+        
+        @ApiModelProperty(value = "店铺名称")
+        private String storeName;
+        
+        @ApiModelProperty(value = "店铺头像(来自store_user表的head_img字段)")
+        private String headImg;
+        
+        @ApiModelProperty(value = "店铺评分(平均评分)")
+        private Double score;
+        
+        @ApiModelProperty(value = "到商铺的距离(单位:千米,如果未提供用户位置则为null)")
+        private Double distance;
+    }
 }
 

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

+ 2 - 2
alien-entity/src/main/java/shop/alien/mapper/LifeUserDynamicsMapper.java

@@ -23,7 +23,7 @@ public interface LifeUserDynamicsMapper extends BaseMapper<LifeUserDynamics> {
             "where lud.delete_flag = 0 and lud.enable_status = 0 and lud.draft = 0 and " +
             "not exists (select 1 from life_user_violation luv where luv.delete_flag = 0 and luv.processing_status = 1 " +
             "AND luv.dynamics_id = lud.id) order by lud.created_time desc) " +
-            "select dynamice.*, info.store_name userName, user.head_img userImage, info.id storeUserId, user.id storeOrUserId, 0 isExpert " +
+            "select dynamice.*, info.store_name userName, user.head_img userImage, info.id storeUserId, user.id storeOrUserId, 0 isExpert, info.score_avg scoreAvg, info.business_section businessSection, info.business_type_name businessTypeName " +
             "from dynamice " +
             "join store_user user on dynamice.phone = user.phone and user.delete_flag = 0 " +
             "and user.status = 0 and user.logout_flag = 0 " +
@@ -33,7 +33,7 @@ public interface LifeUserDynamicsMapper extends BaseMapper<LifeUserDynamics> {
             "where dynamice.flag = 'store' " +
             "union " +
             "select dynamice.*, user.user_name userName, user.user_image userImage, user.id storeUserId, user.id storeOrUserId, " +
-            "IF(lue.expert_code IS NOT NULL , 1, 0) AS isExpert " +
+            "IF(lue.expert_code IS NOT NULL , 1, 0) AS isExpert, 0 scoreAvg, 0 businessSection, 0 businessTypeName " +
             "from dynamice " +
             "join life_user user on dynamice.phone = user.user_phone and user.delete_flag = 0 " +
             "and user.logout_flag = 0 " +

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

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

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

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreCouponUsage;
+
+/**
+ * 优惠券使用记录表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreCouponUsageMapper extends BaseMapper<StoreCouponUsage> {
+}

+ 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/StoreOrderDetailMapper.java

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

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

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreOrderLock;
+
+/**
+ * 订单锁定记录表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreOrderLockMapper extends BaseMapper<StoreOrderLock> {
+}

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

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

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

+ 6 - 0
alien-entity/src/main/resources/mapper/StoreStaffReviewMapper.xml

@@ -134,6 +134,7 @@
         SELECT COUNT(*)
         FROM store_staff_review
         WHERE staff_user_id = #{staffUserId}
+          AND audit_status = 1
         AND delete_flag = 0
     </select>
 
@@ -143,6 +144,7 @@
         SELECT COUNT(*)
         FROM store_staff_review
         WHERE staff_user_id = #{staffUserId}
+          AND audit_status = 1
         AND delete_flag = 0
         AND overall_rating IS NOT NULL
         AND overall_rating >= 4.5
@@ -156,6 +158,7 @@
         SELECT COUNT(*)
         FROM store_staff_review
         WHERE staff_user_id = #{staffUserId}
+          AND audit_status = 1
         AND delete_flag = 0
         AND overall_rating IS NOT NULL
         AND overall_rating >= 3
@@ -169,6 +172,7 @@
         SELECT COUNT(*)
         FROM store_staff_review
         WHERE staff_user_id = #{staffUserId}
+          AND audit_status = 1
         AND delete_flag = 0
         AND overall_rating IS NOT NULL
         AND overall_rating >= 0
@@ -181,6 +185,7 @@
         SELECT COUNT(*)
         FROM store_staff_review
         WHERE staff_user_id = #{staffUserId}
+          AND audit_status = 1
         AND delete_flag = 0
         AND review_images IS NOT NULL
         AND review_images != ''
@@ -233,6 +238,7 @@
             AND CONVERT(llr.dianzan_id, CHAR) = CONVERT(#{currentUserId}, CHAR)
             AND llr.delete_flag = 0
         WHERE ssr.delete_flag = 0
+        AND ssr.audit_status = 1
         AND ssr.staff_user_id = #{staffUserId}
         <if test="type != null">
             <choose>

+ 6 - 9
alien-job/src/main/java/shop/alien/job/store/BadReviewAppealJob.java

@@ -273,16 +273,13 @@ public class BadReviewAppealJob {
             analyzeRequest.put("user_material",
                     storeCommentAppeal.get("comment_content") == null ? "" : storeCommentAppeal.get("comment_content").toString());
 
-            // 商家图片:支持多张,转成 Base64 数组
+            // 商家图片:支持多张,直接使用图片URL
             List<String> merchantImages = new ArrayList<>();
             String imgUrls = storeCommentAppeal.get("img_url") == null ? "" : storeCommentAppeal.get("img_url").toString();
             if (StringUtils.hasText(imgUrls)) {
                 // 假设 img_url 是多个图片用逗号分隔的字符串
                 for (String imageUrl : imgUrls.split(",")) {
-                    String base64 = convertImageToBase64(imageUrl.trim());
-                    if (StringUtils.hasText(base64)) {
-                        merchantImages.add(base64);
-                    }
+                    merchantImages.add(imageUrl.trim());
                 }
             }
             analyzeRequest.put("merchant_images", merchantImages);
@@ -292,11 +289,11 @@ public class BadReviewAppealJob {
             String userImgUrls = storeCommentAppeal.get("user_img_url") == null ? "" : storeCommentAppeal.get("user_img_url").toString();
             if (StringUtils.hasText(userImgUrls)) {
                 for (String imageUrl : userImgUrls.split(",")) {
-                    String base64 = convertImageToBase64(imageUrl.trim());
-                    if (StringUtils.hasText(base64)) {
-                        userImages.add(base64);
+//                    String base64 = convertImageToBase64(imageUrl.trim());
+//                    if (StringUtils.hasText(base64)) {
+                        userImages.add(imageUrl.trim());
                     }
-                }
+//                }
             }
             analyzeRequest.put("user_images", userImages);
 

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

+ 255 - 0
alien-store/src/main/java/shop/alien/store/controller/DiningController.java

@@ -0,0 +1,255 @@
+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.vo.*;
+import shop.alien.store.service.DiningService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.List;
+
+/**
+ * 点餐控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"点餐管理"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/dining")
+@RequiredArgsConstructor
+public class DiningController {
+
+    private final DiningService diningService;
+
+    @ApiOperation(value = "获取点餐页面信息", notes = "获取店铺名称、桌号、就餐人数等信息")
+    @GetMapping("/page-info")
+    public R<DiningPageInfoVO> getDiningPageInfo(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
+        try {
+            DiningPageInfoVO vo = diningService.getDiningPageInfo(tableId, dinerCount);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取点餐页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取点餐页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "搜索菜品", notes = "模糊搜索菜品,关键词限10字")
+    @GetMapping("/search")
+    public R<List<CuisineListVO>> searchCuisines(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "搜索关键词", required = false) @RequestParam(required = false) String keyword,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            // 限制关键词长度
+            if (StringUtils.hasText(keyword) && keyword.length() > 10) {
+                keyword = keyword.substring(0, 10);
+            }
+            List<CuisineListVO> list = diningService.searchCuisines(storeId, keyword, tableId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("搜索菜品失败: {}", e.getMessage(), e);
+            return R.fail("搜索菜品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "根据分类获取菜品列表", notes = "分页获取菜品列表,默认每页12条")
+    @GetMapping("/cuisines")
+    public R<List<CuisineListVO>> getCuisinesByCategory(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId,
+            @ApiParam(value = "分类ID", required = false) @RequestParam(required = false) Integer categoryId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "页码", required = false) @RequestParam(defaultValue = "1") Integer page,
+            @ApiParam(value = "每页数量", required = false) @RequestParam(defaultValue = "12") Integer size) {
+        try {
+            List<CuisineListVO> list = diningService.getCuisinesByCategory(storeId, categoryId, tableId, page, size);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取菜品列表失败: {}", e.getMessage(), e);
+            return R.fail("获取菜品列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取菜品详情", notes = "获取菜品详细信息,包含图片列表、月售数量等")
+    @GetMapping("/cuisine/{cuisineId}")
+    public R<CuisineDetailVO> getCuisineDetail(
+            @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            CuisineDetailVO vo = diningService.getCuisineDetail(cuisineId, tableId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取菜品详情失败: {}", e.getMessage(), e);
+            return R.fail("获取菜品详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取可领取的优惠券列表", notes = "获取用户可领取的优惠券")
+    @GetMapping("/coupons/available")
+    public R<List<AvailableCouponVO>> getAvailableCoupons(
+            @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+            List<AvailableCouponVO> list = diningService.getAvailableCoupons(storeId, userId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取可领取优惠券列表失败: {}", e.getMessage(), e);
+            return R.fail("获取可领取优惠券列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "领取优惠券", notes = "用户领取优惠券")
+    @PostMapping("/coupon/receive")
+    public R<Boolean> receiveCoupon(
+            @ApiParam(value = "优惠券ID", required = true) @RequestParam Integer couponId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            boolean result = diningService.receiveCoupon(couponId, userId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("领取优惠券失败: {}", e.getMessage(), e);
+            return R.fail("领取优惠券失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单确认页面信息", notes = "获取订单确认页面的所有信息")
+    @GetMapping("/order/confirm")
+    public R<OrderConfirmVO> getOrderConfirmInfo(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            OrderConfirmVO vo = diningService.getOrderConfirmInfo(tableId, dinerCount, userId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取订单确认页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取订单确认页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "锁定订单", notes = "锁定订单,防止多人同时下单")
+    @PostMapping("/order/lock")
+    public R<Boolean> lockOrder(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            boolean result = diningService.lockOrder(tableId, userId);
+            if (!result) {
+                return R.fail("订单已被其他用户锁定,无法下单");
+            }
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("锁定订单失败: {}", e.getMessage(), e);
+            return R.fail("锁定订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "解锁订单", notes = "解锁订单")
+    @PostMapping("/order/unlock")
+    public R<Boolean> unlockOrder(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            diningService.unlockOrder(tableId, userId);
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("解锁订单失败: {}", e.getMessage(), e);
+            return R.fail("解锁订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "检查订单锁定状态", notes = "检查订单是否被锁定")
+    @GetMapping("/order/check-lock")
+    public R<Integer> checkOrderLock(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            Integer lockUserId = diningService.checkOrderLock(tableId);
+            return R.data(lockUserId);
+        } catch (Exception e) {
+            log.error("检查订单锁定状态失败: {}", e.getMessage(), e);
+            return R.fail("检查订单锁定状态失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "获取订单结算确认页面信息", notes = "获取订单结算确认页面的所有信息")
+    @GetMapping("/order/settlement")
+    public R<shop.alien.entity.store.vo.OrderSettlementVO> getOrderSettlementInfo(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            shop.alien.entity.store.vo.OrderSettlementVO vo = diningService.getOrderSettlementInfo(orderId, userId);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("获取订单结算确认页面信息失败: {}", e.getMessage(), e);
+            return R.fail("获取订单结算确认页面信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "锁定订单结算", notes = "锁定订单结算,防止多人同时结算")
+    @PostMapping("/order/settlement/lock")
+    public R<Boolean> lockSettlement(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            boolean result = diningService.lockSettlement(orderId, userId);
+            if (!result) {
+                return R.fail("订单已被其他用户锁定,无法结算");
+            }
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("锁定订单结算失败: {}", e.getMessage(), e);
+            return R.fail("锁定订单结算失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "解锁订单结算", notes = "解锁订单结算")
+    @PostMapping("/order/settlement/unlock")
+    public R<Boolean> unlockSettlement(
+            @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo == null) {
+                return R.fail("用户未登录");
+            }
+            Integer userId = userInfo.getInteger("userId");
+            diningService.unlockSettlement(orderId, userId);
+            return R.data(true);
+        } catch (Exception e) {
+            log.error("解锁订单结算失败: {}", e.getMessage(), e);
+            return R.fail("解锁订单结算失败: " + e.getMessage());
+        }
+    }
+}

+ 1 - 1
alien-store/src/main/java/shop/alien/store/controller/StoreBannerController.java

@@ -39,7 +39,7 @@ public class StoreBannerController {
 
     @ApiOperation("删除Banner")
     @ApiOperationSupport(order = 2)
-    @PostMapping("/delete")
+    @GetMapping("/delete")
     public R<String> delete(@RequestParam("id") Integer id) {
         log.info("StoreBannerController.delete?id={}", id);
         return storeBannerService.removeById(id) ? R.success("删除成功") : R.fail("删除失败");

+ 7 - 3
alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java

@@ -7,9 +7,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreClockIn;
+import shop.alien.entity.store.UserLoginInfo;
 import shop.alien.entity.store.vo.StoreClockInVo;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreClockInService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
 
 @Slf4j
 @Api(tags = {"店铺打卡"})
@@ -88,9 +91,10 @@ public class StoreClockInController {
     @ApiOperationSupport(order = 6)
     @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "主键id", dataType = "Integer", paramType = "query")})
     @GetMapping("/getStoreClockInById")
-    public R<StoreClockInVo> getStoreClockInById(Integer id) {
-        log.info("StoreClockInController.getStoreClockInById?id={}", id);
-        return R.data(storeClockInService.getStoreClockInById(id));
+    public R<StoreClockInVo> getStoreClockInById(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo, Integer id) {
+        Integer userId = userLoginInfo != null ? userLoginInfo.getUserId() : null;
+        log.info("StoreClockInController.getStoreClockInById?userId={},id={}", userId, id);
+        return R.data(storeClockInService.getStoreClockInById(id, userId));
     }
 
 }

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

+ 322 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java

@@ -0,0 +1,322 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.StoreOrderDetail;
+import shop.alien.entity.store.StoreTableLog;
+import shop.alien.entity.store.dto.*;
+import shop.alien.mapper.StoreOrderDetailMapper;
+import shop.alien.mapper.StoreTableLogMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.SseService;
+import shop.alien.store.service.StoreOrderService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 订单管理控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"订单管理"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/order")
+@RequiredArgsConstructor
+public class StoreOrderController {
+
+    private final StoreOrderService orderService;
+    private final CartService cartService;
+    private final SseService sseService;
+    private final StoreOrderDetailMapper orderDetailMapper;
+    private final StoreTableLogMapper storeTableLogMapper;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @ApiOperation(value = "获取购物车", notes = "根据桌号ID获取购物车信息")
+    @GetMapping("/cart/{tableId}")
+    public R<CartDTO> getCart(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        try {
+            CartDTO cart = cartService.getCart(tableId);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("获取购物车失败: {}", e.getMessage(), e);
+            return R.fail("获取购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "添加商品到购物车", notes = "添加商品到购物车,并推送SSE消息")
+    @PostMapping("/cart/add")
+    public R<CartDTO> addCartItem(@Valid @RequestBody AddCartItemDTO dto) {
+        try {
+            CartDTO cart = cartService.addItem(dto);
+            // 推送购物车更新消息
+            sseService.pushCartUpdate(dto.getTableId(), cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("添加商品到购物车失败: {}", e.getMessage(), e);
+            return R.fail("添加商品到购物车失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "更新购物车商品数量", notes = "更新购物车中商品的数量,并推送SSE消息")
+    @PutMapping("/cart/update")
+    public R<CartDTO> updateCartItem(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId,
+            @ApiParam(value = "数量", required = true) @RequestParam Integer quantity) {
+        try {
+            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+            // 推送购物车更新消息
+            sseService.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("更新购物车商品数量失败: {}", e.getMessage(), e);
+            return R.fail("更新购物车商品数量失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "删除购物车商品", notes = "从购物车中删除商品,并推送SSE消息")
+    @DeleteMapping("/cart/remove")
+    public R<CartDTO> removeCartItem(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
+            @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId) {
+        try {
+            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+            // 推送购物车更新消息
+            sseService.pushCartUpdate(tableId, cart);
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("删除购物车商品失败: {}", e.getMessage(), e);
+            return R.fail("删除购物车商品失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "创建订单(下单)", notes = "从购物车创建订单,不立即支付")
+    @PostMapping("/create")
+    public R<shop.alien.entity.store.vo.OrderSuccessVO> createOrder(@Valid @RequestBody CreateOrderDTO dto) {
+        try {
+            // 限制备注长度
+            if (dto.getRemark() != null && dto.getRemark().length() > 30) {
+                dto.setRemark(dto.getRemark().substring(0, 30));
+            }
+
+            // 设置不立即支付
+            dto.setImmediatePay(0);
+            StoreOrder order = orderService.createOrder(dto);
+
+            // 转换为OrderSuccessVO
+            shop.alien.entity.store.vo.OrderSuccessVO vo = new shop.alien.entity.store.vo.OrderSuccessVO();
+            vo.setOrderId(order.getId());
+            vo.setOrderNo(order.getOrderNo());
+            vo.setTableNumber(order.getTableNumber());
+            vo.setDinerCount(order.getDinerCount());
+            vo.setOrderStatus(order.getOrderStatus());
+
+            // 查询门店信息
+            StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+            vo.setStoreName(storeInfo != null ? storeInfo.getStoreName() : null);
+
+            // 推送订单创建消息
+            sseService.pushCartUpdate(dto.getTableId(), order);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("创建订单失败: {}", e.getMessage(), e);
+            return R.fail("创建订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "加餐", notes = "在已有订单基础上添加菜品")
+    @PostMapping("/add-dish")
+    public R<StoreOrder> addDishToOrder(@Valid @RequestBody shop.alien.entity.store.dto.AddDishDTO dto) {
+        try {
+            StoreOrder order = orderService.addDishToOrder(dto.getOrderId(), dto.getCuisineId(), dto.getQuantity(), dto.getRemark());
+            return R.data(order);
+        } catch (Exception e) {
+            log.error("加餐失败: {}", e.getMessage(), e);
+            return R.fail("加餐失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "支付订单", notes = "支付订单,支付完成后订单状态变为已完成")
+    @PostMapping("/pay/{orderId}")
+    public R<Object> payOrder(
+            @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId,
+            @ApiParam(value = "支付方式(1:微信, 2:支付宝, 3:现金)", required = true) @RequestParam Integer payType) {
+        try {
+            StoreOrder order = orderService.getOrderById(orderId);
+            if (order == null) {
+                return R.fail("订单不存在");
+            }
+
+            if (order.getOrderStatus() != 0) {
+                return R.fail("订单状态不正确,无法支付");
+            }
+
+            // 如果是微信支付,调用微信支付接口
+            if (payType == 1) {
+                // TODO: 调用微信支付接口,返回支付参数
+                // 这里需要集成微信支付SDK,返回支付参数给前端拉起支付
+                // 支付成功后通过回调接口更新订单状态
+                // 暂时先更新订单状态为已支付(实际应该等支付回调)
+                order = orderService.payOrder(orderId, payType);
+                // 支付完成后,订单状态变为已完成
+                orderService.completeOrder(orderId);
+                return R.data(order);
+            } else {
+                // 其他支付方式(支付宝、现金)直接更新为已支付
+                order = orderService.payOrder(orderId, payType);
+                // 支付完成后,订单状态变为已完成
+                orderService.completeOrder(orderId);
+                return R.data(order);
+            }
+        } catch (Exception e) {
+            log.error("支付订单失败: {}", e.getMessage(), e);
+            return R.fail("支付订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "取消订单", notes = "取消订单")
+    @PostMapping("/cancel/{orderId}")
+    public R<Boolean> cancelOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            boolean result = orderService.cancelOrder(orderId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("取消订单失败: {}", e.getMessage(), e);
+            return R.fail("取消订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单详情", notes = "根据订单ID查询订单详情")
+    @GetMapping("/detail/{orderId}")
+    public R<StoreOrder> getOrderDetail(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            StoreOrder order = orderService.getOrderById(orderId);
+            if (order == null) {
+                return R.fail("订单不存在");
+            }
+            return R.data(order);
+        } catch (Exception e) {
+            log.error("查询订单详情失败: {}", e.getMessage(), e);
+            return R.fail("查询订单详情失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "查询订单明细", notes = "根据订单ID查询订单明细列表")
+    @GetMapping("/detail/list/{orderId}")
+    public R<List<StoreOrderDetail>> getOrderDetailList(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StoreOrderDetail> wrapper =
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+            wrapper.eq(StoreOrderDetail::getOrderId, orderId);
+            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+            wrapper.orderByDesc(StoreOrderDetail::getCreatedTime);
+            List<StoreOrderDetail> details = orderDetailMapper.selectList(wrapper);
+            return R.data(details);
+        } catch (Exception e) {
+            log.error("查询订单明细失败: {}", e.getMessage(), e);
+            return R.fail("查询订单明细失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表")
+    @GetMapping("/page")
+    public R<IPage<StoreOrder>> getOrderPage(
+            @ApiParam(value = "页码", required = true) @RequestParam(defaultValue = "1") Long current,
+            @ApiParam(value = "每页数量", required = true) @RequestParam(defaultValue = "10") Long size,
+            @ApiParam(value = "门店ID") @RequestParam(required = false) Integer storeId,
+            @ApiParam(value = "桌号ID") @RequestParam(required = false) Integer tableId,
+            @ApiParam(value = "订单状态") @RequestParam(required = false) Integer orderStatus) {
+        try {
+            Page<StoreOrder> page = new Page<>(current, size);
+            IPage<StoreOrder> result = orderService.getOrderPage(page, storeId, tableId, orderStatus);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("分页查询订单列表失败: {}", e.getMessage(), e);
+            return R.fail("分页查询订单列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "换桌", notes = "换桌并迁移购物车")
+    @PostMapping("/change-table")
+    public R<CartDTO> changeTable(@Valid @RequestBody ChangeTableDTO dto) {
+        try {
+            // 迁移购物车
+            CartDTO cart = cartService.migrateCart(dto.getFromTableId(), dto.getToTableId());
+
+            // 记录换桌日志
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+
+            // 查询桌号信息
+            StoreTable fromTable = storeTableMapper.selectById(dto.getFromTableId());
+            StoreTable toTable = storeTableMapper.selectById(dto.getToTableId());
+
+            StoreTableLog log = new StoreTableLog();
+            log.setStoreId(cart.getStoreId());
+            log.setFromTableId(dto.getFromTableId());
+            log.setFromTableNumber(fromTable != null ? fromTable.getTableNumber() : null);
+            log.setToTableId(dto.getToTableId());
+            log.setToTableNumber(toTable != null ? toTable.getTableNumber() : null);
+            log.setChangeReason(dto.getChangeReason());
+            log.setCreatedUserId(userId);
+            storeTableLogMapper.insert(log);
+
+            // 推送购物车更新消息到新桌号
+            sseService.pushCartUpdate(dto.getToTableId(), cart);
+
+            return R.data(cart);
+        } catch (Exception e) {
+            log.error("换桌失败: {}", e.getMessage(), e);
+            return R.fail("换桌失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "建立SSE连接", notes = "建立SSE连接,用于接收购物车更新消息")
+    @GetMapping(value = "/sse/{tableId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter createSseConnection(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        log.info("建立SSE连接, tableId={}", tableId);
+        return sseService.createConnection(tableId);
+    }
+
+    @ApiOperation(value = "完成订单", notes = "支付完成后调用,将订单状态改为已完成")
+    @PostMapping("/complete/{orderId}")
+    public R<Boolean> completeOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            boolean result = orderService.completeOrder(orderId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("完成订单失败: {}", e.getMessage(), e);
+            return R.fail("完成订单失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "更新订单优惠券", notes = "重新选择优惠券")
+    @PostMapping("/update-coupon")
+    public R<StoreOrder> updateOrderCoupon(@Valid @RequestBody shop.alien.entity.store.dto.UpdateOrderCouponDTO dto) {
+        try {
+            StoreOrder order = orderService.updateOrderCoupon(dto.getOrderId(), dto.getCouponId());
+            return R.data(order);
+        } catch (Exception e) {
+            log.error("更新订单优惠券失败: {}", e.getMessage(), e);
+            return R.fail("更新订单优惠券失败: " + e.getMessage());
+        }
+    }
+}

+ 8 - 4
alien-store/src/main/java/shop/alien/store/controller/StoreStaffReviewController.java

@@ -57,17 +57,21 @@ public class StoreStaffReviewController {
     @ApiOperationSupport(order = 2)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "reviewId", value = "评价ID", dataTypeClass = Integer.class, paramType = "query", required = true),
-            @ApiImplicitParam(name = "currentUserId", value = "当前用户ID(用于判断是否已点赞)", dataTypeClass = Integer.class, paramType = "query", required = false)
+            @ApiImplicitParam(name = "currentUserId", value = "当前用户ID(用于判断是否已点赞)", dataTypeClass = Integer.class, paramType = "query", required = false),
+            @ApiImplicitParam(name = "longitude", value = "用户经度(用于计算距离,可选)", dataTypeClass = Double.class, paramType = "query", required = false),
+            @ApiImplicitParam(name = "latitude", value = "用户纬度(用于计算距离,可选)", dataTypeClass = Double.class, paramType = "query", required = false)
     })
     @GetMapping("/detail/reviewId")
     public R<StoreStaffReviewDetailVo> getReviewDetail(
             @RequestParam Integer reviewId,
-            @RequestParam(required = false) Integer currentUserId) {
-        log.info("获取评价详情, reviewId={}, currentUserId={}", reviewId, currentUserId);
+            @RequestParam(required = false) Integer currentUserId,
+            @RequestParam(required = false) Double longitude,
+            @RequestParam(required = false) Double latitude) {
+        log.info("获取评价详情, reviewId={}, currentUserId={}, longitude={}, latitude={}", reviewId, currentUserId, longitude, latitude);
         if (reviewId == null) {
             return R.fail("评价ID不能为空");
         }
-        return storeStaffReviewService.getReviewDetail(reviewId, currentUserId);
+        return storeStaffReviewService.getReviewDetail(reviewId, currentUserId, longitude, latitude);
     }
 
     @ApiOperation("点赞评价")

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

+ 88 - 0
alien-store/src/main/java/shop/alien/store/service/CartService.java

@@ -0,0 +1,88 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+
+/**
+ * 购物车服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface CartService {
+
+    /**
+     * 获取购物车
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO getCart(Integer tableId);
+
+    /**
+     * 添加商品到购物车
+     *
+     * @param dto 添加购物车商品DTO
+     * @return 购物车信息
+     */
+    CartDTO addItem(AddCartItemDTO dto);
+
+    /**
+     * 更新购物车商品数量
+     *
+     * @param tableId   桌号ID
+     * @param cuisineId 菜品ID
+     * @param quantity  数量
+     * @return 购物车信息
+     */
+    CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity);
+
+    /**
+     * 删除购物车商品
+     *
+     * @param tableId   桌号ID
+     * @param cuisineId 菜品ID
+     * @return 购物车信息
+     */
+    CartDTO removeItem(Integer tableId, Integer cuisineId);
+
+    /**
+     * 清空购物车
+     *
+     * @param tableId 桌号ID
+     */
+    void clearCart(Integer tableId);
+
+    /**
+     * 换桌时迁移购物车
+     *
+     * @param fromTableId 原桌号ID
+     * @param toTableId   目标桌号ID
+     * @return 迁移后的购物车信息
+     */
+    CartDTO migrateCart(Integer fromTableId, Integer toTableId);
+
+    /**
+     * 检查桌号是否已使用优惠券
+     *
+     * @param tableId 桌号ID
+     * @return true-已使用, false-未使用
+     */
+    boolean hasUsedCoupon(Integer tableId);
+
+    /**
+     * 标记桌号已使用优惠券
+     *
+     * @param tableId  桌号ID
+     * @param couponId 优惠券ID
+     */
+    void markCouponUsed(Integer tableId, Integer couponId);
+
+    /**
+     * 清除桌号优惠券使用标记(换桌时使用)
+     *
+     * @param tableId 桌号ID
+     */
+    void clearCouponUsed(Integer tableId);
+}

+ 134 - 0
alien-store/src/main/java/shop/alien/store/service/DiningService.java

@@ -0,0 +1,134 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.*;
+import shop.alien.entity.store.dto.CartDTO;
+
+import java.util.List;
+
+/**
+ * 点餐服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface DiningService {
+
+    /**
+     * 获取点餐页面信息
+     *
+     * @param tableId 桌号ID
+     * @param dinerCount 就餐人数
+     * @return 点餐页面信息
+     */
+    DiningPageInfoVO getDiningPageInfo(Integer tableId, Integer dinerCount);
+
+    /**
+     * 搜索菜品(模糊搜索,限10字)
+     *
+     * @param storeId 门店ID
+     * @param keyword 搜索关键词
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @return 菜品列表
+     */
+    List<CuisineListVO> searchCuisines(Integer storeId, String keyword, Integer tableId);
+
+    /**
+     * 根据分类获取菜品列表
+     *
+     * @param storeId 门店ID
+     * @param categoryId 分类ID(可为空,查询所有)
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @param page 页码(默认1)
+     * @param size 每页数量(默认12)
+     * @return 菜品列表
+     */
+    List<CuisineListVO> getCuisinesByCategory(Integer storeId, Integer categoryId, Integer tableId, Integer page, Integer size);
+
+    /**
+     * 获取菜品详情
+     *
+     * @param cuisineId 菜品ID
+     * @param tableId 桌号ID(用于获取购物车数量)
+     * @return 菜品详情
+     */
+    CuisineDetailVO getCuisineDetail(Integer cuisineId, Integer tableId);
+
+    /**
+     * 获取可领取的优惠券列表
+     *
+     * @param storeId 门店ID
+     * @param userId 用户ID
+     * @return 可领取优惠券列表
+     */
+    List<AvailableCouponVO> getAvailableCoupons(Integer storeId, Integer userId);
+
+    /**
+     * 领取优惠券
+     *
+     * @param couponId 优惠券ID
+     * @param userId 用户ID
+     * @return 是否成功
+     */
+    boolean receiveCoupon(Integer couponId, Integer userId);
+
+    /**
+     * 获取订单确认页面信息
+     *
+     * @param tableId 桌号ID
+     * @param dinerCount 就餐人数
+     * @param userId 用户ID
+     * @return 订单确认页面信息
+     */
+    OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId);
+
+    /**
+     * 锁定订单(防止多人同时下单)
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 解锁订单
+     *
+     * @param tableId 桌号ID
+     * @param userId 用户ID
+     */
+    void unlockOrder(Integer tableId, Integer userId);
+
+    /**
+     * 检查订单是否被锁定
+     *
+     * @param tableId 桌号ID
+     * @return 锁定用户ID,如果未锁定返回null
+     */
+    Integer checkOrderLock(Integer tableId);
+
+    /**
+     * 获取订单结算确认页面信息
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     * @return 订单结算确认页面信息
+     */
+    shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId);
+
+    /**
+     * 锁定订单结算(防止多人同时结算)
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     * @return 是否锁定成功
+     */
+    boolean lockSettlement(Integer orderId, Integer userId);
+
+    /**
+     * 解锁订单结算
+     *
+     * @param orderId 订单ID
+     * @param userId 用户ID
+     */
+    void unlockSettlement(Integer orderId, Integer userId);
+}

+ 33 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java

@@ -54,6 +54,8 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
 
     private final CommonCommentMapper commonCommentMapper;
 
+    private final CommonRatingService commonRatingService;
+
     public int addLiulanCount(String id) {
         LambdaUpdateWrapper<LifeUserDynamics> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
         lambdaUpdateWrapper.eq(LifeUserDynamics::getId, id);
@@ -157,13 +159,38 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
         // 查询动态数据并按类型过滤
         List<LifeUserDynamicsVo> lifeUserDynamicsVoList = lifeUserDynamicsMapper.getLifeUserDynamicsList();
 
+        // 店铺id
+        // TODO 可以优化小驴:优化方案直接sql分组查询评价数量,避免循环查询
+        List<String> storeUserIdList = new ArrayList<>();
+        Map<String, Integer> commitCountMap = new HashMap<>();
         //对lifeUserDynamicsVoList数据进行处理,当type为2的时候,把userName的数值赋值到storeName
         lifeUserDynamicsVoList.forEach(item -> {
             if (item.getType().equals("2")) {
                 item.setStoreName(item.getUserName());
+                storeUserIdList.add(item.getStoreUserId());
             }
         });
 
+        for (String storeId : storeUserIdList) {
+            Integer totalCount = 0;
+            double storeScore;
+            Object ratingObj =  commonRatingService.getRatingCount(Integer.parseInt(storeId), 1);
+            if (ratingObj != null) {
+                Map<String, Object> ratingMap = (Map<String, Object>) ratingObj;
+                Object totalCountObj = ratingMap.get("totalCount");
+                if (totalCountObj != null) {
+                    // 安全转换为整数
+                    try {
+                        totalCount = Integer.parseInt(totalCountObj.toString().trim());
+                    } catch (NumberFormatException e) {
+                        totalCount = 0; // 转换失败时默认值
+                    }
+                } else {
+                    totalCount = 0;
+                }
+            }
+            commitCountMap.put(storeId, totalCount);
+        }
         if (!StringUtils.isEmpty(type)) {
             lifeUserDynamicsVoList = lifeUserDynamicsVoList.stream().filter(item -> type.equals(item.getType())).collect(Collectors.toList());
         }
@@ -207,6 +234,12 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
         // 设置动态对象的状态信息:是否关注对方、是否被关注、是否点赞及评论数量
         // 设置.imagePath。视频为mp4+jpg格式,图片为jpg/png格式。
         for (LifeUserDynamicsVo vo : lifeUserDynamicsVoList) {
+            // 设置评价数量
+            if (commitCountMap.containsKey(vo.getStoreUserId())) {
+                vo.setRatingCount(commitCountMap.get(vo.getStoreUserId()).toString());
+            } else {
+                vo.setRatingCount("0");
+            }
             if (followList.contains(vo.getPhoneId())) {
                 vo.setIsFollowThis("1");
             } else {

+ 35 - 0
alien-store/src/main/java/shop/alien/store/service/SseService.java

@@ -0,0 +1,35 @@
+package shop.alien.store.service;
+
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * SSE推送服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface SseService {
+
+    /**
+     * 创建SSE连接
+     *
+     * @param tableId 桌号ID
+     * @return SSE连接对象
+     */
+    SseEmitter createConnection(Integer tableId);
+
+    /**
+     * 推送购物车更新消息
+     *
+     * @param tableId 桌号ID
+     * @param message 消息内容
+     */
+    void pushCartUpdate(Integer tableId, Object message);
+
+    /**
+     * 关闭SSE连接
+     *
+     * @param tableId 桌号ID
+     */
+    void closeConnection(Integer tableId);
+}

+ 2 - 1
alien-store/src/main/java/shop/alien/store/service/StoreClockInService.java

@@ -40,7 +40,8 @@ public interface StoreClockInService extends IService<StoreClockIn> {
      * 根据id查询打卡记录
      *
      * @param id 主键id
+     * @param userId 当前登录用户ID(用于查询收藏状态)
      * @return 打卡记录
      */
-    StoreClockInVo getStoreClockInById(Integer id);
+    StoreClockInVo getStoreClockInById(Integer id, Integer userId);
 }

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

+ 95 - 0
alien-store/src/main/java/shop/alien/store/service/StoreOrderService.java

@@ -0,0 +1,95 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+
+/**
+ * 订单服务接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreOrderService {
+
+    /**
+     * 创建订单
+     *
+     * @param dto 创建订单DTO
+     * @return 订单信息
+     */
+    StoreOrder createOrder(CreateOrderDTO dto);
+
+    /**
+     * 支付订单
+     *
+     * @param orderId 订单ID
+     * @param payType 支付方式
+     * @return 订单信息
+     */
+    StoreOrder payOrder(Integer orderId, Integer payType);
+
+    /**
+     * 取消订单
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean cancelOrder(Integer orderId);
+
+    /**
+     * 根据订单号查询订单
+     *
+     * @param orderNo 订单号
+     * @return 订单信息
+     */
+    StoreOrder getOrderByOrderNo(String orderNo);
+
+    /**
+     * 根据ID查询订单
+     *
+     * @param orderId 订单ID
+     * @return 订单信息
+     */
+    StoreOrder getOrderById(Integer orderId);
+
+    /**
+     * 分页查询订单列表
+     *
+     * @param page     分页参数
+     * @param storeId  门店ID
+     * @param tableId  桌号ID
+     * @param orderStatus 订单状态
+     * @return 订单分页列表
+     */
+    IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus);
+
+    /**
+     * 加餐(在已有订单基础上添加菜品)
+     *
+     * @param orderId 订单ID
+     * @param cuisineId 菜品ID
+     * @param quantity 数量
+     * @param remark 备注
+     * @return 订单信息
+     */
+    StoreOrder addDishToOrder(Integer orderId, Integer cuisineId, Integer quantity, String remark);
+
+    /**
+     * 完成订单(支付完成后调用)
+     *
+     * @param orderId 订单ID
+     * @return 是否成功
+     */
+    boolean completeOrder(Integer orderId);
+
+    /**
+     * 更新订单优惠券
+     *
+     * @param orderId 订单ID
+     * @param couponId 优惠券ID(可为空,表示不使用优惠券)
+     * @return 订单信息
+     */
+    StoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
+}

+ 3 - 1
alien-store/src/main/java/shop/alien/store/service/StoreStaffReviewService.java

@@ -31,9 +31,11 @@ public interface StoreStaffReviewService extends IService<StoreStaffReview> {
      *
      * @param reviewId 评价ID
      * @param currentUserId 当前用户ID(用于判断是否已点赞,可为null)
+     * @param longitude 用户经度(用于计算距离,可为null)
+     * @param latitude 用户纬度(用于计算距离,可为null)
      * @return R<StoreStaffReviewDetailVo>
      */
-    R<StoreStaffReviewDetailVo> getReviewDetail(Integer reviewId, Integer currentUserId);
+    R<StoreStaffReviewDetailVo> getReviewDetail(Integer reviewId, Integer currentUserId, Double longitude, Double latitude);
 
     /**
      * 点赞评价

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

+ 549 - 0
alien-store/src/main/java/shop/alien/store/service/impl/CartServiceImpl.java

@@ -0,0 +1,549 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreCart;
+import shop.alien.entity.store.StoreCouponUsage;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+import shop.alien.mapper.StoreCartMapper;
+import shop.alien.mapper.StoreCouponUsageMapper;
+import shop.alien.mapper.StoreCuisineMapper;
+import shop.alien.mapper.StoreTableMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.CartService;
+import shop.alien.util.common.JwtUtil;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 购物车服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CartServiceImpl implements CartService {
+
+    private static final String CART_KEY_PREFIX = "cart:table:";
+    private static final String COUPON_USED_KEY_PREFIX = "coupon:used:table:";
+    private static final int CART_EXPIRE_SECONDS = 24 * 60 * 60; // 24小时过期
+
+    private final BaseRedisService baseRedisService;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreCartMapper storeCartMapper;
+    private final StoreCouponUsageMapper storeCouponUsageMapper;
+
+    @Override
+    public CartDTO getCart(Integer tableId) {
+        log.info("获取购物车, tableId={}", tableId);
+        String cartKey = CART_KEY_PREFIX + tableId;
+        String cartJson = baseRedisService.getString(cartKey);
+
+        CartDTO cart = new CartDTO();
+        cart.setTableId(tableId);
+
+        // 查询桌号信息
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            cart.setTableNumber(table.getTableNumber());
+            cart.setStoreId(table.getStoreId());
+        }
+
+        if (StringUtils.hasText(cartJson)) {
+            try {
+                JSONObject cartObj = JSON.parseObject(cartJson);
+                List<CartItemDTO> items = cartObj.getList("items", CartItemDTO.class);
+                if (items != null) {
+                    cart.setItems(items);
+                    // 计算总金额和总数量
+                    BigDecimal totalAmount = items.stream()
+                            .map(CartItemDTO::getSubtotalAmount)
+                            .reduce(BigDecimal.ZERO, BigDecimal::add);
+                    Integer totalQuantity = items.stream()
+                            .mapToInt(CartItemDTO::getQuantity)
+                            .sum();
+                    cart.setTotalAmount(totalAmount);
+                    cart.setTotalQuantity(totalQuantity);
+                } else {
+                    cart.setItems(new ArrayList<>());
+                    cart.setTotalAmount(BigDecimal.ZERO);
+                    cart.setTotalQuantity(0);
+                }
+            } catch (Exception e) {
+                log.error("解析购物车数据失败: {}", e.getMessage(), e);
+                cart.setItems(new ArrayList<>());
+                cart.setTotalAmount(BigDecimal.ZERO);
+                cart.setTotalQuantity(0);
+            }
+        } else {
+            // Redis中没有,尝试从数据库加载
+            cart = loadCartFromDatabase(tableId);
+        }
+
+        return cart;
+    }
+
+    /**
+     * 从数据库加载购物车
+     */
+    private CartDTO loadCartFromDatabase(Integer tableId) {
+        log.info("从数据库加载购物车, tableId={}", tableId);
+        CartDTO cart = new CartDTO();
+        cart.setTableId(tableId);
+        cart.setItems(new ArrayList<>());
+        cart.setTotalAmount(BigDecimal.ZERO);
+        cart.setTotalQuantity(0);
+
+        // 查询桌号信息
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            cart.setTableNumber(table.getTableNumber());
+            cart.setStoreId(table.getStoreId());
+        }
+
+        // 从数据库查询购物车数据
+        LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCart::getTableId, tableId);
+        wrapper.eq(StoreCart::getDeleteFlag, 0);
+        List<StoreCart> cartList = storeCartMapper.selectList(wrapper);
+
+        if (cartList != null && !cartList.isEmpty()) {
+            List<CartItemDTO> items = cartList.stream().map(cartItem -> {
+                CartItemDTO item = new CartItemDTO();
+                item.setCuisineId(cartItem.getCuisineId());
+                item.setCuisineName(cartItem.getCuisineName());
+                item.setCuisineImage(cartItem.getCuisineImage());
+                item.setUnitPrice(cartItem.getUnitPrice());
+                item.setQuantity(cartItem.getQuantity());
+                item.setSubtotalAmount(cartItem.getSubtotalAmount());
+                item.setAddUserId(cartItem.getAddUserId());
+                item.setAddUserPhone(cartItem.getAddUserPhone());
+                item.setRemark(cartItem.getRemark());
+                return item;
+            }).collect(Collectors.toList());
+
+            cart.setItems(items);
+            BigDecimal totalAmount = items.stream()
+                    .map(CartItemDTO::getSubtotalAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            Integer totalQuantity = items.stream()
+                    .mapToInt(CartItemDTO::getQuantity)
+                    .sum();
+            cart.setTotalAmount(totalAmount);
+            cart.setTotalQuantity(totalQuantity);
+
+            // 同步到Redis
+            saveCartToRedis(cart);
+        }
+
+        return cart;
+    }
+
+    @Override
+    public CartDTO addItem(AddCartItemDTO dto) {
+        log.info("添加商品到购物车, dto={}", dto);
+        // 验证桌号
+        StoreTable table = storeTableMapper.selectById(dto.getTableId());
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        // 验证菜品
+        StoreCuisine cuisine = storeCuisineMapper.selectById(dto.getCuisineId());
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        if (cuisine.getShelfStatus() != 1) {
+            throw new RuntimeException("菜品已下架");
+        }
+
+        // 获取当前用户信息
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+        String userPhone = userInfo != null ? userInfo.getString("phone") : null;
+
+        // 获取购物车
+        CartDTO cart = getCart(dto.getTableId());
+
+        // 查找是否已存在该商品
+        List<CartItemDTO> items = cart.getItems();
+        CartItemDTO existingItem = items.stream()
+                .filter(item -> item.getCuisineId().equals(dto.getCuisineId()))
+                .findFirst()
+                .orElse(null);
+
+        if (existingItem != null) {
+            // 更新数量
+            existingItem.setQuantity(existingItem.getQuantity() + dto.getQuantity());
+            existingItem.setSubtotalAmount(existingItem.getUnitPrice()
+                    .multiply(BigDecimal.valueOf(existingItem.getQuantity())));
+            if (StringUtils.hasText(dto.getRemark())) {
+                existingItem.setRemark(dto.getRemark());
+            }
+        } else {
+            // 添加新商品
+            CartItemDTO newItem = new CartItemDTO();
+            newItem.setCuisineId(cuisine.getId());
+            newItem.setCuisineName(cuisine.getName());
+            newItem.setCuisineType(cuisine.getCuisineType());
+            newItem.setCuisineImage(cuisine.getImages());
+            newItem.setUnitPrice(cuisine.getTotalPrice());
+            newItem.setQuantity(dto.getQuantity());
+            newItem.setSubtotalAmount(cuisine.getTotalPrice()
+                    .multiply(BigDecimal.valueOf(dto.getQuantity())));
+            newItem.setAddUserId(userId);
+            newItem.setAddUserPhone(userPhone);
+            newItem.setRemark(dto.getRemark());
+            items.add(newItem);
+        }
+
+        // 重新计算总金额和总数量
+        BigDecimal totalAmount = items.stream()
+                .map(CartItemDTO::getSubtotalAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Integer totalQuantity = items.stream()
+                .mapToInt(CartItemDTO::getQuantity)
+                .sum();
+        cart.setTotalAmount(totalAmount);
+        cart.setTotalQuantity(totalQuantity);
+
+        // 保存到Redis和数据库(双写策略)
+        saveCart(cart);
+
+        return cart;
+    }
+
+    @Override
+    public CartDTO updateItemQuantity(Integer tableId, Integer cuisineId, Integer quantity) {
+        log.info("更新购物车商品数量, tableId={}, cuisineId={}, quantity={}", tableId, cuisineId, quantity);
+        if (quantity <= 0) {
+            return removeItem(tableId, cuisineId);
+        }
+
+        CartDTO cart = getCart(tableId);
+        List<CartItemDTO> items = cart.getItems();
+        CartItemDTO item = items.stream()
+                .filter(i -> i.getCuisineId().equals(cuisineId))
+                .findFirst()
+                .orElse(null);
+
+        if (item != null) {
+            item.setQuantity(quantity);
+            item.setSubtotalAmount(item.getUnitPrice()
+                    .multiply(BigDecimal.valueOf(quantity)));
+
+            // 重新计算总金额和总数量
+            BigDecimal totalAmount = items.stream()
+                    .map(CartItemDTO::getSubtotalAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            Integer totalQuantity = items.stream()
+                    .mapToInt(CartItemDTO::getQuantity)
+                    .sum();
+            cart.setTotalAmount(totalAmount);
+            cart.setTotalQuantity(totalQuantity);
+
+            saveCart(cart);
+        }
+
+        return cart;
+    }
+
+    @Override
+    public CartDTO removeItem(Integer tableId, Integer cuisineId) {
+        log.info("删除购物车商品, tableId={}, cuisineId={}", tableId, cuisineId);
+        CartDTO cart = getCart(tableId);
+        List<CartItemDTO> items = cart.getItems();
+        items.removeIf(item -> item.getCuisineId().equals(cuisineId));
+
+        // 重新计算总金额和总数量
+        BigDecimal totalAmount = items.stream()
+                .map(CartItemDTO::getSubtotalAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Integer totalQuantity = items.stream()
+                .mapToInt(CartItemDTO::getQuantity)
+                .sum();
+        cart.setTotalAmount(totalAmount);
+        cart.setTotalQuantity(totalQuantity);
+
+        saveCart(cart);
+        return cart;
+    }
+
+    @Override
+    public void clearCart(Integer tableId) {
+        log.info("清空购物车, tableId={}", tableId);
+        // 清空Redis
+        String cartKey = CART_KEY_PREFIX + tableId;
+        baseRedisService.delete(cartKey);
+
+        // 清空数据库(逻辑删除)
+        LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCart::getTableId, tableId);
+        wrapper.eq(StoreCart::getDeleteFlag, 0);
+        List<StoreCart> cartList = storeCartMapper.selectList(wrapper);
+        if (cartList != null && !cartList.isEmpty()) {
+            for (StoreCart cart : cartList) {
+                cart.setDeleteFlag(1);
+                cart.setUpdatedTime(new Date());
+                storeCartMapper.updateById(cart);
+            }
+        }
+
+        // 更新桌号表的购物车统计
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            table.setCartItemCount(0);
+            table.setCartTotalAmount(BigDecimal.ZERO);
+            storeTableMapper.updateById(table);
+        }
+    }
+
+    @Override
+    public CartDTO migrateCart(Integer fromTableId, Integer toTableId) {
+        log.info("迁移购物车, fromTableId={}, toTableId={}", fromTableId, toTableId);
+        // 获取原购物车
+        CartDTO fromCart = getCart(fromTableId);
+
+        // 验证目标桌号
+        StoreTable toTable = storeTableMapper.selectById(toTableId);
+        if (toTable == null) {
+            throw new RuntimeException("目标桌号不存在");
+        }
+
+        // 获取目标购物车
+        CartDTO toCart = getCart(toTableId);
+
+        // 合并购物车(如果目标桌号已有商品,则合并)
+        List<CartItemDTO> mergedItems = new ArrayList<>(toCart.getItems());
+        for (CartItemDTO fromItem : fromCart.getItems()) {
+            CartItemDTO existingItem = mergedItems.stream()
+                    .filter(item -> item.getCuisineId().equals(fromItem.getCuisineId()))
+                    .findFirst()
+                    .orElse(null);
+
+            if (existingItem != null) {
+                // 合并数量
+                existingItem.setQuantity(existingItem.getQuantity() + fromItem.getQuantity());
+                existingItem.setSubtotalAmount(existingItem.getUnitPrice()
+                        .multiply(BigDecimal.valueOf(existingItem.getQuantity())));
+            } else {
+                mergedItems.add(fromItem);
+            }
+        }
+
+        toCart.setItems(mergedItems);
+        toCart.setTableId(toTableId);
+        toCart.setTableNumber(toTable.getTableNumber());
+        toCart.setStoreId(toTable.getStoreId());
+
+        // 重新计算总金额和总数量
+        BigDecimal totalAmount = mergedItems.stream()
+                .map(CartItemDTO::getSubtotalAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Integer totalQuantity = mergedItems.stream()
+                .mapToInt(CartItemDTO::getQuantity)
+                .sum();
+        toCart.setTotalAmount(totalAmount);
+        toCart.setTotalQuantity(totalQuantity);
+
+        // 保存目标购物车
+        saveCart(toCart);
+
+        // 清空原购物车
+        clearCart(fromTableId);
+
+        // 迁移优惠券使用标记
+        if (hasUsedCoupon(fromTableId)) {
+            String couponUsedKey = COUPON_USED_KEY_PREFIX + fromTableId;
+            String couponId = baseRedisService.getString(couponUsedKey);
+            if (StringUtils.hasText(couponId)) {
+                markCouponUsed(toTableId, Integer.parseInt(couponId));
+                clearCouponUsed(fromTableId);
+            }
+        }
+
+        return toCart;
+    }
+
+    @Override
+    public boolean hasUsedCoupon(Integer tableId) {
+        // 先查Redis
+        String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
+        String couponId = baseRedisService.getString(couponUsedKey);
+        if (StringUtils.hasText(couponId)) {
+            return true;
+        }
+
+        // Redis中没有,查数据库
+        LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCouponUsage::getTableId, tableId);
+        wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+        wrapper.in(StoreCouponUsage::getUsageStatus, 0, 1, 2); // 已标记使用、已下单、已支付
+        wrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+        wrapper.last("LIMIT 1");
+        StoreCouponUsage usage = storeCouponUsageMapper.selectOne(wrapper);
+        return usage != null;
+    }
+
+    @Override
+    public void markCouponUsed(Integer tableId, Integer couponId) {
+        // 保存到Redis
+        String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
+        baseRedisService.setString(couponUsedKey, String.valueOf(couponId), (long) CART_EXPIRE_SECONDS);
+
+        // 保存到数据库
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            log.warn("桌号不存在, tableId={}", tableId);
+            return;
+        }
+
+        // 检查是否已存在
+        LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCouponUsage::getTableId, tableId);
+        wrapper.eq(StoreCouponUsage::getCouponId, couponId);
+        wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+        StoreCouponUsage existing = storeCouponUsageMapper.selectOne(wrapper);
+
+        if (existing == null) {
+            StoreCouponUsage usage = new StoreCouponUsage();
+            usage.setTableId(tableId);
+            usage.setStoreId(table.getStoreId());
+            usage.setCouponId(couponId);
+            usage.setUsageStatus(0); // 已标记使用
+            usage.setCreatedTime(new Date());
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                usage.setCreatedUserId(userInfo.getInteger("userId"));
+            }
+            storeCouponUsageMapper.insert(usage);
+        }
+
+        // 更新桌号表的优惠券ID
+        table.setCurrentCouponId(couponId);
+        storeTableMapper.updateById(table);
+    }
+
+    @Override
+    public void clearCouponUsed(Integer tableId) {
+        // 清空Redis
+        String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
+        baseRedisService.delete(couponUsedKey);
+
+        // 更新数据库(逻辑删除未下单的记录)
+        LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCouponUsage::getTableId, tableId);
+        wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+        wrapper.eq(StoreCouponUsage::getUsageStatus, 0); // 只删除已标记使用但未下单的
+        List<StoreCouponUsage> usageList = storeCouponUsageMapper.selectList(wrapper);
+        if (usageList != null && !usageList.isEmpty()) {
+            for (StoreCouponUsage usage : usageList) {
+                usage.setDeleteFlag(1);
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            }
+        }
+
+        // 更新桌号表的优惠券ID
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table != null) {
+            table.setCurrentCouponId(null);
+            storeTableMapper.updateById(table);
+        }
+    }
+
+    /**
+     * 保存购物车到Redis和数据库(双写策略)
+     */
+    private void saveCart(CartDTO cart) {
+        // 保存到Redis
+        saveCartToRedis(cart);
+
+        // 保存到数据库
+        saveCartToDatabase(cart);
+    }
+
+    /**
+     * 保存购物车到Redis
+     */
+    private void saveCartToRedis(CartDTO cart) {
+        String cartKey = CART_KEY_PREFIX + cart.getTableId();
+        String cartJson = JSON.toJSONString(cart);
+        baseRedisService.setString(cartKey, cartJson, (long) CART_EXPIRE_SECONDS);
+    }
+
+    /**
+     * 保存购物车到数据库
+     */
+    private void saveCartToDatabase(CartDTO cart) {
+        try {
+            // 先删除该桌号的所有购物车记录(逻辑删除)
+            LambdaQueryWrapper<StoreCart> deleteWrapper = new LambdaQueryWrapper<>();
+            deleteWrapper.eq(StoreCart::getTableId, cart.getTableId());
+            deleteWrapper.eq(StoreCart::getDeleteFlag, 0);
+            List<StoreCart> existingCarts = storeCartMapper.selectList(deleteWrapper);
+            if (existingCarts != null && !existingCarts.isEmpty()) {
+                Date now = new Date();
+                for (StoreCart existing : existingCarts) {
+                    existing.setDeleteFlag(1);
+                    existing.setUpdatedTime(now);
+                    storeCartMapper.updateById(existing);
+                }
+            }
+
+            // 插入新的购物车记录
+            if (cart.getItems() != null && !cart.getItems().isEmpty()) {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+                Date now = new Date();
+
+                for (CartItemDTO item : cart.getItems()) {
+                    StoreCart storeCart = new StoreCart();
+                    storeCart.setTableId(cart.getTableId());
+                    storeCart.setStoreId(cart.getStoreId());
+                    storeCart.setCuisineId(item.getCuisineId());
+                    storeCart.setCuisineName(item.getCuisineName());
+                    storeCart.setCuisineImage(item.getCuisineImage());
+                    storeCart.setUnitPrice(item.getUnitPrice());
+                    storeCart.setQuantity(item.getQuantity());
+                    storeCart.setSubtotalAmount(item.getSubtotalAmount());
+                    storeCart.setAddUserId(item.getAddUserId());
+                    storeCart.setAddUserPhone(item.getAddUserPhone());
+                    storeCart.setRemark(item.getRemark());
+                    storeCart.setDeleteFlag(0);
+                    storeCart.setCreatedTime(now);
+                    storeCart.setCreatedUserId(userId);
+                    storeCart.setUpdatedTime(now);
+                    storeCartMapper.insert(storeCart);
+                }
+            }
+
+            // 更新桌号表的购物车统计
+            StoreTable table = storeTableMapper.selectById(cart.getTableId());
+            if (table != null) {
+                table.setCartItemCount(cart.getTotalQuantity());
+                table.setCartTotalAmount(cart.getTotalAmount());
+                storeTableMapper.updateById(table);
+            }
+        } catch (Exception e) {
+            log.error("保存购物车到数据库失败: {}", e.getMessage(), e);
+            // 数据库保存失败不影响Redis,继续执行
+        }
+    }
+}

+ 7 - 3
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -101,7 +101,10 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             int i = this.save(commonRating) ? 0 : 1;
             // 一次遍历完成分类,避免多次流式处理
             Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(Arrays.asList(commonRating.getImageUrls().split(",")));
-            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), urlCategoryMap.get("image"));
+            AiContentModerationUtil.AuditResult auditResult = new AiContentModerationUtil.AuditResult(true, "");
+            if( StringUtils.isNotEmpty(commonRating.getContent()) || urlCategoryMap.get("image").size() > 0){
+                auditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), urlCategoryMap.get("image"));
+            }
             if (!auditResult.isPassed()) {
                 // 审核不通过
                 CommonRating rating = this.getById(commonRating.getId());
@@ -339,6 +342,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(CommonRating::getBusinessId, businessId);
         wrapper.eq(CommonRating::getBusinessType, businessType);
+        wrapper.eq(CommonRating::getAuditStatus, 1);
         wrapper.eq(CommonRating::getIsShow, 1);
         List<CommonRating> commonRatings = commonRatingMapper.selectList(wrapper);
         List<Long> collect = commonRatings.stream().map(x -> x.getId()).collect(Collectors.toList());
@@ -381,7 +385,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             AtomicReference<Integer> count = new AtomicReference<>(0);
             count.updateAndGet(v -> v + collect.size());
             // 1.查询评价的评论的记录
-            LambdaQueryWrapper<CommonComment> commentWrapper = new LambdaQueryWrapper<CommonComment>()
+/*            LambdaQueryWrapper<CommonComment> commentWrapper = new LambdaQueryWrapper<CommonComment>()
                     .eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
                     .in(CommonComment::getSourceId, collect);
             List<Long> collect1 = commonCommentMapper.selectList(commentWrapper).stream().map(x -> x.getId()).collect(Collectors.toList());
@@ -390,7 +394,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 // 2.在再评论的表中查询评论的回复(根评论)
                 Integer allCommentsOnCommentsNum = getAllCommentsOnCommentsNum(collect1);
                 count.updateAndGet(v -> v + allCommentsOnCommentsNum);
-            }
+            }*/
             ratingCount.put("totalCount", count.get());
 
             // 3用户图片

+ 569 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DiningServiceImpl.java

@@ -0,0 +1,569 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
+import shop.alien.entity.store.vo.*;
+import shop.alien.mapper.*;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.DiningService;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 点餐服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DiningServiceImpl implements DiningService {
+
+    private static final String ORDER_LOCK_KEY_PREFIX = "order:lock:table:";
+    private static final int ORDER_LOCK_EXPIRE_SECONDS = 300; // 5分钟过期
+
+    private final StoreTableMapper storeTableMapper;
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
+    private final StoreCuisineComboMapper storeCuisineComboMapper;
+    private final StoreOrderDetailMapper storeOrderDetailMapper;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    private final CartService cartService;
+    private final BaseRedisService baseRedisService;
+    private final shop.alien.store.service.StoreOrderService storeOrderService;
+    private final shop.alien.mapper.StoreOrderMapper storeOrderMapper;
+
+    @Override
+    public DiningPageInfoVO getDiningPageInfo(Integer tableId, Integer dinerCount) {
+        log.info("获取点餐页面信息, tableId={}, dinerCount={}", tableId, dinerCount);
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        StoreInfo storeInfo = storeInfoMapper.selectById(table.getStoreId());
+        if (storeInfo == null) {
+            throw new RuntimeException("门店不存在");
+        }
+
+        DiningPageInfoVO vo = new DiningPageInfoVO();
+        vo.setStoreName(storeInfo.getStoreName());
+        vo.setTableNumber(table.getTableNumber());
+        vo.setDinerCount(dinerCount);
+        vo.setStoreId(table.getStoreId());
+        vo.setTableId(tableId);
+
+        return vo;
+    }
+
+    @Override
+    public List<CuisineListVO> searchCuisines(Integer storeId, String keyword, Integer tableId) {
+        log.info("搜索菜品, storeId={}, keyword={}, tableId={}", storeId, keyword, tableId);
+
+        // 限制搜索关键词长度
+        if (StringUtils.hasText(keyword) && keyword.length() > 10) {
+            keyword = keyword.substring(0, 10);
+        }
+
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        if (StringUtils.hasText(keyword)) {
+            wrapper.like(StoreCuisine::getName, keyword);
+        }
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+
+        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        return convertToCuisineListVO(cuisines, tableId);
+    }
+
+    @Override
+    public List<CuisineListVO> getCuisinesByCategory(Integer storeId, Integer categoryId, Integer tableId, Integer page, Integer size) {
+        log.info("根据分类获取菜品列表, storeId={}, categoryId={}, tableId={}, page={}, size={}", storeId, categoryId, tableId, page, size);
+
+        if (page == null || page < 1) {
+            page = 1;
+        }
+        if (size == null || size < 1) {
+            size = 12;
+        }
+
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 只查询上架的
+        if (categoryId != null) {
+            // 这里假设菜品表有category_id字段,如果没有需要关联查询
+            // wrapper.eq(StoreCuisine::getCategoryId, categoryId);
+        }
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime);
+
+        // 分页查询
+        int offset = (page - 1) * size;
+        wrapper.last("LIMIT " + offset + ", " + size);
+
+        List<StoreCuisine> cuisines = storeCuisineMapper.selectList(wrapper);
+        return convertToCuisineListVO(cuisines, tableId);
+    }
+
+    @Override
+    public CuisineDetailVO getCuisineDetail(Integer cuisineId, Integer tableId) {
+        log.info("获取菜品详情, cuisineId={}, tableId={}", cuisineId, tableId);
+
+        StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+
+        CuisineDetailVO vo = new CuisineDetailVO();
+        BeanUtils.copyProperties(cuisine, vo);
+
+        // 处理图片列表
+        if (StringUtils.hasText(cuisine.getImages())) {
+            List<String> images = Arrays.asList(cuisine.getImages().split(","));
+            vo.setImages(images);
+        } else {
+            vo.setImages(new ArrayList<>());
+        }
+
+        // 计算月售数量
+        vo.setMonthlySales(getMonthlySales(cuisineId));
+
+        // 获取购物车数量
+        CartDTO cart = cartService.getCart(tableId);
+        if (cart.getItems() != null) {
+            Optional<CartItemDTO> cartItem = cart.getItems().stream()
+                    .filter(item -> item.getCuisineId().equals(cuisineId))
+                    .findFirst();
+            vo.setCartQuantity(cartItem.map(CartItemDTO::getQuantity).orElse(0));
+        } else {
+            vo.setCartQuantity(0);
+        }
+
+        // 如果是套餐,获取套餐包含的菜品
+        if (cuisine.getCuisineType() != null && cuisine.getCuisineType() == 2) {
+            LambdaQueryWrapper<StoreCuisineCombo> comboWrapper = new LambdaQueryWrapper<>();
+            comboWrapper.eq(StoreCuisineCombo::getCid, cuisineId);
+            comboWrapper.eq(StoreCuisineCombo::getDeleteFlag, 0);
+            List<StoreCuisineCombo> combos = storeCuisineComboMapper.selectList(comboWrapper);
+
+            List<CuisineComboItemVO> comboItems = combos.stream().map(combo -> {
+                CuisineComboItemVO item = new CuisineComboItemVO();
+                item.setCuisineId(combo.getSid());
+                item.setQuantity(combo.getSnum());
+                item.setCategory(combo.getCategory());
+                // 查询菜品名称
+                StoreCuisine comboCuisine = storeCuisineMapper.selectById(combo.getSid());
+                if (comboCuisine != null) {
+                    item.setCuisineName(comboCuisine.getName());
+                }
+                return item;
+            }).collect(Collectors.toList());
+            vo.setComboItems(comboItems);
+        }
+
+        return vo;
+    }
+
+    @Override
+    public List<AvailableCouponVO> getAvailableCoupons(Integer storeId, Integer userId) {
+        log.info("获取可领取优惠券列表, storeId={}, userId={}", storeId, userId);
+
+        // 查询门店的优惠券
+        LambdaQueryWrapper<LifeDiscountCoupon> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LifeDiscountCoupon::getStoreId, String.valueOf(storeId));
+        wrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+        wrapper.eq(LifeDiscountCoupon::getGetStatus, 1); // 开启领取
+        wrapper.gt(LifeDiscountCoupon::getSingleQty, 0); // 有库存
+        LocalDate now = LocalDate.now();
+        wrapper.le(LifeDiscountCoupon::getStartDate, now);
+        wrapper.ge(LifeDiscountCoupon::getEndDate, now);
+        wrapper.orderByDesc(LifeDiscountCoupon::getCreatedTime);
+
+        List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(wrapper);
+
+        // 查询用户已领取的优惠券
+        Set<Integer> receivedCouponIds = new HashSet<>();
+        if (userId != null) {
+            LambdaQueryWrapper<LifeDiscountCouponUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+            List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
+            receivedCouponIds = userCoupons.stream()
+                    .map(LifeDiscountCouponUser::getCouponId)
+                    .collect(Collectors.toSet());
+        }
+
+        final Set<Integer> finalReceivedCouponIds = receivedCouponIds;
+        return coupons.stream().map(coupon -> {
+            AvailableCouponVO vo = new AvailableCouponVO();
+            vo.setId(coupon.getId());
+            vo.setName(coupon.getName());
+            vo.setNominalValue(coupon.getNominalValue());
+            vo.setMinimumSpendingAmount(coupon.getMinimumSpendingAmount());
+            vo.setEndDate(coupon.getEndDate());
+            vo.setIsReceived(finalReceivedCouponIds.contains(coupon.getId()));
+            vo.setIsAvailable(coupon.getSingleQty() > 0 && coupon.getEndDate().isAfter(now) || coupon.getEndDate().isEqual(now));
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean receiveCoupon(Integer couponId, Integer userId) {
+        log.info("领取优惠券, couponId={}, userId={}", couponId, userId);
+
+        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+        if (coupon == null) {
+            throw new RuntimeException("优惠券不存在");
+        }
+
+        // 检查是否已领取
+        LambdaQueryWrapper<LifeDiscountCouponUser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LifeDiscountCouponUser::getUserId, userId);
+        wrapper.eq(LifeDiscountCouponUser::getCouponId, couponId);
+        LifeDiscountCouponUser existing = lifeDiscountCouponUserMapper.selectOne(wrapper);
+        if (existing != null) {
+            throw new RuntimeException("您已领取过该优惠券");
+        }
+
+        // 检查库存
+        if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {
+            throw new RuntimeException("优惠券已领完");
+        }
+
+        // 创建用户优惠券记录
+        LifeDiscountCouponUser userCoupon = new LifeDiscountCouponUser();
+        userCoupon.setUserId(userId);
+        userCoupon.setCouponId(couponId);
+        userCoupon.setReceiveTime(new Date());
+        userCoupon.setStatus(0); // 待使用
+        if (coupon.getSpecifiedDay() != null && !coupon.getSpecifiedDay().isEmpty()) {
+            try {
+                int days = Integer.parseInt(coupon.getSpecifiedDay());
+                LocalDate expirationDate = LocalDate.now().plusDays(days);
+                userCoupon.setExpirationTime(expirationDate);
+            } catch (NumberFormatException e) {
+                userCoupon.setExpirationTime(coupon.getEndDate());
+            }
+        } else {
+            userCoupon.setExpirationTime(coupon.getEndDate());
+        }
+        lifeDiscountCouponUserMapper.insert(userCoupon);
+
+        // 更新库存
+        coupon.setSingleQty(coupon.getSingleQty() - 1);
+        lifeDiscountCouponMapper.updateById(coupon);
+
+        return true;
+    }
+
+    @Override
+    public OrderConfirmVO getOrderConfirmInfo(Integer tableId, Integer dinerCount, Integer userId) {
+        log.info("获取订单确认页面信息, tableId={}, dinerCount={}, userId={}", tableId, dinerCount, userId);
+
+        // 获取点餐页面信息
+        DiningPageInfoVO pageInfo = getDiningPageInfo(tableId, dinerCount);
+
+        // 获取购物车
+        CartDTO cart = cartService.getCart(tableId);
+
+        // 检查订单锁定
+        Integer lockUserId = checkOrderLock(tableId);
+        boolean isLocked = lockUserId != null && !lockUserId.equals(userId);
+
+        OrderConfirmVO vo = new OrderConfirmVO();
+        vo.setStoreName(pageInfo.getStoreName());
+        vo.setTableNumber(pageInfo.getTableNumber());
+        vo.setDinerCount(dinerCount);
+        // 联系电话和备注由前端传入,这里不设置默认值
+        vo.setItems(cart.getItems());
+        vo.setTotalAmount(cart.getTotalAmount());
+        vo.setIsLocked(isLocked);
+        vo.setLockUserId(lockUserId);
+
+        // 计算餐具费(默认1元/人)
+        BigDecimal tablewareUnitPrice = new BigDecimal("1.00");
+        BigDecimal tablewareFee = BigDecimal.ZERO;
+        if (dinerCount != null && dinerCount > 0) {
+            tablewareFee = tablewareUnitPrice.multiply(BigDecimal.valueOf(dinerCount));
+        }
+        vo.setTablewareFee(tablewareFee);
+        vo.setTablewareUnitPrice(tablewareUnitPrice);
+
+        // 自动匹配最优惠的优惠券
+        List<AvailableCouponVO> availableCoupons = new ArrayList<>();
+        if (userId != null && cart.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
+            availableCoupons = getAvailableCoupons(pageInfo.getStoreId(), userId);
+            // 过滤出可用的优惠券(已领取且满足最低消费)
+            BigDecimal totalWithTableware = cart.getTotalAmount().add(tablewareFee);
+            List<AvailableCouponVO> usableCoupons = availableCoupons.stream()
+                    .filter(c -> c.getIsReceived() && c.getIsAvailable())
+                    .filter(c -> totalWithTableware.compareTo(c.getMinimumSpendingAmount() != null ? c.getMinimumSpendingAmount() : BigDecimal.ZERO) >= 0)
+                    .sorted((a, b) -> b.getNominalValue().compareTo(a.getNominalValue())) // 按优惠金额降序
+                    .collect(Collectors.toList());
+
+            if (!usableCoupons.isEmpty()) {
+                AvailableCouponVO bestCoupon = usableCoupons.get(0);
+                vo.setCouponId(bestCoupon.getId());
+                vo.setCouponName(bestCoupon.getName());
+                BigDecimal discountAmount = bestCoupon.getNominalValue();
+                BigDecimal totalAmount = cart.getTotalAmount().add(tablewareFee);
+                if (discountAmount.compareTo(totalAmount) > 0) {
+                    discountAmount = totalAmount;
+                }
+                vo.setDiscountAmount(discountAmount);
+                vo.setPayAmount(totalAmount.subtract(discountAmount));
+            } else {
+                vo.setPayAmount(cart.getTotalAmount().add(tablewareFee));
+            }
+        } else {
+            vo.setPayAmount(cart.getTotalAmount().add(tablewareFee));
+        }
+        vo.setAvailableCoupons(availableCoupons);
+
+        return vo;
+    }
+
+    @Override
+    public boolean lockOrder(Integer tableId, Integer userId) {
+        log.info("锁定订单, tableId={}, userId={}", tableId, userId);
+
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock)) {
+            // 已锁定,检查是否是当前用户
+            if (existingLock.equals(String.valueOf(userId))) {
+                // 刷新过期时间
+                baseRedisService.setString(lockKey, existingLock, (long) ORDER_LOCK_EXPIRE_SECONDS);
+                return true;
+            } else {
+                // 被其他用户锁定
+                return false;
+            }
+        }
+
+        // 设置锁定
+        baseRedisService.setString(lockKey, String.valueOf(userId), (long) ORDER_LOCK_EXPIRE_SECONDS);
+        return true;
+    }
+
+    @Override
+    public void unlockOrder(Integer tableId, Integer userId) {
+        log.info("解锁订单, tableId={}, userId={}", tableId, userId);
+
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock) && existingLock.equals(String.valueOf(userId))) {
+            baseRedisService.delete(lockKey);
+        }
+    }
+
+    @Override
+    public Integer checkOrderLock(Integer tableId) {
+        String lockKey = ORDER_LOCK_KEY_PREFIX + tableId;
+        String lockUserId = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(lockUserId)) {
+            try {
+                return Integer.parseInt(lockUserId);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 转换为菜品列表VO
+     */
+    private List<CuisineListVO> convertToCuisineListVO(List<StoreCuisine> cuisines, Integer tableId) {
+        // 获取购物车
+        CartDTO cart = cartService.getCart(tableId);
+        Map<Integer, Integer> cartQuantityMap = new HashMap<>();
+        if (cart.getItems() != null) {
+            cartQuantityMap = cart.getItems().stream()
+                    .collect(Collectors.toMap(CartItemDTO::getCuisineId, CartItemDTO::getQuantity));
+        }
+
+        // 批量查询月售数量
+        Map<Integer, Integer> monthlySalesMap = new HashMap<>();
+        for (StoreCuisine cuisine : cuisines) {
+            monthlySalesMap.put(cuisine.getId(), getMonthlySales(cuisine.getId()));
+        }
+
+        final Map<Integer, Integer> finalCartQuantityMap = cartQuantityMap;
+        final Map<Integer, Integer> finalMonthlySalesMap = monthlySalesMap;
+
+        return cuisines.stream().map(cuisine -> {
+            CuisineListVO vo = new CuisineListVO();
+            BeanUtils.copyProperties(cuisine, vo);
+            vo.setPrice(cuisine.getTotalPrice());
+
+            // 处理首张图片
+            if (StringUtils.hasText(cuisine.getImages())) {
+                String[] images = cuisine.getImages().split(",");
+                vo.setFirstImage(images.length > 0 ? images[0] : null);
+            }
+
+            // 设置购物车数量
+            vo.setCartQuantity(finalCartQuantityMap.getOrDefault(cuisine.getId(), 0));
+
+            // 设置月售数量
+            vo.setMonthlySales(finalMonthlySalesMap.getOrDefault(cuisine.getId(), 0));
+
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取菜品月售数量(当月1日至当前日期)
+     */
+    private Integer getMonthlySales(Integer cuisineId) {
+        try {
+            // 计算当月1日
+            Calendar calendar = Calendar.getInstance();
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date monthStart = calendar.getTime();
+
+            // 查询订单明细中该菜品的销售数量
+            LambdaQueryWrapper<StoreOrderDetail> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreOrderDetail::getCuisineId, cuisineId);
+            wrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+            wrapper.ge(StoreOrderDetail::getCreatedTime, monthStart);
+            // 只统计已支付的订单
+            wrapper.inSql(StoreOrderDetail::getOrderId,
+                    "SELECT id FROM store_order WHERE pay_status = 1 AND delete_flag = 0");
+
+            List<StoreOrderDetail> details = storeOrderDetailMapper.selectList(wrapper);
+            return details.stream().mapToInt(StoreOrderDetail::getQuantity).sum();
+        } catch (Exception e) {
+            log.error("获取月售数量失败, cuisineId={}", cuisineId, e);
+            return 0;
+        }
+    }
+
+    @Override
+    public shop.alien.entity.store.vo.OrderSettlementVO getOrderSettlementInfo(Integer orderId, Integer userId) {
+        log.info("获取订单结算确认页面信息, orderId={}, userId={}", orderId, userId);
+
+        // 查询订单
+        shop.alien.entity.store.StoreOrder order = storeOrderService.getOrderById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        // 查询订单明细
+        LambdaQueryWrapper<shop.alien.entity.store.StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(shop.alien.entity.store.StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByDesc(shop.alien.entity.store.StoreOrderDetail::getCreatedTime);
+        List<shop.alien.entity.store.StoreOrderDetail> details = storeOrderDetailMapper.selectList(detailWrapper);
+
+        // 转换为CartItemDTO
+        List<shop.alien.entity.store.dto.CartItemDTO> items = details.stream().map(detail -> {
+            shop.alien.entity.store.dto.CartItemDTO item = new shop.alien.entity.store.dto.CartItemDTO();
+            item.setCuisineId(detail.getCuisineId());
+            item.setCuisineName(detail.getCuisineName());
+            item.setCuisineType(detail.getCuisineType());
+            item.setCuisineImage(detail.getCuisineImage());
+            item.setUnitPrice(detail.getUnitPrice());
+            item.setQuantity(detail.getQuantity());
+            item.setSubtotalAmount(detail.getSubtotalAmount());
+            item.setAddUserId(detail.getAddUserId());
+            item.setAddUserPhone(detail.getAddUserPhone());
+            item.setRemark(detail.getRemark());
+            return item;
+        }).collect(Collectors.toList());
+
+        // 查询门店信息
+        shop.alien.entity.store.StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+
+        // 检查结算锁定
+        String lockKey = "settlement:lock:order:" + orderId;
+        String lockUserIdStr = baseRedisService.getString(lockKey);
+        Integer lockUserId = null;
+        boolean isLocked = false;
+        if (StringUtils.hasText(lockUserIdStr)) {
+            try {
+                lockUserId = Integer.parseInt(lockUserIdStr);
+                isLocked = !lockUserId.equals(userId);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        shop.alien.entity.store.vo.OrderSettlementVO vo = new shop.alien.entity.store.vo.OrderSettlementVO();
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setStoreName(storeInfo != null ? storeInfo.getStoreName() : null);
+        vo.setTableNumber(order.getTableNumber());
+        vo.setDinerCount(order.getDinerCount());
+        vo.setContactPhone(order.getContactPhone());
+        vo.setRemark(order.getRemark());
+        vo.setItems(items);
+        vo.setTotalAmount(order.getTotalAmount());
+        vo.setTablewareFee(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO);
+        vo.setCouponId(order.getCouponId());
+        vo.setDiscountAmount(order.getDiscountAmount() != null ? order.getDiscountAmount() : BigDecimal.ZERO);
+        vo.setPayAmount(order.getPayAmount());
+        vo.setIsLocked(isLocked);
+        vo.setLockUserId(lockUserId);
+
+        // 查询优惠券名称
+        if (order.getCouponId() != null) {
+            shop.alien.entity.store.LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            if (coupon != null) {
+                vo.setCouponName(coupon.getName());
+            }
+        }
+
+        return vo;
+    }
+
+    @Override
+    public boolean lockSettlement(Integer orderId, Integer userId) {
+        log.info("锁定订单结算, orderId={}, userId={}", orderId, userId);
+
+        String lockKey = "settlement:lock:order:" + orderId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock)) {
+            if (existingLock.equals(String.valueOf(userId))) {
+                baseRedisService.setString(lockKey, existingLock, (long) ORDER_LOCK_EXPIRE_SECONDS);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        baseRedisService.setString(lockKey, String.valueOf(userId), (long) ORDER_LOCK_EXPIRE_SECONDS);
+        return true;
+    }
+
+    @Override
+    public void unlockSettlement(Integer orderId, Integer userId) {
+        log.info("解锁订单结算, orderId={}, userId={}", orderId, userId);
+
+        String lockKey = "settlement:lock:order:" + orderId;
+        String existingLock = baseRedisService.getString(lockKey);
+        if (StringUtils.hasText(existingLock) && existingLock.equals(String.valueOf(userId))) {
+            baseRedisService.delete(lockKey);
+        }
+    }
+}

+ 152 - 0
alien-store/src/main/java/shop/alien/store/service/impl/SseServiceImpl.java

@@ -0,0 +1,152 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * SSE推送服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+public class SseServiceImpl implements shop.alien.store.service.SseService {
+
+    // 存储每个桌号的SSE连接,一个桌号可以有多个连接(多个用户)
+    private final ConcurrentHashMap<Integer, ConcurrentHashMap<String, SseEmitter>> connections = new ConcurrentHashMap<>();
+
+    // 定时任务执行器,用于发送心跳
+    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
+
+    @Override
+    public SseEmitter createConnection(Integer tableId) {
+        log.info("创建SSE连接, tableId={}", tableId);
+
+        // 创建SSE连接,设置超时时间为30分钟
+        SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
+
+        // 生成连接ID
+        String connectionId = "conn_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId();
+
+        // 存储连接
+        connections.computeIfAbsent(tableId, k -> new ConcurrentHashMap<>()).put(connectionId, emitter);
+
+        // 设置完成和超时回调
+        emitter.onCompletion(() -> {
+            log.info("SSE连接完成, tableId={}, connectionId={}", tableId, connectionId);
+            removeConnection(tableId, connectionId);
+        });
+
+        emitter.onTimeout(() -> {
+            log.info("SSE连接超时, tableId={}, connectionId={}", tableId, connectionId);
+            removeConnection(tableId, connectionId);
+        });
+
+        emitter.onError((ex) -> {
+            log.error("SSE连接错误, tableId={}, connectionId={}, error={}", tableId, connectionId, ex.getMessage(), ex);
+            removeConnection(tableId, connectionId);
+        });
+
+        // 发送初始连接成功消息
+        try {
+            emitter.send(SseEmitter.event()
+                    .name("connected")
+                    .data("连接成功"));
+        } catch (IOException e) {
+            log.error("发送初始消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            removeConnection(tableId, connectionId);
+            return null;
+        }
+
+        // 启动心跳任务
+        startHeartbeat(tableId, connectionId, emitter);
+
+        return emitter;
+    }
+
+    @Override
+    public void pushCartUpdate(Integer tableId, Object message) {
+        log.info("推送购物车更新, tableId={}", tableId);
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections == null || tableConnections.isEmpty()) {
+            log.warn("该桌号没有SSE连接, tableId={}", tableId);
+            return;
+        }
+
+        String messageJson = JSON.toJSONString(message);
+        tableConnections.forEach((connectionId, emitter) -> {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("cart_update")
+                        .data(messageJson));
+                log.info("推送购物车更新成功, tableId={}, connectionId={}", tableId, connectionId);
+            } catch (IOException e) {
+                log.error("推送购物车更新失败, tableId={}, connectionId={}, error={}", tableId, connectionId, e.getMessage(), e);
+                removeConnection(tableId, connectionId);
+            }
+        });
+    }
+
+    @Override
+    public void closeConnection(Integer tableId) {
+        log.info("关闭SSE连接, tableId={}", tableId);
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections != null) {
+            tableConnections.forEach((connectionId, emitter) -> {
+                try {
+                    emitter.complete();
+                } catch (Exception e) {
+                    log.error("关闭SSE连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            });
+            connections.remove(tableId);
+        }
+    }
+
+    /**
+     * 移除连接
+     */
+    private void removeConnection(Integer tableId, String connectionId) {
+        ConcurrentHashMap<String, SseEmitter> tableConnections = connections.get(tableId);
+        if (tableConnections != null) {
+            SseEmitter emitter = tableConnections.remove(connectionId);
+            if (emitter != null) {
+                try {
+                    emitter.complete();
+                } catch (Exception e) {
+                    log.error("完成SSE连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            }
+            if (tableConnections.isEmpty()) {
+                connections.remove(tableId);
+            }
+        }
+    }
+
+    /**
+     * 启动心跳任务
+     */
+    private void startHeartbeat(Integer tableId, String connectionId, SseEmitter emitter) {
+        scheduler.scheduleAtFixedRate(() -> {
+            try {
+                if (emitter != null) {
+                    emitter.send(SseEmitter.event()
+                            .name("heartbeat")
+                            .data("ping"));
+                }
+            } catch (IOException e) {
+                log.error("发送心跳失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                removeConnection(tableId, connectionId);
+            }
+        }, 30, 30, TimeUnit.SECONDS); // 每30秒发送一次心跳
+    }
+}

+ 28 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBannerServiceImpl.java

@@ -24,9 +24,37 @@ import shop.alien.store.service.StoreBannerService;
 @Service
 public class StoreBannerServiceImpl extends ServiceImpl<StoreBannerMapper, StoreBanner> implements StoreBannerService {
 
+//    @Transactional
+//    @Override
+//    public R<String> saveBanner(StoreBannerDto storeBannerDto) {
+//        StoreBanner storeBanner = new StoreBanner();
+//        BeanUtils.copyProperties(storeBannerDto, storeBanner);
+//        boolean update = storeBanner.getId() != null;
+//        boolean result = this.saveOrUpdate(storeBanner);
+//        if (result) {
+//            return R.success(update ? "修改成功" : "新增成功");
+//        }
+//        return R.fail(update ? "修改失败" : "新增失败");
+//    }
+
+
     @Transactional
     @Override
     public R<String> saveBanner(StoreBannerDto storeBannerDto) {
+        // 先检查 moduleName 是否已存在
+        LambdaQueryWrapper<StoreBanner> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreBanner::getModuleName, storeBannerDto.getModuleName());
+
+        // 如果是更新操作,需要排除当前记录本身
+        if (storeBannerDto.getId() != null) {
+            queryWrapper.ne(StoreBanner::getId, storeBannerDto.getId());
+        }
+
+        long count = this.count(queryWrapper);
+        if (count > 0) {
+            return R.fail("该模块名称已存在,不能重复添加或修改");
+        }
+
         StoreBanner storeBanner = new StoreBanner();
         BeanUtils.copyProperties(storeBannerDto, storeBanner);
         boolean update = storeBanner.getId() != null;

+ 10 - 9
alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java

@@ -429,7 +429,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
     }
 
     @Override
-    public StoreClockInVo getStoreClockInById(Integer id) {
+    public StoreClockInVo getStoreClockInById(Integer id, Integer userId) {
         StoreClockIn storeClockIn = storeClockInMapper.selectById(id);
         StoreClockInVo storeClockInVo = BeanUtil.copyProperties(storeClockIn, StoreClockInVo.class);
         if (null == storeClockIn) {
@@ -458,15 +458,16 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         storeClockInVo.setBusinessClassifyName(storeInfo.getBusinessClassifyName());
         // 地址
         storeClockInVo.setStoreAddress(storeInfo.getStoreAddress());
-        // 是否收藏
-        // 查询我的收藏
-        LambdaQueryWrapper<LifeCollect> collectWrapper = new LambdaQueryWrapper<>();
-        collectWrapper.eq(LifeCollect::getUserId, String.valueOf(storeClockInVo.getUserId()));
-        List<LifeCollect> lifeCollectList = lifeCollectMapper.selectList(collectWrapper);
-        List<String> collectList = lifeCollectList.stream().map(LifeCollect::getStoreId).collect(Collectors.toList());
+        // 是否收藏 - 使用当前登录用户的ID查询收藏
         storeClockInVo.setIsCollect("0");
-        if (collectList.contains(storeClockInVo.getStoreId().toString())){
-            storeClockInVo.setIsCollect("1");
+        if (userId != null) {
+            LambdaQueryWrapper<LifeCollect> collectWrapper = new LambdaQueryWrapper<>();
+            collectWrapper.eq(LifeCollect::getUserId, String.valueOf(userId));
+            List<LifeCollect> lifeCollectList = lifeCollectMapper.selectList(collectWrapper);
+            List<String> collectList = lifeCollectList.stream().map(LifeCollect::getStoreId).collect(Collectors.toList());
+            if (collectList.contains(storeClockInVo.getStoreId().toString())){
+                storeClockInVo.setIsCollect("1");
+            }
         }
         return storeClockInVo;
     }

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

+ 85 - 34
alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java

@@ -59,6 +59,8 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private final StoreEvaluationMapper storeEvaluationMapper;
     private final StoreCommentAppealMapper storeCommentAppealMapper;
     private final StorePriceMapper storePriceMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreInfoMapper storeInfoMapper;
     private final StoreUserMapper storeUserMapper;
     private final StoreOperationalStatisticsHistoryMapper statisticsHistoryMapper;
     private final StoreTrackStatisticsMapper storeTrackStatisticsMapper;
@@ -115,7 +117,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             }
             
             // 聚合统计数据(优先取结束日期的数据,只累加新增访客数)
-            return aggregateStatistics(statisticsList, endDate);
+            return aggregateStatistics(statisticsList, endDate, storeId);
             
         } catch (ParseException e) {
             log.error("StoreOperationalStatisticsServiceImpl.calculateStatistics - 时间解析错误", e);
@@ -132,8 +134,9 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
      * 
      * @param statisticsList 统计数据列表
      * @param endDate 结束日期(用于优先选择该日期的数据)
+     * @param storeId 店铺ID
      */
-    private StoreOperationalStatisticsVo aggregateStatistics(List<StoreTrackStatistics> statisticsList, Date endDate) {
+    private StoreOperationalStatisticsVo aggregateStatistics(List<StoreTrackStatistics> statisticsList, Date endDate, Integer storeId) {
         StoreOperationalStatisticsVo result = new StoreOperationalStatisticsVo();
         
         // 优先查找结束日期的数据(比较日期部分,忽略时间)
@@ -247,7 +250,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         }
         
         if (latestStat.getPriceRankingData() != null && !latestStat.getPriceRankingData().isEmpty()) {
-            result.setPriceListRanking(convertToPriceListRankingVoFromJson(latestStat.getPriceRankingData()));
+            result.setPriceListRanking(convertToPriceListRankingVoFromJson(latestStat.getPriceRankingData(), storeId));
         }
         
         return result;
@@ -292,7 +295,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setCouponData(buildCouponDataComparison(currentStatistics.getCouponData(), previousStatistics.getCouponData()));
         comparison.setVoucherData(buildVoucherDataComparison(currentStatistics.getVoucherData(), previousStatistics.getVoucherData()));
         comparison.setServiceQualityData(buildServiceQualityDataComparison(currentStatistics.getServiceQualityData(), previousStatistics.getServiceQualityData()));
-        comparison.setPriceListRanking(buildPriceListRankingComparison(currentStatistics.getPriceListRanking(), previousStatistics.getPriceListRanking()));
+        comparison.setPriceListRanking(buildPriceListRankingComparison(currentStatistics.getPriceListRanking(), previousStatistics.getPriceListRanking(), storeId));
 
         // 保存历史记录(不包含PDF URL,PDF URL只在 generateStatisticsComparisonPdf 接口中保存)
         Integer historyId = saveStatisticsHistory(storeId, currentStartTime, currentEndTime, 
@@ -889,7 +892,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
      * 构建价目表排名数据对比
      */
     private List<StoreOperationalStatisticsComparisonVo.PriceListRankingComparison> buildPriceListRankingComparison(
-            List<StoreOperationalStatisticsVo.PriceListRanking> current, List<StoreOperationalStatisticsVo.PriceListRanking> previous) {
+            List<StoreOperationalStatisticsVo.PriceListRanking> current, List<StoreOperationalStatisticsVo.PriceListRanking> previous, Integer storeId) {
         List<StoreOperationalStatisticsComparisonVo.PriceListRankingComparison> result = new ArrayList<>();
         
         // 如果当期数据为空,返回空列表
@@ -919,16 +922,32 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             }
         }
         
-        // 批量查询价目表名称
+        // 批量查询价目表名称(根据business_section判断查询美食价目表还是通用价目表)
         Map<Integer, String> priceNameMap = new HashMap<>();
-        if (!needQueryPriceIds.isEmpty()) {
-            LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
-            priceWrapper.in(StorePrice::getId, needQueryPriceIds)
-                        .eq(StorePrice::getDeleteFlag, 0);
-            List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
-            for (StorePrice price : prices) {
-                if (price.getId() != null && price.getName() != null) {
-                    priceNameMap.put(price.getId(), price.getName());
+        if (!needQueryPriceIds.isEmpty() && storeId != null) {
+            // 查询店铺信息,获取business_section
+            StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+            if (storeInfo != null && storeInfo.getBusinessSection() != null && storeInfo.getBusinessSection() == 1) {
+                // business_section = 1 表示美食,查询美食价目表
+                LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
+                cuisineWrapper.in(StoreCuisine::getId, needQueryPriceIds)
+                            .eq(StoreCuisine::getDeleteFlag, 0);
+                List<StoreCuisine> cuisines = storeCuisineMapper.selectList(cuisineWrapper);
+                for (StoreCuisine cuisine : cuisines) {
+                    if (cuisine.getId() != null && cuisine.getName() != null) {
+                        priceNameMap.put(cuisine.getId(), cuisine.getName());
+                    }
+                }
+            } else {
+                // 其他情况查询通用价目表
+                LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
+                priceWrapper.in(StorePrice::getId, needQueryPriceIds)
+                            .eq(StorePrice::getDeleteFlag, 0);
+                List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
+                for (StorePrice price : prices) {
+                    if (price.getId() != null && price.getName() != null) {
+                        priceNameMap.put(price.getId(), price.getName());
+                    }
                 }
             }
         }
@@ -1668,7 +1687,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     /**
      * 转换为价目表排名数据VO
      */
-    private List<StoreOperationalStatisticsVo.PriceListRanking> convertToPriceListRankingVo(Map<Integer, Map<String, Long>> accumulator) {
+    private List<StoreOperationalStatisticsVo.PriceListRanking> convertToPriceListRankingVo(Map<Integer, Map<String, Long>> accumulator, Integer storeId) {
         List<StoreOperationalStatisticsVo.PriceListRanking> result = new ArrayList<>();
         
         // 如果累加器为空,直接返回
@@ -1676,17 +1695,33 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             return result;
         }
         
-        // 批量查询价目表名称
+        // 批量查询价目表名称(根据business_section判断查询美食价目表还是通用价目表)
         List<Integer> priceIds = new ArrayList<>(accumulator.keySet());
         Map<Integer, String> priceNameMap = new HashMap<>();
-        if (!priceIds.isEmpty()) {
-            LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
-            priceWrapper.in(StorePrice::getId, priceIds)
-                        .eq(StorePrice::getDeleteFlag, 0);
-            List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
-            for (StorePrice price : prices) {
-                if (price.getId() != null && price.getName() != null) {
-                    priceNameMap.put(price.getId(), price.getName());
+        if (!priceIds.isEmpty() && storeId != null) {
+            // 查询店铺信息,获取business_section
+            StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+            if (storeInfo != null && storeInfo.getBusinessSection() != null && storeInfo.getBusinessSection() == 1) {
+                // business_section = 1 表示美食,查询美食价目表
+                LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
+                cuisineWrapper.in(StoreCuisine::getId, priceIds)
+                            .eq(StoreCuisine::getDeleteFlag, 0);
+                List<StoreCuisine> cuisines = storeCuisineMapper.selectList(cuisineWrapper);
+                for (StoreCuisine cuisine : cuisines) {
+                    if (cuisine.getId() != null && cuisine.getName() != null) {
+                        priceNameMap.put(cuisine.getId(), cuisine.getName());
+                    }
+                }
+            } else {
+                // 其他情况查询通用价目表
+                LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
+                priceWrapper.in(StorePrice::getId, priceIds)
+                            .eq(StorePrice::getDeleteFlag, 0);
+                List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
+                for (StorePrice price : prices) {
+                    if (price.getId() != null && price.getName() != null) {
+                        priceNameMap.put(price.getId(), price.getName());
+                    }
                 }
             }
         }
@@ -2129,7 +2164,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
      * 从JSON字符串转换为价目表排名数据VO
      */
     @SuppressWarnings("unchecked")
-    private List<StoreOperationalStatisticsVo.PriceListRanking> convertToPriceListRankingVoFromJson(String priceRankingDataJson) {
+    private List<StoreOperationalStatisticsVo.PriceListRanking> convertToPriceListRankingVoFromJson(String priceRankingDataJson, Integer storeId) {
         List<StoreOperationalStatisticsVo.PriceListRanking> result = new ArrayList<>();
         
         try {
@@ -2148,14 +2183,30 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             }
             
             Map<Integer, String> priceNameMap = new HashMap<>();
-            if (!priceIds.isEmpty()) {
-                LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
-                priceWrapper.in(StorePrice::getId, priceIds)
-                            .eq(StorePrice::getDeleteFlag, 0);
-                List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
-                for (StorePrice price : prices) {
-                    if (price.getId() != null && price.getName() != null) {
-                        priceNameMap.put(price.getId(), price.getName());
+            if (!priceIds.isEmpty() && storeId != null) {
+                // 查询店铺信息,获取business_section
+                StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+                if (storeInfo != null && storeInfo.getBusinessSection() != null && storeInfo.getBusinessSection() == 1) {
+                    // business_section = 1 表示美食,查询美食价目表
+                    LambdaQueryWrapper<StoreCuisine> cuisineWrapper = new LambdaQueryWrapper<>();
+                    cuisineWrapper.in(StoreCuisine::getId, priceIds)
+                                .eq(StoreCuisine::getDeleteFlag, 0);
+                    List<StoreCuisine> cuisines = storeCuisineMapper.selectList(cuisineWrapper);
+                    for (StoreCuisine cuisine : cuisines) {
+                        if (cuisine.getId() != null && cuisine.getName() != null) {
+                            priceNameMap.put(cuisine.getId(), cuisine.getName());
+                        }
+                    }
+                } else {
+                    // 其他情况查询通用价目表
+                    LambdaQueryWrapper<StorePrice> priceWrapper = new LambdaQueryWrapper<>();
+                    priceWrapper.in(StorePrice::getId, priceIds)
+                                .eq(StorePrice::getDeleteFlag, 0);
+                    List<StorePrice> prices = storePriceMapper.selectList(priceWrapper);
+                    for (StorePrice price : prices) {
+                        if (price.getId() != null && price.getName() != null) {
+                            priceNameMap.put(price.getId(), price.getName());
+                        }
                     }
                 }
             }
@@ -2270,7 +2321,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setCouponData(buildCouponDataComparison(currentStatistics.getCouponData(), previousStatistics.getCouponData()));
         comparison.setVoucherData(buildVoucherDataComparison(currentStatistics.getVoucherData(), previousStatistics.getVoucherData()));
         comparison.setServiceQualityData(buildServiceQualityDataComparison(currentStatistics.getServiceQualityData(), previousStatistics.getServiceQualityData()));
-        comparison.setPriceListRanking(buildPriceListRankingComparison(currentStatistics.getPriceListRanking(), previousStatistics.getPriceListRanking()));
+        comparison.setPriceListRanking(buildPriceListRankingComparison(currentStatistics.getPriceListRanking(), previousStatistics.getPriceListRanking(), storeId));
 
         // 不保存历史记录,由调用方决定是否保存
 

+ 583 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreOrderServiceImpl.java

@@ -0,0 +1,583 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+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.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.mapper.*;
+import shop.alien.store.service.CartService;
+import shop.alien.store.service.StoreOrderService;
+import shop.alien.util.common.JwtUtil;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 订单服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOrder> implements StoreOrderService {
+
+    private final StoreOrderDetailMapper orderDetailMapper;
+    private final StoreTableMapper storeTableMapper;
+    private final StoreTableLogMapper storeTableLogMapper;
+    private final StoreCuisineMapper storeCuisineMapper;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+    private final CartService cartService;
+    private final StoreOrderLockMapper storeOrderLockMapper;
+    private final StoreCouponUsageMapper storeCouponUsageMapper;
+
+    @Override
+    public StoreOrder createOrder(CreateOrderDTO dto) {
+        log.info("创建订单, dto={}", dto);
+
+        // 获取当前用户信息
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        if (userInfo == null) {
+            throw new RuntimeException("用户未登录");
+        }
+        Integer userId = userInfo.getInteger("userId");
+        String userPhone = userInfo.getString("phone");
+
+        // 验证桌号
+        StoreTable table = storeTableMapper.selectById(dto.getTableId());
+        if (table == null) {
+            throw new RuntimeException("桌号不存在");
+        }
+
+        // 获取购物车
+        CartDTO cart = cartService.getCart(dto.getTableId());
+        if (cart.getItems() == null || cart.getItems().isEmpty()) {
+            throw new RuntimeException("购物车为空");
+        }
+
+        // 验证优惠券
+        BigDecimal discountAmount = BigDecimal.ZERO;
+        if (dto.getCouponId() != null) {
+            // 检查桌号是否已使用优惠券(考虑换桌情况)
+            if (cartService.hasUsedCoupon(dto.getTableId())) {
+                throw new RuntimeException("该桌已使用优惠券,不能重复使用");
+            }
+
+            // 验证优惠券
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(dto.getCouponId());
+            if (coupon == null) {
+                throw new RuntimeException("优惠券不存在");
+            }
+
+            // 验证优惠券是否属于该门店
+            if (!coupon.getStoreId().equals(String.valueOf(table.getStoreId()))) {
+                throw new RuntimeException("优惠券不属于该门店");
+            }
+
+            // 计算餐具费(用于验证最低消费)
+            BigDecimal tablewareUnitPrice = new BigDecimal("1.00");
+            BigDecimal tablewareFee = BigDecimal.ZERO;
+            if (dto.getDinerCount() != null && dto.getDinerCount() > 0) {
+                tablewareFee = tablewareUnitPrice.multiply(BigDecimal.valueOf(dto.getDinerCount()));
+            }
+
+            // 验证最低消费(菜品总价 + 餐具费)
+            BigDecimal totalWithTableware = cart.getTotalAmount().add(tablewareFee);
+            if (coupon.getMinimumSpendingAmount() != null
+                    && totalWithTableware.compareTo(coupon.getMinimumSpendingAmount()) < 0) {
+                throw new RuntimeException("订单金额未达到优惠券最低消费要求");
+            }
+
+            // 计算优惠金额
+            discountAmount = coupon.getNominalValue();
+            if (discountAmount.compareTo(totalWithTableware) > 0) {
+                discountAmount = totalWithTableware;
+            }
+
+            // 标记桌号已使用优惠券
+            cartService.markCouponUsed(dto.getTableId(), dto.getCouponId());
+        }
+
+        // 计算餐具费(默认1元/人,可从配置读取)
+        BigDecimal tablewareUnitPrice = new BigDecimal("1.00");
+        BigDecimal tablewareFee = BigDecimal.ZERO;
+        if (dto.getDinerCount() != null && dto.getDinerCount() > 0) {
+            tablewareFee = tablewareUnitPrice.multiply(BigDecimal.valueOf(dto.getDinerCount()));
+        }
+
+        // 计算实付金额(菜品总价 + 餐具费 - 优惠金额)
+        BigDecimal payAmount = cart.getTotalAmount().add(tablewareFee).subtract(discountAmount);
+
+        // 生成订单号
+        String orderNo = generateOrderNo();
+
+        // 创建订单
+        StoreOrder order = new StoreOrder();
+        order.setOrderNo(orderNo);
+        order.setStoreId(table.getStoreId());
+        order.setTableId(table.getId());
+        order.setTableNumber(table.getTableNumber());
+        order.setDinerCount(dto.getDinerCount());
+        order.setContactPhone(dto.getContactPhone());
+        order.setTablewareFee(tablewareFee);
+        order.setPayUserId(userId);
+        order.setPayUserPhone(userPhone);
+        order.setOrderStatus(0); // 待支付
+        order.setTotalAmount(cart.getTotalAmount());
+        order.setCouponId(dto.getCouponId());
+        order.setCurrentCouponId(dto.getCouponId()); // 记录当前使用的优惠券
+        order.setDiscountAmount(discountAmount);
+        order.setPayAmount(payAmount);
+        
+        // 如果immediatePay为0,只创建订单不支付;为1则创建订单并支付
+        // 暂时不实现立即支付,由前端调用支付接口
+        // payType在支付时设置
+        order.setPayStatus(0); // 未支付
+        order.setRemark(dto.getRemark());
+        order.setCreatedUserId(userId);
+        this.save(order);
+
+        // 更新优惠券使用记录状态为已下单
+        if (dto.getCouponId() != null) {
+            LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
+            usageWrapper.eq(StoreCouponUsage::getTableId, dto.getTableId());
+            usageWrapper.eq(StoreCouponUsage::getCouponId, dto.getCouponId());
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setOrderId(order.getId());
+                usage.setUsageStatus(1); // 已下单
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            }
+        }
+
+        // 创建订单明细
+        List<StoreOrderDetail> orderDetails = cart.getItems().stream()
+                .map(item -> {
+                    StoreOrderDetail detail = new StoreOrderDetail();
+                    detail.setOrderId(order.getId());
+                    detail.setOrderNo(orderNo);
+                    detail.setCuisineId(item.getCuisineId());
+                    detail.setCuisineName(item.getCuisineName());
+                    detail.setCuisineType(item.getCuisineType());
+                    detail.setCuisineImage(item.getCuisineImage());
+                    detail.setUnitPrice(item.getUnitPrice());
+                    detail.setQuantity(item.getQuantity());
+                    detail.setSubtotalAmount(item.getSubtotalAmount());
+                    detail.setAddUserId(item.getAddUserId());
+                    detail.setAddUserPhone(item.getAddUserPhone());
+                    detail.setRemark(item.getRemark());
+                    detail.setCreatedUserId(userId);
+                    return detail;
+                })
+                .collect(Collectors.toList());
+
+        for (StoreOrderDetail detail : orderDetails) {
+            orderDetailMapper.insert(detail);
+        }
+
+        // 更新桌号的当前订单ID
+        table.setCurrentOrderId(order.getId());
+        table.setStatus(1); // 就餐中
+        storeTableMapper.updateById(table);
+
+        // 下单后不清空购物车,允许加餐(加餐时添加到同一订单)
+        // 只有在支付完成后才清空购物车
+
+        log.info("订单创建成功, orderId={}, orderNo={}", order.getId(), orderNo);
+        return order;
+    }
+
+    @Override
+    public StoreOrder payOrder(Integer orderId, Integer payType) {
+        log.info("支付订单, orderId={}, payType={}", orderId, payType);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        if (order.getOrderStatus() != 0) {
+            throw new RuntimeException("订单状态不正确,无法支付");
+        }
+
+        // 这里可以调用支付接口,暂时直接更新为已支付
+        order.setOrderStatus(1); // 已支付
+        order.setPayStatus(1); // 已支付
+        order.setPayType(payType);
+        order.setPayTime(new Date());
+        order.setPayTradeNo("TRADE_" + System.currentTimeMillis()); // 模拟交易号
+        order.setUpdatedTime(new Date());
+
+        // 获取当前用户信息
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        if (userInfo != null) {
+            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        }
+
+        this.updateById(order);
+
+        // 更新优惠券使用记录状态为已支付
+        if (order.getCouponId() != null) {
+            LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
+            usageWrapper.eq(StoreCouponUsage::getOrderId, orderId);
+            usageWrapper.eq(StoreCouponUsage::getCouponId, order.getCouponId());
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setUsageStatus(2); // 已支付
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            }
+        }
+
+        // 支付成功后,清空购物车
+        cartService.clearCart(order.getTableId());
+
+        log.info("订单支付成功, orderId={}", orderId);
+        return order;
+    }
+
+    @Override
+    public boolean cancelOrder(Integer orderId) {
+        log.info("取消订单, orderId={}", orderId);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        if (order.getOrderStatus() != 0) {
+            throw new RuntimeException("订单状态不正确,无法取消");
+        }
+
+        order.setOrderStatus(2); // 已取消
+        order.setUpdatedTime(new Date());
+
+        // 获取当前用户信息
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        if (userInfo != null) {
+            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        }
+
+        this.updateById(order);
+
+        // 更新桌号状态
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            table.setStatus(0); // 空闲
+            storeTableMapper.updateById(table);
+        }
+
+        // 清除优惠券使用标记
+        cartService.clearCouponUsed(order.getTableId());
+
+        log.info("订单取消成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public StoreOrder getOrderByOrderNo(String orderNo) {
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getOrderNo, orderNo);
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public StoreOrder getOrderById(Integer orderId) {
+        return this.getById(orderId);
+    }
+
+    @Override
+    public IPage<StoreOrder> getOrderPage(Page<StoreOrder> page, Integer storeId, Integer tableId, Integer orderStatus) {
+        LambdaQueryWrapper<StoreOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreOrder::getDeleteFlag, 0);
+        if (storeId != null) {
+            wrapper.eq(StoreOrder::getStoreId, storeId);
+        }
+        if (tableId != null) {
+            wrapper.eq(StoreOrder::getTableId, tableId);
+        }
+        if (orderStatus != null) {
+            wrapper.eq(StoreOrder::getOrderStatus, orderStatus);
+        }
+        wrapper.orderByDesc(StoreOrder::getCreatedTime);
+        return this.page(page, wrapper);
+    }
+
+    @Override
+    public StoreOrder addDishToOrder(Integer orderId, Integer cuisineId, Integer quantity, String remark) {
+        log.info("加餐, orderId={}, cuisineId={}, quantity={}", orderId, cuisineId, quantity);
+
+        // 验证订单
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        if (order.getOrderStatus() != 0) {
+            throw new RuntimeException("订单状态不正确,无法加餐");
+        }
+
+        // 验证菜品
+        StoreCuisine cuisine = storeCuisineMapper.selectById(cuisineId);
+        if (cuisine == null) {
+            throw new RuntimeException("菜品不存在");
+        }
+        if (cuisine.getShelfStatus() != 1) {
+            throw new RuntimeException("菜品已下架");
+        }
+
+        // 获取当前用户信息
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+        String userPhone = userInfo != null ? userInfo.getString("phone") : null;
+
+        // 查询是否已有该菜品
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(StoreOrderDetail::getCuisineId, cuisineId);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        StoreOrderDetail existingDetail = orderDetailMapper.selectOne(detailWrapper);
+
+        Date now = new Date();
+        if (existingDetail != null) {
+            // 更新数量
+            existingDetail.setQuantity(existingDetail.getQuantity() + quantity);
+            existingDetail.setSubtotalAmount(existingDetail.getUnitPrice()
+                    .multiply(BigDecimal.valueOf(existingDetail.getQuantity())));
+            if (StringUtils.hasText(remark)) {
+                existingDetail.setRemark(remark);
+            }
+            // 如果之前不是加餐,现在标记为加餐
+            if (existingDetail.getIsAddDish() == null || existingDetail.getIsAddDish() == 0) {
+                existingDetail.setIsAddDish(1); // 标记为加餐
+                existingDetail.setAddDishTime(now);
+            }
+            existingDetail.setUpdatedTime(now);
+            existingDetail.setUpdatedUserId(userId);
+            orderDetailMapper.updateById(existingDetail);
+        } else {
+            // 添加新菜品(加餐)
+            StoreOrderDetail detail = new StoreOrderDetail();
+            detail.setOrderId(orderId);
+            detail.setOrderNo(order.getOrderNo());
+            detail.setCuisineId(cuisine.getId());
+            detail.setCuisineName(cuisine.getName());
+            detail.setCuisineType(cuisine.getCuisineType());
+            detail.setCuisineImage(cuisine.getImages());
+            detail.setUnitPrice(cuisine.getTotalPrice());
+            detail.setQuantity(quantity);
+            detail.setSubtotalAmount(cuisine.getTotalPrice().multiply(BigDecimal.valueOf(quantity)));
+            detail.setAddUserId(userId);
+            detail.setAddUserPhone(userPhone);
+            detail.setIsAddDish(1); // 标记为加餐
+            detail.setAddDishTime(now);
+            detail.setRemark(remark);
+            detail.setCreatedUserId(userId);
+            orderDetailMapper.insert(detail);
+        }
+
+        // 重新计算订单总金额
+        LambdaQueryWrapper<StoreOrderDetail> allDetailWrapper = new LambdaQueryWrapper<>();
+        allDetailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+        allDetailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        List<StoreOrderDetail> allDetails = orderDetailMapper.selectList(allDetailWrapper);
+        BigDecimal newTotalAmount = allDetails.stream()
+                .map(StoreOrderDetail::getSubtotalAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        // 重新计算实付金额(菜品总价 + 餐具费 - 优惠金额)
+        BigDecimal newPayAmount = newTotalAmount.add(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO)
+                .subtract(order.getDiscountAmount() != null ? order.getDiscountAmount() : BigDecimal.ZERO);
+
+        order.setTotalAmount(newTotalAmount);
+        order.setPayAmount(newPayAmount);
+        order.setUpdatedTime(new Date());
+        order.setUpdatedUserId(userId);
+        this.updateById(order);
+
+        log.info("加餐成功, orderId={}", orderId);
+        return order;
+    }
+
+    @Override
+    public boolean completeOrder(Integer orderId) {
+        log.info("完成订单, orderId={}", orderId);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        if (order.getPayStatus() != 1) {
+            throw new RuntimeException("订单未支付,无法完成");
+        }
+
+        order.setOrderStatus(3); // 已完成
+        order.setUpdatedTime(new Date());
+
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        if (userInfo != null) {
+            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        }
+
+        this.updateById(order);
+
+        // 更新桌号状态
+        StoreTable table = storeTableMapper.selectById(order.getTableId());
+        if (table != null) {
+            table.setCurrentOrderId(null);
+            table.setStatus(0); // 空闲
+            storeTableMapper.updateById(table);
+        }
+
+        log.info("订单完成成功, orderId={}", orderId);
+        return true;
+    }
+
+    @Override
+    public StoreOrder updateOrderCoupon(Integer orderId, Integer couponId) {
+        log.info("更新订单优惠券, orderId={}, couponId={}", orderId, couponId);
+
+        StoreOrder order = this.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+
+        if (order.getOrderStatus() != 0) {
+            throw new RuntimeException("订单状态不正确,无法修改优惠券");
+        }
+
+        BigDecimal discountAmount = BigDecimal.ZERO;
+
+        if (couponId != null) {
+            // 验证优惠券
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+            if (coupon == null) {
+                throw new RuntimeException("优惠券不存在");
+            }
+
+            // 验证优惠券是否属于该门店
+            if (!coupon.getStoreId().equals(String.valueOf(order.getStoreId()))) {
+                throw new RuntimeException("优惠券不属于该门店");
+            }
+
+            // 验证最低消费(菜品总价 + 餐具费)
+            BigDecimal totalWithTableware = order.getTotalAmount().add(order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO);
+            if (coupon.getMinimumSpendingAmount() != null
+                    && totalWithTableware.compareTo(coupon.getMinimumSpendingAmount()) < 0) {
+                throw new RuntimeException("订单金额未达到优惠券最低消费要求");
+            }
+
+            // 计算优惠金额
+            discountAmount = coupon.getNominalValue();
+            BigDecimal totalAmount = totalWithTableware;
+            if (discountAmount.compareTo(totalAmount) > 0) {
+                discountAmount = totalAmount;
+            }
+        }
+
+        // 如果之前有优惠券,更新使用记录状态为已取消
+        if (order.getCouponId() != null && (couponId == null || !order.getCouponId().equals(couponId))) {
+            LambdaQueryWrapper<StoreCouponUsage> oldUsageWrapper = new LambdaQueryWrapper<>();
+            oldUsageWrapper.eq(StoreCouponUsage::getOrderId, orderId);
+            oldUsageWrapper.eq(StoreCouponUsage::getCouponId, order.getCouponId());
+            oldUsageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            oldUsageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            oldUsageWrapper.last("LIMIT 1");
+            StoreCouponUsage oldUsage = storeCouponUsageMapper.selectOne(oldUsageWrapper);
+            if (oldUsage != null) {
+                oldUsage.setUsageStatus(3); // 已取消
+                oldUsage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(oldUsage);
+            }
+        }
+
+        // 如果新设置了优惠券,更新或创建使用记录
+        if (couponId != null) {
+            LambdaQueryWrapper<StoreCouponUsage> usageWrapper = new LambdaQueryWrapper<>();
+            usageWrapper.eq(StoreCouponUsage::getTableId, order.getTableId());
+            usageWrapper.eq(StoreCouponUsage::getCouponId, couponId);
+            usageWrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
+            usageWrapper.orderByDesc(StoreCouponUsage::getCreatedTime);
+            usageWrapper.last("LIMIT 1");
+            StoreCouponUsage usage = storeCouponUsageMapper.selectOne(usageWrapper);
+            if (usage != null) {
+                usage.setOrderId(orderId);
+                usage.setUsageStatus(1); // 已下单
+                usage.setDiscountAmount(discountAmount);
+                usage.setUpdatedTime(new Date());
+                storeCouponUsageMapper.updateById(usage);
+            } else {
+                // 创建新的使用记录
+                StoreTable table = storeTableMapper.selectById(order.getTableId());
+                if (table != null) {
+                    StoreCouponUsage newUsage = new StoreCouponUsage();
+                    newUsage.setTableId(order.getTableId());
+                    newUsage.setStoreId(order.getStoreId());
+                    newUsage.setOrderId(orderId);
+                    newUsage.setCouponId(couponId);
+                    newUsage.setDiscountAmount(discountAmount);
+                    newUsage.setUsageStatus(1); // 已下单
+                    newUsage.setCreatedTime(new Date());
+                    com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                    if (userInfo != null) {
+                        newUsage.setCreatedUserId(userInfo.getInteger("userId"));
+                    }
+                    storeCouponUsageMapper.insert(newUsage);
+                }
+            }
+        }
+
+        // 重新计算实付金额(菜品总价 + 餐具费 - 优惠金额)
+        BigDecimal tablewareFee = order.getTablewareFee() != null ? order.getTablewareFee() : BigDecimal.ZERO;
+        BigDecimal payAmount = order.getTotalAmount().add(tablewareFee).subtract(discountAmount);
+
+        order.setCouponId(couponId);
+        order.setCurrentCouponId(couponId);
+        order.setDiscountAmount(discountAmount);
+        order.setPayAmount(payAmount);
+        order.setUpdatedTime(new Date());
+
+        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+        if (userInfo != null) {
+            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        }
+
+        this.updateById(order);
+
+        log.info("更新订单优惠券成功, orderId={}", orderId);
+        return order;
+    }
+
+    /**
+     * 生成订单号
+     */
+    private String generateOrderNo() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        String timestamp = sdf.format(new Date());
+        String random = String.valueOf((int) (Math.random() * 10000));
+        return "ORD" + timestamp + String.format("%04d", Integer.parseInt(random));
+    }
+}

+ 175 - 287
alien-store/src/main/java/shop/alien/store/service/impl/StorePlatformMenuServiceImpl.java

@@ -254,6 +254,10 @@ public class StorePlatformMenuServiceImpl extends ServiceImpl<StorePlatformMenuM
                     .eq(StorePlatformMenu::getParentId, currentMenu.getParentId())
                     .eq(StorePlatformMenu::getDelFlag, "0");
             StorePlatformMenu duplicateMenu = storePlatformMenuMapper.selectOne(nameCheck);
+            if (duplicateMenu.getMenuId().equals(menuId)){
+                editUpdateMenuStatus(menuId,currentMenu.getStatus(),currentMenu.getLevel());
+                return R.data(currentMenu);
+            }
             if (duplicateMenu != null) {
                 log.warn("更新菜单失败:菜单名称已存在,菜单名称={}, parentId={}",
                         currentMenu.getMenuName(), currentMenu.getParentId());
@@ -339,157 +343,87 @@ public class StorePlatformMenuServiceImpl extends ServiceImpl<StorePlatformMenuM
         
         // 根据层级进行级联删除
         if (level == 1) {
-            // 删除层级1的菜单:同时删除层级2和3的菜单
-            log.info("删除一级菜单,同时删除层级2和3的菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level1Query = new LambdaQueryWrapper<>();
-            level1Query
-                    .eq(StorePlatformMenu::getLevel, 1)
-                    .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> levelMenus = storePlatformMenuMapper.selectList(level1Query);
-            List<Long> levelMenuIds = levelMenus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            if (!CollectionUtils.isEmpty(levelMenus)){
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 1)
-                        .in(StorePlatformMenu::getMenuId, levelMenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count;
-                log.info("删除一级菜单本身,菜单ID={}, 更新记录数={}", menuId, count);
-            }
-            // 1. 先查询所有层级2的子菜单
-            LambdaQueryWrapper<StorePlatformMenu> level2Query = new LambdaQueryWrapper<>();
-            level2Query
-                      .eq(StorePlatformMenu::getLevel, 2)
-                      .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> level2Menus = storePlatformMenuMapper.selectList(level2Query);
-            List<Long> level2MenuIds = level2Menus.stream()
-                                                  .map(StorePlatformMenu::getMenuId)
-                                                  .collect(Collectors.toList());
-            log.info("查询到层级2子菜单数量:{}", level2Menus != null ? level2Menus.size() : 0);
-
-
-            // 3. 删除所有层级2子菜单
-            if (!CollectionUtils.isEmpty(level2Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 2)
-                        .in(StorePlatformMenu::getMenuId, level2MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("删除层级2子菜单,parent_id={}, 更新记录数={}", menuId, count2);
-            } else {
-                log.info("没有层级2子菜单需要删除,菜单ID={}", menuId);
-            }
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query
-                    .eq(StorePlatformMenu::getLevel, 3)
-                    .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            // 4. 删除所有层级3子菜单(parent_id在层级2菜单ID列表中)
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper3 = new LambdaUpdateWrapper<>();
-                wrapper3.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count3 = storePlatformMenuMapper.update(null, wrapper3);
-                totalUpdateCount += count3;
-                log.info("删除层级3子菜单, 更新记录数={}", level3MenuIds, count3);
-            } else {
-                log.info("没有层级3子菜单需要删除,菜单ID={}", menuId);
-            }
+            // 删除一级菜单:删除本级和所有子级(二级和三级)
+            log.info("删除一级菜单,同时删除所有子级菜单,菜单ID={}, 层级={}", menuId, level);
             
-            log.info("删除一级菜单成功,菜单ID={}, 层级={}, 总更新记录数={}", menuId, level, totalUpdateCount);
+            // 1. 查找所有子菜单ID(包括二级和三级)
+            List<Long> allChildrenIds = findAllChildrenMenuIds(menuId);
             
-        } else if (level == 2) {
-            // 删除层级2的菜单:同时删除层级3的菜单
-            log.info("删除二级菜单,同时删除层级3的菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level2Query = new LambdaQueryWrapper<>();
-            level2Query
-                    .eq(StorePlatformMenu::getLevel, 2)
-                    .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> level2Menus = storePlatformMenuMapper.selectList(level2Query);
-            List<Long> level2MenuIds = level2Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            // 3. 删除所有层级2子菜单
-            if (!CollectionUtils.isEmpty(level2Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 2)
-                        .in(StorePlatformMenu::getMenuId, level2MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("删除层级2子菜单,菜单ID={}, 更新记录数={}", menuId, count2);
+            // 2. 构建要删除的菜单ID列表(包括本级和所有子级)
+            List<Long> menuIdsToDelete = new ArrayList<>();
+            menuIdsToDelete.add(menuId);
+            menuIdsToDelete.addAll(allChildrenIds);
+            
+            // 3. 批量删除(逻辑删除)
+            if (!CollectionUtils.isEmpty(menuIdsToDelete)) {
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.in(StorePlatformMenu::getMenuId, menuIdsToDelete)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getDelFlag, "2");
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("删除一级菜单及其所有子菜单成功,菜单ID={}, 子菜单数量={}, 总更新记录数={}", 
+                        menuId, allChildrenIds.size(), totalUpdateCount);
             } else {
-                log.info("没有层级2子菜单需要删除,菜单ID={}", menuId);
+                // 只删除本级菜单
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getDelFlag, "2");
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("删除一级菜单成功(无子菜单),菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
             }
-
-            // 1. 先查询所有层级3的子菜单
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query
-                      .eq(StorePlatformMenu::getLevel, 3)
-                      .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            log.info("查询到层级3子菜单数量:{}", level3Menus != null ? level3Menus.size() : 0);
             
-            // 3. 删除所有层级3子菜单
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("删除层级3子菜单,菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
+        } else if (level == 2) {
+            // 删除二级菜单:删除本级和所有子级(三级)
+            log.info("删除二级菜单,同时删除所有子级菜单,菜单ID={}, 层级={}", menuId, level);
+            
+            // 1. 查找所有子菜单ID(三级菜单)
+            List<Long> allChildrenIds = findAllChildrenMenuIds(menuId);
+            
+            // 2. 构建要删除的菜单ID列表(包括本级和所有子级)
+            List<Long> menuIdsToDelete = new ArrayList<>();
+            menuIdsToDelete.add(menuId);
+            menuIdsToDelete.addAll(allChildrenIds);
+            
+            // 3. 批量删除(逻辑删除)
+            if (!CollectionUtils.isEmpty(menuIdsToDelete)) {
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.in(StorePlatformMenu::getMenuId, menuIdsToDelete)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getDelFlag, "2");
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("删除二级菜单及其所有子菜单成功,菜单ID={}, 子菜单数量={}, 总更新记录数={}", 
+                        menuId, allChildrenIds.size(), totalUpdateCount);
             } else {
-                log.info("没有层级3子菜单需要删除,菜单ID={}", menuId);
+                // 只删除本级菜单
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getDelFlag, "2");
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("删除二级菜单成功(无子菜单),菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
             }
-
             
         } else if (level == 3) {
-            // 删除层级3的菜单:只删除当前菜单
+            // 删除三级菜单:只删除本级
             log.info("删除三级菜单,只删除当前菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query
-                    .eq(StorePlatformMenu::getLevel, 3)
-                    .eq(StorePlatformMenu::getDelFlag, "0");
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            log.info("查询到层级3子菜单数量:{}", level3Menus != null ? level3Menus.size() : 0);
-
-            // 3. 删除所有层级3子菜单
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getDelFlag, "2");
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("删除层级3子菜单,parent_id={}, 更新记录数={}", menuId, count2);
-            } else {
-                log.info("没有层级3子菜单需要删除,菜单ID={}", menuId);
-            }
+            
+            LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+            wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                   .eq(StorePlatformMenu::getDelFlag, "0")
+                   .set(StorePlatformMenu::getDelFlag, "2");
+            int count = storePlatformMenuMapper.update(null, wrapper);
+            totalUpdateCount = count;
             
             if (totalUpdateCount > 0) {
                 log.info("删除三级菜单成功,菜单ID={}, 层级={}, 更新记录数={}", menuId, level, totalUpdateCount);
             } else {
-                log.warn("删除三级菜单未找到记录或已删除,菜单ID={}, 层级={}, del_flag={}", menuId, level, currentMenu.getDelFlag());
+                log.warn("删除三级菜单未找到记录或已删除,菜单ID={}, 层级={}, del_flag={}", 
+                        menuId, level, currentMenu.getDelFlag());
                 return R.fail("删除失败:菜单不存在或已被删除");
             }
             
@@ -537,170 +471,94 @@ public class StorePlatformMenuServiceImpl extends ServiceImpl<StorePlatformMenuM
         }
 
 
-        // 根据层级进行级联删除
+        // 根据层级进行级联更新状态
         if (level == 1) {
-            // 删除层级1的菜单:同时删除层级2和3的菜单
-            log.info("启用禁用一级菜单,同时启用禁用层级2和3的菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level1Query = new LambdaQueryWrapper<>();
-            level1Query
-                    .eq(StorePlatformMenu::getLevel, 1)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> levelMenus = storePlatformMenuMapper.selectList(level1Query);
-            List<Long> levelMenuIds = levelMenus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            if (!CollectionUtils.isEmpty(levelMenus)){
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 1)
-                        .in(StorePlatformMenu::getMenuId, levelMenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count;
-                log.info("启用禁用一级菜单本身,菜单ID={}, 更新记录数={}", menuId, count);
-            }
-            // 1. 先查询所有层级2的子菜单
-            LambdaQueryWrapper<StorePlatformMenu> level2Query = new LambdaQueryWrapper<>();
-            level2Query
-                    .eq(StorePlatformMenu::getLevel, 2)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> level2Menus = storePlatformMenuMapper.selectList(level2Query);
-            List<Long> level2MenuIds = level2Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            log.info("查询到层级2子菜单数量:{}", level2Menus != null ? level2Menus.size() : 0);
-
-
-            // 3. 删除所有层级2子菜单
-            if (!CollectionUtils.isEmpty(level2Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 2)
-                        .in(StorePlatformMenu::getMenuId, level2MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("启用禁用层级2子菜单,菜单Id={}, 更新记录数={}", menuId, totalUpdateCount);
-            } else {
-                log.info("没有层级2子菜单需要启用禁用,菜单ID={}", menuId);
-            }
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query
-                    .eq(StorePlatformMenu::getLevel, 3)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            // 4. 删除所有层级3子菜单(parent_id在层级2菜单ID列表中)
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper3 = new LambdaUpdateWrapper<>();
-                wrapper3.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count3 = storePlatformMenuMapper.update(null, wrapper3);
-                totalUpdateCount += count3;
-                log.info("启用禁用层级3子菜单, 更新记录数={}", level3MenuIds, count3);
+            // 关闭/开启一级菜单:关闭/开启本级和所有子级(二级和三级)
+            log.info("关闭/开启一级菜单,同时关闭/开启所有子级菜单,菜单ID={}, 层级={}, 新状态={}", menuId, level, newStatus);
+            
+            // 1. 查找所有子菜单ID(包括二级和三级)
+            List<Long> allChildrenIds = findAllChildrenMenuIds(menuId);
+            
+            // 2. 构建要更新状态的菜单ID列表(包括本级和所有子级)
+            List<Long> menuIdsToUpdate = new ArrayList<>();
+            menuIdsToUpdate.add(menuId);
+            menuIdsToUpdate.addAll(allChildrenIds);
+            
+            // 3. 批量更新状态
+            if (!CollectionUtils.isEmpty(menuIdsToUpdate)) {
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.in(StorePlatformMenu::getMenuId, menuIdsToUpdate)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getStatus, newStatus);
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("关闭/开启一级菜单及其所有子菜单成功,菜单ID={}, 子菜单数量={}, 总更新记录数={}", 
+                        menuId, allChildrenIds.size(), totalUpdateCount);
             } else {
-                log.info("没有层级3子菜单需要操作,菜单ID={}", menuId);
+                // 只更新本级菜单状态
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getStatus, newStatus);
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("关闭/开启一级菜单成功(无子菜单),菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
             }
 
-            log.info("操作一级菜单成功,菜单ID={}, 层级={}, 总更新记录数={}", menuId, level, totalUpdateCount);
-
         } else if (level == 2) {
-            // 删除层级2的菜单:同时删除层级3的菜单
-            log.info("启用禁用二级菜单,同时启用禁用层级3的菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level2Query = new LambdaQueryWrapper<>();
-            level2Query
-                    .eq(StorePlatformMenu::getLevel, 2)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> level2Menus = storePlatformMenuMapper.selectList(level2Query);
-            List<Long> level2MenuIds = level2Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            // 3. 删除所有层级2子菜单
-            if (!CollectionUtils.isEmpty(level2Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 2)
-                        .in(StorePlatformMenu::getMenuId, level2MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("启用禁用层级2子菜单,菜单ID={}, 更新记录数={}", menuId, count2);
-            } else {
-                log.info("没有层级2子菜单需要操作,菜单ID={}", menuId);
-            }
-
-            // 1. 先查询所有层级3的子菜单
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query.eq(StorePlatformMenu::getMenuId, menuId)
-                    .eq(StorePlatformMenu::getLevel, 3)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            log.info("查询到层级3子菜单数量:{}", level3Menus != null ? level3Menus.size() : 0);
-
-            // 3. 删除所有层级3子菜单
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("启用禁用层级3子菜单,菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
+            // 关闭/开启二级菜单:关闭/开启本级和所有子级(三级)
+            log.info("关闭/开启二级菜单,同时关闭/开启所有子级菜单,菜单ID={}, 层级={}, 新状态={}", menuId, level, newStatus);
+            
+            // 1. 查找所有子菜单ID(三级菜单)
+            List<Long> allChildrenIds = findAllChildrenMenuIds(menuId);
+            
+            // 2. 构建要更新状态的菜单ID列表(包括本级和所有子级)
+            List<Long> menuIdsToUpdate = new ArrayList<>();
+            menuIdsToUpdate.add(menuId);
+            menuIdsToUpdate.addAll(allChildrenIds);
+            
+            // 3. 批量更新状态
+            if (!CollectionUtils.isEmpty(menuIdsToUpdate)) {
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.in(StorePlatformMenu::getMenuId, menuIdsToUpdate)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getStatus, newStatus);
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("关闭/开启二级菜单及其所有子菜单成功,菜单ID={}, 子菜单数量={}, 总更新记录数={}", 
+                        menuId, allChildrenIds.size(), totalUpdateCount);
             } else {
-                log.info("没有层级3子菜单需要操作,菜单ID={}", menuId);
+                // 只更新本级菜单状态
+                LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+                wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                       .eq(StorePlatformMenu::getDelFlag, "0")
+                       .set(StorePlatformMenu::getStatus, newStatus);
+                int count = storePlatformMenuMapper.update(null, wrapper);
+                totalUpdateCount = count;
+                log.info("关闭/开启二级菜单成功(无子菜单),菜单ID={}, 更新记录数={}", menuId, totalUpdateCount);
             }
 
-
         } else if (level == 3) {
-            // 删除层级3的菜单:只删除当前菜单
-            log.info("删除三级菜单,只删除当前菜单,菜单ID={}, 层级={}", menuId, level);
-            LambdaQueryWrapper<StorePlatformMenu> level3Query = new LambdaQueryWrapper<>();
-            level3Query.eq(StorePlatformMenu::getMenuId, menuId)
-                    .eq(StorePlatformMenu::getLevel, 3)
-                    .eq(StorePlatformMenu::getDelFlag, "0")
-                    .eq(StorePlatformMenu::getStatus, currentStatus);
-            List<StorePlatformMenu> level3Menus = storePlatformMenuMapper.selectList(level3Query);
-            List<Long> level3MenuIds = level3Menus.stream()
-                    .map(StorePlatformMenu::getMenuId)
-                    .collect(Collectors.toList());
-            log.info("查询到层级3子菜单数量:{}", level3Menus != null ? level3Menus.size() : 0);
-
-            // 3. 删除所有层级3子菜单
-            if (!CollectionUtils.isEmpty(level3Menus)) {
-                LambdaUpdateWrapper<StorePlatformMenu> wrapper2 = new LambdaUpdateWrapper<>();
-                wrapper2.eq(StorePlatformMenu::getLevel, 3)
-                        .in(StorePlatformMenu::getMenuId, level3MenuIds)
-                        .eq(StorePlatformMenu::getDelFlag, "0")
-                        .set(StorePlatformMenu::getStatus, newStatus);
-                int count2 = storePlatformMenuMapper.update(null, wrapper2);
-                totalUpdateCount += count2;
-                log.info("删除层级3子菜单,parent_id={}, 更新记录数={}", menuId, count2);
-            } else {
-                log.info("没有层级3子菜单需要删除,菜单ID={}", menuId);
-            }
-
+            // 关闭/开启三级菜单:只更新本级
+            log.info("关闭/开启三级菜单,只更新当前菜单,菜单ID={}, 层级={}, 新状态={}", menuId, level, newStatus);
+            
+            LambdaUpdateWrapper<StorePlatformMenu> wrapper = new LambdaUpdateWrapper<>();
+            wrapper.eq(StorePlatformMenu::getMenuId, menuId)
+                   .eq(StorePlatformMenu::getDelFlag, "0")
+                   .set(StorePlatformMenu::getStatus, newStatus);
+            int count = storePlatformMenuMapper.update(null, wrapper);
+            totalUpdateCount = count;
+            
             if (totalUpdateCount > 0) {
-                log.info("删除三级菜单成功,菜单ID={}, 层级={}, 更新记录数={}", menuId, level, totalUpdateCount);
+                log.info("关闭/开启三级菜单成功,菜单ID={}, 层级={}, 更新记录数={}", menuId, level, totalUpdateCount);
             } else {
-                log.warn("删除三级菜单未找到记录或已删除,菜单ID={}, 层级={}, del_flag={}", menuId, level, currentMenu.getDelFlag());
-                return R.fail("删除失败:菜单不存在或已被删除");
+                log.warn("关闭/开启三级菜单未找到记录或已删除,菜单ID={}, 层级={}, del_flag={}", 
+                        menuId, level, currentMenu.getDelFlag());
+                return R.fail("操作失败:菜单不存在或已被删除");
             }
 
         } else {
-            log.warn("删除菜单失败:层级值不正确,菜单ID={}, 层级={}", menuId, level);
+            log.warn("更新菜单状态失败:层级值不正确,菜单ID={}, 层级={}", menuId, level);
             return R.fail("层级值不正确,应为1、2或3");
         }
 
@@ -996,6 +854,36 @@ public class StorePlatformMenuServiceImpl extends ServiceImpl<StorePlatformMenuM
         }
     }
 
+    /**
+     * 递归查找所有子菜单ID(包括直接子菜单和间接子菜单)
+     * 
+     * @param parentId 父菜单ID
+     * @return 所有子菜单ID列表(包括子菜单的子菜单)
+     */
+    private List<Long> findAllChildrenMenuIds(Long parentId) {
+        List<Long> allChildrenIds = new ArrayList<>();
+        if (parentId == null) {
+            return allChildrenIds;
+        }
+        
+        // 查询直接子菜单
+        LambdaQueryWrapper<StorePlatformMenu> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StorePlatformMenu::getParentId, parentId)
+                   .eq(StorePlatformMenu::getDelFlag, "0");
+        List<StorePlatformMenu> children = storePlatformMenuMapper.selectList(queryWrapper);
+        
+        if (!CollectionUtils.isEmpty(children)) {
+            for (StorePlatformMenu child : children) {
+                Long childId = child.getMenuId();
+                allChildrenIds.add(childId);
+                // 递归查找子菜单的子菜单
+                List<Long> grandChildrenIds = findAllChildrenMenuIds(childId);
+                allChildrenIds.addAll(grandChildrenIds);
+            }
+        }
+        
+        return allChildrenIds;
+    }
 
 }
 

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

+ 72 - 7
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONArray;
 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.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
@@ -17,14 +18,12 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreStaffReviewDto;
 import shop.alien.entity.store.vo.*;
-import shop.alien.mapper.LifeLikeRecordMapper;
-import shop.alien.mapper.StoreStaffCommentMapper;
-import shop.alien.mapper.StoreStaffConfigMapper;
-import shop.alien.mapper.StoreStaffReviewMapper;
+import shop.alien.mapper.*;
 import shop.alien.store.service.StoreStaffCommentService;
 import shop.alien.store.service.StoreStaffReviewService;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.store.util.ai.AiVideoModerationUtil;
+import shop.alien.util.common.DistanceUtil;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -51,6 +50,8 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
     private final StoreStaffCommentMapper storeStaffCommentMapper;
     private final LifeLikeRecordMapper lifeLikeRecordMapper;
     private final StoreStaffConfigMapper storeStaffConfigMapper;
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreUserMapper storeUserMapper;
     private final AiContentModerationUtil aiContentModerationUtil;
     @Qualifier("commonVideoTaskExecutor")
     private final ExecutorService commonVideoTaskExecutor;
@@ -72,7 +73,11 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
         // AI审核
         // 一次遍历完成分类,避免多次流式处理
         Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(reviewDto.getReviewImages());
-        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(reviewDto.getReviewContent(), urlCategoryMap.get("image"));
+
+        AiContentModerationUtil.AuditResult auditResult = new AiContentModerationUtil.AuditResult(true, "");
+        if( StringUtils.isNotEmpty(reviewDto.getReviewContent()) || urlCategoryMap.get("image").size() > 0){
+            auditResult = aiContentModerationUtil.auditContent(reviewDto.getReviewContent(), urlCategoryMap.get("image"));
+        }
         if (!auditResult.isPassed()) {
             // 审核不通过
             StoreStaffReview staffReview = this.getById(review.getId());
@@ -154,8 +159,8 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
     }
 
     @Override
-    public R<StoreStaffReviewDetailVo> getReviewDetail(Integer reviewId, Integer currentUserId) {
-        log.info("获取评价详情, reviewId={}, currentUserId={}", reviewId, currentUserId);
+    public R<StoreStaffReviewDetailVo> getReviewDetail(Integer reviewId, Integer currentUserId, Double longitude, Double latitude) {
+        log.info("获取评价详情, reviewId={}, currentUserId={}, longitude={}, latitude={}", reviewId, currentUserId, longitude, latitude);
 
         if (reviewId == null) {
             return R.fail("评价ID不能为空");
@@ -183,19 +188,79 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
 
         // 查询人员信息
         StoreStaffConfig staffInfo = null;
+        Integer storeId = null;
         if (reviewVo.getStaffUserId() != null) {
             try {
                 staffInfo = storeStaffConfigMapper.selectById(reviewVo.getStaffUserId());
+                if (staffInfo != null) {
+                    storeId = staffInfo.getStoreId();
+                }
             } catch (Exception e) {
                 log.warn("查询人员信息失败, staffUserId={}, error={}", reviewVo.getStaffUserId(), e.getMessage());
             }
         }
 
+        // 查询店铺信息(包括店铺头像、评分和距离)
+        StoreStaffReviewDetailVo.StoreInfoForStaffReviewVo storeInfoVo = null;
+        if (storeId != null) {
+            try {
+                // 查询店铺基本信息
+                StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+                if (storeInfo != null) {
+                    storeInfoVo = new StoreStaffReviewDetailVo.StoreInfoForStaffReviewVo();
+                    storeInfoVo.setStoreId(storeId);
+                    storeInfoVo.setStoreName(storeInfo.getStoreName());
+                    
+                    // 设置店铺评分(平均评分)
+                    if (storeInfo.getScoreAvg() != null) {
+                        storeInfoVo.setScore(storeInfo.getScoreAvg());
+                    } else {
+                        storeInfoVo.setScore(null);
+                    }
+                    
+                    // 查询店铺头像(来自store_user表的head_img字段)
+                    LambdaQueryWrapper<StoreUser> userWrapper = new LambdaQueryWrapper<>();
+                    userWrapper.eq(StoreUser::getStoreId, storeId)
+                              .eq(StoreUser::getDeleteFlag, 0);
+                    StoreUser storeUser = storeUserMapper.selectOne(userWrapper);
+                    if (storeUser != null && storeUser.getHeadImg() != null && !storeUser.getHeadImg().trim().isEmpty()) {
+                        storeInfoVo.setHeadImg(storeUser.getHeadImg());
+                    } else {
+                        storeInfoVo.setHeadImg(null);
+                    }
+                    
+                    // 计算距离(如果提供了用户经纬度)
+                    if (longitude != null && latitude != null && storeInfo.getStorePosition() != null && !storeInfo.getStorePosition().trim().isEmpty()) {
+                        try {
+                            String[] positionArray = storeInfo.getStorePosition().split(",");
+                            if (positionArray.length >= 2) {
+                                double storeLon = Double.parseDouble(positionArray[0].trim());
+                                double storeLat = Double.parseDouble(positionArray[1].trim());
+                                // 计算距离(单位:公里)
+                                double distanceKm = DistanceUtil.haversineCalculateDistance(longitude, latitude, storeLon, storeLat);
+                                storeInfoVo.setDistance(distanceKm);
+                            } else {
+                                storeInfoVo.setDistance(null);
+                            }
+                        } catch (Exception e) {
+                            log.warn("计算店铺距离失败, storeId={}, error={}", storeId, e.getMessage());
+                            storeInfoVo.setDistance(null);
+                        }
+                    } else {
+                        storeInfoVo.setDistance(null);
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("查询店铺信息失败, storeId={}, error={}", storeId, e.getMessage());
+            }
+        }
+
         // 构建返回结果
         StoreStaffReviewDetailVo detailVo = new StoreStaffReviewDetailVo();
         detailVo.setReview(reviewVo);
         detailVo.setCommentList(comments != null ? comments : new ArrayList<>());
         detailVo.setStaffInfo(staffInfo);
+        detailVo.setStoreInfo(storeInfoVo);
 
         return R.data(detailVo);
     }

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

+ 3 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java

@@ -425,19 +425,10 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
                         .collect(Collectors.toList());
             }
 
-            // 当id不为空时,通过中间表storePlatformUserRole判断是否为子账号(必须是子账号
+            // 当id不为空时,对ID进行模糊查询(将ID转换为字符串进行模糊匹配
             if (StringUtils.isNotEmpty(id)) {
-                Integer parsedId = safeParseInt(id);
-                if (parsedId != null) {
-                    // 必须通过中间表判断,只有存在中间表中的才是子账号
-                    if (subAccountUserIds != null && subAccountUserIds.contains(parsedId)) {
-                        // 如果在中间表中存在,说明是子账号,添加ID条件
-                        subAccountWrapper.eq(StoreUser::getId, parsedId);
-                    } else {
-                        // 如果不在中间表中,添加一个无法匹配的条件,避免查询到非子账号
-                        subAccountWrapper.eq(StoreUser::getId, -1);
-                    }
-                }
+                // 使用apply方法,将ID转换为字符串进行模糊查询
+                subAccountWrapper.apply("CAST(id AS CHAR) LIKE {0}", "%" + id + "%");
             }
 
             // 当phone不为空时,对子账号和主账号电话进行模糊查询,并关联storePlatformUserRole

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

+ 195 - 0
订单系统完整表结构.sql

@@ -0,0 +1,195 @@
+-- =============================================
+-- 订单系统完整表结构(最终版本)
+-- 包含所有新增表和优化后的表结构
+-- =============================================
+
+-- =============================================
+-- 一、新增表
+-- =============================================
+
+-- 1. 购物车表
+CREATE TABLE IF NOT EXISTS `store_cart` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `cuisine_id` int(11) NOT NULL COMMENT '菜品ID',
+  `cuisine_name` varchar(200) NOT NULL COMMENT '菜品名称',
+  `cuisine_image` varchar(500) DEFAULT NULL COMMENT '菜品图片',
+  `unit_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
+  `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
+  `subtotal_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '小计金额',
+  `add_user_id` int(11) DEFAULT NULL COMMENT '添加该菜品的用户ID',
+  `add_user_phone` varchar(20) DEFAULT NULL COMMENT '添加该菜品的用户手机号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_cuisine_id` (`cuisine_id`),
+  KEY `idx_add_user_id` (`add_user_id`),
+  KEY `idx_created_time` (`created_time`),
+  KEY `idx_table_delete` (`table_id`, `delete_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
+
+-- 2. 优惠券使用记录表
+CREATE TABLE IF NOT EXISTS `store_coupon_usage` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `order_id` int(11) DEFAULT NULL COMMENT '订单ID',
+  `coupon_id` int(11) NOT NULL COMMENT '优惠券ID',
+  `coupon_name` varchar(200) DEFAULT NULL COMMENT '优惠券名称',
+  `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
+  `usage_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '使用状态(0:已标记使用, 1:已下单, 2:已支付, 3:已取消)',
+  `from_table_id` int(11) DEFAULT NULL COMMENT '换桌前的桌号ID',
+  `migrate_time` datetime DEFAULT NULL COMMENT '换桌迁移时间',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_table_coupon` (`table_id`, `coupon_id`, `delete_flag`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_coupon_id` (`coupon_id`),
+  KEY `idx_usage_status` (`usage_status`),
+  KEY `idx_created_time` (`created_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券使用记录表';
+
+-- 3. 订单锁定记录表
+CREATE TABLE IF NOT EXISTS `store_order_lock` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) DEFAULT NULL COMMENT '桌号ID',
+  `order_id` int(11) DEFAULT NULL COMMENT '订单ID',
+  `lock_type` tinyint(4) NOT NULL COMMENT '锁定类型(1:下单锁定, 2:结算锁定)',
+  `lock_user_id` int(11) NOT NULL COMMENT '锁定用户ID',
+  `lock_user_phone` varchar(20) DEFAULT NULL COMMENT '锁定用户手机号',
+  `lock_expire_time` datetime NOT NULL COMMENT '锁定过期时间',
+  `lock_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '锁定状态(0:已释放, 1:锁定中)',
+  `release_time` datetime DEFAULT NULL COMMENT '释放时间',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_lock_type` (`lock_type`),
+  KEY `idx_lock_status` (`lock_status`),
+  KEY `idx_lock_expire_time` (`lock_expire_time`),
+  KEY `idx_table_lock` (`table_id`, `lock_type`, `lock_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单锁定记录表';
+
+-- =============================================
+-- 二、订单表(store_order)- 完整结构
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS `store_order` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `order_no` varchar(64) NOT NULL COMMENT '订单号',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `table_number` varchar(50) DEFAULT NULL COMMENT '桌号',
+  `diner_count` int(11) DEFAULT NULL COMMENT '就餐人数',
+  `pay_user_id` int(11) DEFAULT NULL COMMENT '支付用户ID',
+  `pay_user_phone` varchar(20) DEFAULT NULL COMMENT '支付用户手机号',
+  `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
+  `tableware_fee` decimal(10,2) DEFAULT '0.00' COMMENT '餐具费',
+  `order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)',
+  `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额',
+  `coupon_id` int(11) DEFAULT NULL COMMENT '优惠券ID',
+  `current_coupon_id` int(11) DEFAULT NULL COMMENT '当前使用的优惠券ID',
+  `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
+  `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
+  `pay_type` tinyint(4) DEFAULT NULL COMMENT '支付方式(1:微信, 2:支付宝, 3:现金)',
+  `pay_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付状态(0:未支付, 1:已支付, 2:已退款)',
+  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
+  `pay_trade_no` varchar(128) DEFAULT NULL COMMENT '支付交易号',
+  `lock_user_id` int(11) DEFAULT NULL COMMENT '锁定用户ID',
+  `lock_expire_time` datetime DEFAULT NULL COMMENT '锁定过期时间',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_order_no` (`order_no`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_pay_user_id` (`pay_user_id`),
+  KEY `idx_order_status` (`order_status`),
+  KEY `idx_created_time` (`created_time`),
+  KEY `idx_current_coupon_id` (`current_coupon_id`),
+  KEY `idx_lock_user_id` (`lock_user_id`),
+  KEY `idx_table_status` (`table_id`, `order_status`),
+  KEY `idx_store_status` (`store_id`, `order_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
+
+-- =============================================
+-- 三、订单明细表(store_order_detail)- 完整结构
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS `store_order_detail` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `order_id` int(11) NOT NULL COMMENT '订单ID',
+  `order_no` varchar(64) NOT NULL COMMENT '订单号',
+  `cuisine_id` int(11) NOT NULL COMMENT '菜品ID',
+  `cuisine_name` varchar(200) NOT NULL COMMENT '菜品名称',
+  `cuisine_type` tinyint(4) NOT NULL COMMENT '菜品类型(1:单品, 2:套餐)',
+  `cuisine_image` varchar(500) DEFAULT NULL COMMENT '菜品图片',
+  `unit_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
+  `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
+  `subtotal_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '小计金额',
+  `add_user_id` int(11) DEFAULT NULL COMMENT '添加该菜品的用户ID',
+  `add_user_phone` varchar(20) DEFAULT NULL COMMENT '添加该菜品的用户手机号',
+  `is_add_dish` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否加餐(0:初始下单, 1:加餐)',
+  `add_dish_time` datetime DEFAULT NULL COMMENT '加餐时间',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_order_no` (`order_no`),
+  KEY `idx_cuisine_id` (`cuisine_id`),
+  KEY `idx_add_user_id` (`add_user_id`),
+  KEY `idx_created_time` (`created_time`),
+  KEY `idx_order_cuisine` (`order_id`, `cuisine_id`),
+  KEY `idx_is_add_dish` (`is_add_dish`),
+  KEY `idx_add_dish_time` (`add_dish_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
+
+-- =============================================
+-- 四、桌号表(store_table)- 新增字段说明
+-- =============================================
+-- 注意:此表为已存在的表,以下为新增字段和索引
+-- 如果表已存在,请使用 ALTER TABLE 语句添加字段
+
+-- 新增字段(如果表已存在,请执行以下SQL):
+-- ALTER TABLE `store_table` 
+--   ADD COLUMN `current_coupon_id` int(11) DEFAULT NULL COMMENT '当前使用的优惠券ID' AFTER `current_order_id`,
+--   ADD COLUMN `cart_item_count` int(11) DEFAULT '0' COMMENT '购物车商品数量' AFTER `current_coupon_id`,
+--   ADD COLUMN `cart_total_amount` decimal(10,2) DEFAULT '0.00' COMMENT '购物车总金额' AFTER `cart_item_count`;
+
+-- 新增索引(如果表已存在,请执行以下SQL):
+-- ALTER TABLE `store_table` 
+--   ADD INDEX `idx_current_coupon_id` (`current_coupon_id`),
+--   ADD INDEX `idx_store_status` (`store_id`, `status`);
+
+-- =============================================
+-- 表结构说明
+-- =============================================
+-- 1. store_cart: 购物车表,用于持久化购物车数据
+-- 2. store_coupon_usage: 优惠券使用记录表,记录每张桌子的优惠券使用情况
+-- 3. store_order_lock: 订单锁定记录表,记录下单和结算时的锁定信息
+-- 4. store_order: 订单表,包含所有订单信息(已优化)
+-- 5. store_order_detail: 订单明细表,包含订单明细信息(已优化)
+-- 6. store_table: 桌号表,包含桌号信息(需添加新字段)

+ 286 - 0
订单系统表结构说明.md

@@ -0,0 +1,286 @@
+# 订单系统表结构说明
+
+## 一、新增表
+
+### 1. 购物车表(store_cart)
+
+```sql
+CREATE TABLE `store_cart` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `cuisine_id` int(11) NOT NULL COMMENT '菜品ID',
+  `cuisine_name` varchar(200) NOT NULL COMMENT '菜品名称',
+  `cuisine_image` varchar(500) DEFAULT NULL COMMENT '菜品图片',
+  `unit_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
+  `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
+  `subtotal_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '小计金额',
+  `add_user_id` int(11) DEFAULT NULL COMMENT '添加该菜品的用户ID',
+  `add_user_phone` varchar(20) DEFAULT NULL COMMENT '添加该菜品的用户手机号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_cuisine_id` (`cuisine_id`),
+  KEY `idx_add_user_id` (`add_user_id`),
+  KEY `idx_created_time` (`created_time`),
+  KEY `idx_table_delete` (`table_id`, `delete_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
+```
+
+**说明:** 用于持久化购物车数据,Redis作为缓存层,避免Redis宕机导致数据丢失。
+
+---
+
+### 2. 优惠券使用记录表(store_coupon_usage)
+
+```sql
+CREATE TABLE `store_coupon_usage` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `order_id` int(11) DEFAULT NULL COMMENT '订单ID',
+  `coupon_id` int(11) NOT NULL COMMENT '优惠券ID',
+  `coupon_name` varchar(200) DEFAULT NULL COMMENT '优惠券名称',
+  `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
+  `usage_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '使用状态(0:已标记使用, 1:已下单, 2:已支付, 3:已取消)',
+  `from_table_id` int(11) DEFAULT NULL COMMENT '换桌前的桌号ID',
+  `migrate_time` datetime DEFAULT NULL COMMENT '换桌迁移时间',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_table_coupon` (`table_id`, `coupon_id`, `delete_flag`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_coupon_id` (`coupon_id`),
+  KEY `idx_usage_status` (`usage_status`),
+  KEY `idx_created_time` (`created_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券使用记录表';
+```
+
+**说明:** 记录每张桌子的优惠券使用情况,支持换桌场景的优惠券迁移。
+
+---
+
+### 3. 订单锁定记录表(store_order_lock)
+
+```sql
+CREATE TABLE `store_order_lock` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `table_id` int(11) DEFAULT NULL COMMENT '桌号ID',
+  `order_id` int(11) DEFAULT NULL COMMENT '订单ID',
+  `lock_type` tinyint(4) NOT NULL COMMENT '锁定类型(1:下单锁定, 2:结算锁定)',
+  `lock_user_id` int(11) NOT NULL COMMENT '锁定用户ID',
+  `lock_user_phone` varchar(20) DEFAULT NULL COMMENT '锁定用户手机号',
+  `lock_expire_time` datetime NOT NULL COMMENT '锁定过期时间',
+  `lock_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '锁定状态(0:已释放, 1:锁定中)',
+  `release_time` datetime DEFAULT NULL COMMENT '释放时间',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_lock_type` (`lock_type`),
+  KEY `idx_lock_status` (`lock_status`),
+  KEY `idx_lock_expire_time` (`lock_expire_time`),
+  KEY `idx_table_lock` (`table_id`, `lock_type`, `lock_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单锁定记录表';
+```
+
+**说明:** 记录下单和结算时的锁定信息,便于排查问题和数据恢复。
+
+---
+
+## 二、优化后的表
+
+### 4. 订单表(store_order)
+
+**基础字段:**
+```sql
+CREATE TABLE `store_order` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `order_no` varchar(64) NOT NULL COMMENT '订单号',
+  `store_id` int(11) NOT NULL COMMENT '门店ID',
+  `table_id` int(11) NOT NULL COMMENT '桌号ID',
+  `table_number` varchar(50) DEFAULT NULL COMMENT '桌号',
+  `diner_count` int(11) DEFAULT NULL COMMENT '就餐人数',
+  `pay_user_id` int(11) DEFAULT NULL COMMENT '支付用户ID',
+  `pay_user_phone` varchar(20) DEFAULT NULL COMMENT '支付用户手机号',
+  `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
+  `tableware_fee` decimal(10,2) DEFAULT '0.00' COMMENT '餐具费',
+  `order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)',
+  `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额',
+  `coupon_id` int(11) DEFAULT NULL COMMENT '优惠券ID',
+  `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
+  `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
+  `pay_type` tinyint(4) DEFAULT NULL COMMENT '支付方式(1:微信, 2:支付宝, 3:现金)',
+  `pay_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付状态(0:未支付, 1:已支付, 2:已退款)',
+  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
+  `pay_trade_no` varchar(128) DEFAULT NULL COMMENT '支付交易号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_order_no` (`order_no`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_table_id` (`table_id`),
+  KEY `idx_pay_user_id` (`pay_user_id`),
+  KEY `idx_order_status` (`order_status`),
+  KEY `idx_created_time` (`created_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
+```
+
+**新增字段:**
+- `current_coupon_id` int(11) DEFAULT NULL COMMENT '当前使用的优惠券ID' - 用于换桌场景
+- `lock_user_id` int(11) DEFAULT NULL COMMENT '锁定用户ID' - 下单或结算时锁定
+- `lock_expire_time` datetime DEFAULT NULL COMMENT '锁定过期时间' - 锁定过期时间
+
+**新增索引:**
+- `idx_current_coupon_id` (`current_coupon_id`)
+- `idx_lock_user_id` (`lock_user_id`)
+- `idx_table_status` (`table_id`, `order_status`)
+- `idx_store_status` (`store_id`, `order_status`)
+
+---
+
+### 5. 订单明细表(store_order_detail)
+
+**基础字段:**
+```sql
+CREATE TABLE `store_order_detail` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `order_id` int(11) NOT NULL COMMENT '订单ID',
+  `order_no` varchar(64) NOT NULL COMMENT '订单号',
+  `cuisine_id` int(11) NOT NULL COMMENT '菜品ID',
+  `cuisine_name` varchar(200) NOT NULL COMMENT '菜品名称',
+  `cuisine_type` tinyint(4) NOT NULL COMMENT '菜品类型(1:单品, 2:套餐)',
+  `cuisine_image` varchar(500) DEFAULT NULL COMMENT '菜品图片',
+  `unit_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
+  `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
+  `subtotal_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '小计金额',
+  `add_user_id` int(11) DEFAULT NULL COMMENT '添加该菜品的用户ID',
+  `add_user_phone` varchar(20) DEFAULT NULL COMMENT '添加该菜品的用户手机号',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `delete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `created_user_id` int(11) DEFAULT NULL COMMENT '创建人ID',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `updated_user_id` int(11) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_order_no` (`order_no`),
+  KEY `idx_cuisine_id` (`cuisine_id`),
+  KEY `idx_add_user_id` (`add_user_id`),
+  KEY `idx_created_time` (`created_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
+```
+
+**新增字段:**
+- `is_add_dish` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否加餐(0:初始下单, 1:加餐)' - 区分初始下单和加餐
+- `add_dish_time` datetime DEFAULT NULL COMMENT '加餐时间' - 加餐时间
+
+**新增索引:**
+- `idx_order_cuisine` (`order_id`, `cuisine_id`)
+- `idx_is_add_dish` (`is_add_dish`)
+- `idx_add_dish_time` (`add_dish_time`)
+
+---
+
+### 6. 桌号表(store_table)
+
+**说明:** 此表为已存在的表,新增以下字段和索引。
+
+**新增字段:**
+- `current_coupon_id` int(11) DEFAULT NULL COMMENT '当前使用的优惠券ID' - 便于查询和换桌迁移
+- `cart_item_count` int(11) DEFAULT '0' COMMENT '购物车商品数量' - 购物车商品数量(缓存)
+- `cart_total_amount` decimal(10,2) DEFAULT '0.00' COMMENT '购物车总金额' - 购物车总金额(缓存)
+
+**新增索引:**
+- `idx_current_coupon_id` (`current_coupon_id`)
+- `idx_store_status` (`store_id`, `status`)
+
+---
+
+## 三、表关系说明
+
+1. **store_order** ←→ **store_order_detail**:一对多关系,通过 `order_id` 关联
+2. **store_table** ←→ **store_order**:一对多关系,通过 `table_id` 关联
+3. **store_table** ←→ **store_cart**:一对多关系,通过 `table_id` 关联
+4. **store_table** ←→ **store_coupon_usage**:一对多关系,通过 `table_id` 关联
+5. **store_order** ←→ **store_coupon_usage**:一对一关系,通过 `order_id` 关联
+6. **store_table** ←→ **store_order_lock**:一对多关系,通过 `table_id` 关联(下单锁定)
+7. **store_order** ←→ **store_order_lock**:一对一关系,通过 `order_id` 关联(结算锁定)
+
+---
+
+## 四、字段说明
+
+### 订单状态(order_status)
+- `0`: 待支付
+- `1`: 已支付
+- `2`: 已取消
+- `3`: 已完成
+
+### 支付状态(pay_status)
+- `0`: 未支付
+- `1`: 已支付
+- `2`: 已退款
+
+### 支付方式(pay_type)
+- `1`: 微信
+- `2`: 支付宝
+- `3`: 现金
+
+### 优惠券使用状态(usage_status)
+- `0`: 已标记使用
+- `1`: 已下单
+- `2`: 已支付
+- `3`: 已取消
+
+### 锁定类型(lock_type)
+- `1`: 下单锁定
+- `2`: 结算锁定
+
+### 锁定状态(lock_status)
+- `0`: 已释放
+- `1`: 锁定中
+
+### 是否加餐(is_add_dish)
+- `0`: 初始下单
+- `1`: 加餐
+
+---
+
+## 五、索引说明
+
+### 主要查询场景索引:
+
+1. **按桌号查询订单**:`idx_table_id`, `idx_table_status`
+2. **按门店查询订单**:`idx_store_id`, `idx_store_status`
+3. **按订单号查询**:`uk_order_no`(唯一索引)
+4. **按用户查询订单**:`idx_pay_user_id`
+5. **按状态查询订单**:`idx_order_status`
+6. **按时间查询订单**:`idx_created_time`
+7. **购物车查询**:`idx_table_id`, `idx_table_delete`
+8. **优惠券查询**:`uk_table_coupon`(唯一索引),`idx_table_id`, `idx_coupon_id`
+9. **锁定查询**:`idx_table_lock`, `idx_lock_expire_time`
+
+---
+
+## 六、执行顺序
+
+1. 先执行 `store_order_tables.sql` 创建基础表(如果表不存在)
+2. 再执行 `store_order_optimization_final.sql` 进行优化(添加字段和索引)