Jelajahi Sumber

开发点餐模块

lutong 2 bulan lalu
induk
melakukan
c22c8dec62
38 mengubah file dengan 4463 tambahan dan 0 penghapusan
  1. 95 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java
  2. 88 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCouponUsage.java
  3. 141 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrder.java
  4. 108 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderDetail.java
  5. 76 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOrderLock.java
  6. 12 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTable.java
  7. 35 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddCartItemDTO.java
  8. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AddDishDTO.java
  9. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartDTO.java
  10. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java
  11. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/ChangeTableDTO.java
  12. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CreateOrderDTO.java
  13. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/UpdateOrderCouponDTO.java
  14. 40 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java
  15. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineComboItemVO.java
  16. 52 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineDetailVO.java
  17. 49 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CuisineListVO.java
  18. 31 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/DiningPageInfoVO.java
  19. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderConfirmVO.java
  20. 68 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSettlementVO.java
  21. 34 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderSuccessVO.java
  22. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCartMapper.java
  23. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCouponUsageMapper.java
  24. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderDetailMapper.java
  25. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderLockMapper.java
  26. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreOrderMapper.java
  27. 255 0
      alien-store/src/main/java/shop/alien/store/controller/DiningController.java
  28. 322 0
      alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java
  29. 88 0
      alien-store/src/main/java/shop/alien/store/service/CartService.java
  30. 134 0
      alien-store/src/main/java/shop/alien/store/service/DiningService.java
  31. 35 0
      alien-store/src/main/java/shop/alien/store/service/SseService.java
  32. 95 0
      alien-store/src/main/java/shop/alien/store/service/StoreOrderService.java
  33. 549 0
      alien-store/src/main/java/shop/alien/store/service/impl/CartServiceImpl.java
  34. 569 0
      alien-store/src/main/java/shop/alien/store/service/impl/DiningServiceImpl.java
  35. 152 0
      alien-store/src/main/java/shop/alien/store/service/impl/SseServiceImpl.java
  36. 583 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOrderServiceImpl.java
  37. 195 0
      订单系统完整表结构.sql
  38. 286 0
      订单系统表结构说明.md

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

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

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

@@ -37,6 +37,18 @@ public class StoreTable {
     @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;

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

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

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

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

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

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

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

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

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

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

+ 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,继续执行
+        }
+    }
+}

+ 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秒发送一次心跳
+    }
+}

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

+ 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` 进行优化(添加字段和索引)