Kaynağa Gözat

feat(store): 添加用户预约功能模块

- 新增用户预约实体类 UserReservation 和关联表实体 UserReservationTable
- 创建用户预约数据传输对象 DTO 和视图对象 VO
- 实现预约的增删改查接口和分页查询功能
- 添加预约与桌号关联的业务逻辑处理
- 集成 MyBatis-Plus 数据持久化操作
- 实现预约状态管理和时间格式化处理
fcw 1 ay önce
ebeveyn
işleme
6a89e14bec

+ 95 - 0
alien-entity/src/main/java/shop/alien/entity/store/UserReservation.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.util.Date;
+
+/**
+ * 用户预约信息表(订桌时间可往后延续)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("user_reservation")
+@ApiModel(value = "UserReservation对象", description = "用户预约信息表")
+public class UserReservation {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "预约号(券码)")
+    @TableField("reservation_no")
+    private String reservationNo;
+
+    @ApiModelProperty(value = "用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "预约日期")
+    @TableField("reservation_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+
+    @ApiModelProperty(value = "预约开始时间 HH:mm")
+    @TableField("start_time")
+    private String startTime;
+
+    @ApiModelProperty(value = "预约结束时间 HH:mm(可往后延续)")
+    @TableField("end_time")
+    private String endTime;
+
+    @ApiModelProperty(value = "预约人数")
+    @TableField("guest_count")
+    private Integer guestCount;
+
+    @ApiModelProperty(value = "偏好区域/分类ID(如大厅/包间)")
+    @TableField("category_id")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "预约状态 0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "实际到店时间")
+    @TableField("actual_arrival_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date actualArrivalTime;
+
+    @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;
+}

+ 61 - 0
alien-entity/src/main/java/shop/alien/entity/store/UserReservationTable.java

@@ -0,0 +1,61 @@
+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
+ */
+@Data
+@JsonInclude
+@TableName("user_reservation_table")
+@ApiModel(value = "UserReservationTable对象", description = "用户预约与桌号关联表")
+public class UserReservationTable {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "预约ID")
+    @TableField("reservation_id")
+    private Integer reservationId;
+
+    @ApiModelProperty(value = "桌号信息ID(关联store_booking_table)")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "排序")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 53 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/UserReservationDTO.java

@@ -0,0 +1,53 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户预约 DTO(新增/修改)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "UserReservationDTO", description = "用户预约DTO")
+public class UserReservationDTO {
+
+    @ApiModelProperty(value = "预约ID(修改时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "预约日期", required = true, example = "2025-03-10")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+
+    @ApiModelProperty(value = "预约开始时间 HH:mm", example = "09:00")
+    private String startTime;
+
+    @ApiModelProperty(value = "预约结束时间 HH:mm", example = "12:00")
+    private String endTime;
+
+    @ApiModelProperty(value = "预约人数", required = true)
+    private Integer guestCount;
+
+    @ApiModelProperty(value = "偏好区域/分类ID(如大厅/包间)")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "预约状态 0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时")
+    private Integer status;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "关联的桌号ID列表(store_booking_table.id)")
+    private List<Integer> tableIds;
+}

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

@@ -0,0 +1,68 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户预约 VO(详情/列表)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "UserReservationVo", description = "用户预约VO")
+public class UserReservationVo {
+
+    @ApiModelProperty(value = "预约ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "预约号(券码)")
+    private String reservationNo;
+
+    @ApiModelProperty(value = "用户ID")
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "预约日期")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+
+    @ApiModelProperty(value = "预约开始时间 HH:mm")
+    private String startTime;
+
+    @ApiModelProperty(value = "预约结束时间 HH:mm")
+    private String endTime;
+
+    @ApiModelProperty(value = "预约人数")
+    private Integer guestCount;
+
+    @ApiModelProperty(value = "偏好区域/分类ID")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "预约状态 0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时")
+    private Integer status;
+
+    @ApiModelProperty(value = "实际到店时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date actualArrivalTime;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "关联的桌号ID列表")
+    private List<Integer> tableIds;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "修改时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 12 - 0
alien-entity/src/main/java/shop/alien/mapper/UserReservationMapper.java

@@ -0,0 +1,12 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.UserReservation;
+
+/**
+ * 用户预约信息表 Mapper 接口
+ *
+ * @author system
+ */
+public interface UserReservationMapper extends BaseMapper<UserReservation> {
+}

+ 21 - 0
alien-entity/src/main/java/shop/alien/mapper/UserReservationTableMapper.java

@@ -0,0 +1,21 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.UserReservationTable;
+
+/**
+ * 用户预约与桌号关联表 Mapper 接口
+ *
+ * @author system
+ */
+public interface UserReservationTableMapper extends BaseMapper<UserReservationTable> {
+
+    /**
+     * 按预约ID物理删除关联(用于更新预约桌号时先清空再插入,避免逻辑删除导致唯一约束冲突)
+     *
+     * @param reservationId 预约ID
+     * @return 删除行数
+     */
+    int physicalDeleteByReservationId(@Param("reservationId") Integer reservationId);
+}

+ 25 - 0
alien-entity/src/main/resources/mapper/UserReservationMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.UserReservationMapper">
+
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.UserReservation">
+        <id column="id" property="id" />
+        <result column="reservation_no" property="reservationNo" />
+        <result column="user_id" property="userId" />
+        <result column="store_id" property="storeId" />
+        <result column="reservation_date" property="reservationDate" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="guest_count" property="guestCount" />
+        <result column="category_id" property="categoryId" />
+        <result column="status" property="status" />
+        <result column="actual_arrival_time" property="actualArrivalTime" />
+        <result column="remark" property="remark" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+</mapper>

+ 9 - 0
alien-entity/src/main/resources/mapper/UserReservationTableMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.UserReservationTableMapper">
+
+    <delete id="physicalDeleteByReservationId">
+        DELETE FROM user_reservation_table WHERE reservation_id = #{reservationId}
+    </delete>
+
+</mapper>

+ 139 - 0
alien-store/src/main/java/shop/alien/store/controller/UserReservationController.java

@@ -0,0 +1,139 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.store.service.UserReservationService;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户预约 前端控制器
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"用户预约"})
+@CrossOrigin
+@RestController
+@RequestMapping("/user/reservation")
+@RequiredArgsConstructor
+public class UserReservationController {
+
+    private final UserReservationService userReservationService;
+
+    @ApiOperation("新增预约")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/add")
+    public R<Integer> add(@RequestBody UserReservationDTO dto) {
+        log.info("UserReservationController.add?userId={}, storeId={}", dto.getUserId(), dto.getStoreId());
+        try {
+            Integer id = userReservationService.add(dto);
+            return R.data(id);
+        } catch (Exception e) {
+            log.error("新增预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改预约")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/update")
+    public R<String> update(@RequestBody UserReservationDTO dto) {
+        log.info("UserReservationController.update?id={}", dto.getId());
+        if (dto.getId() == null) {
+            return R.fail("预约ID不能为空");
+        }
+        try {
+            boolean ok = userReservationService.updateReservation(dto);
+            return ok ? R.success("修改成功") : R.fail("修改失败");
+        } catch (Exception e) {
+            log.error("修改预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除预约")
+    @ApiOperationSupport(order = 3)
+    @ApiImplicitParam(name = "id", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    @PostMapping("/delete")
+    public R<String> delete(@RequestParam Integer id) {
+        log.info("UserReservationController.delete?id={}", id);
+        if (id == null) {
+            return R.fail("预约ID不能为空");
+        }
+        try {
+            boolean ok = userReservationService.removeReservation(id);
+            return ok ? R.success("删除成功") : R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("删除预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("预约详情")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParam(name = "id", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    @GetMapping("/detail")
+    public R<UserReservationVo> detail(@RequestParam Integer id) {
+        log.info("UserReservationController.detail?id={}", id);
+        if (id == null) {
+            return R.fail("预约ID不能为空");
+        }
+        UserReservationVo vo = userReservationService.getDetail(id);
+        if (vo == null) {
+            return R.fail("预约不存在");
+        }
+        return R.data(vo);
+    }
+
+    @ApiOperation("分页查询预约列表")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "预约状态", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "dateFrom", value = "预约日期起 yyyy-MM-dd", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "dateTo", value = "预约日期止 yyyy-MM-dd", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数", dataType = "Integer", paramType = "query", defaultValue = "10")
+    })
+    @GetMapping("/page")
+    public R<IPage<UserReservationVo>> page(
+            @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateFrom,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateTo,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        log.info("UserReservationController.page?userId={}, storeId={}, status={}, pageNum={}, pageSize={}",
+                userId, storeId, status, pageNum, pageSize);
+        IPage<UserReservationVo> page = userReservationService.pageList(userId, storeId, status, dateFrom, dateTo, pageNum, pageSize);
+        return R.data(page);
+    }
+
+    @ApiOperation("预约列表(不分页)")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "预约状态", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/list")
+    public R<List<UserReservationVo>> list(
+            @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer status) {
+        log.info("UserReservationController.list?userId={}, storeId={}, status={}", userId, storeId, status);
+        List<UserReservationVo> list = userReservationService.list(userId, storeId, status);
+        return R.data(list);
+    }
+}

+ 76 - 0
alien-store/src/main/java/shop/alien/store/service/UserReservationService.java

@@ -0,0 +1,76 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.UserReservationVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户预约 服务类
+ *
+ * @author system
+ */
+public interface UserReservationService extends IService<UserReservation> {
+
+    /**
+     * 新增预约(含关联桌号)
+     *
+     * @param dto 预约DTO
+     * @return 预约ID
+     */
+    Integer add(UserReservationDTO dto);
+
+    /**
+     * 修改预约(含关联桌号)
+     *
+     * @param dto 预约DTO
+     * @return 是否成功
+     */
+    boolean updateReservation(UserReservationDTO dto);
+
+    /**
+     * 删除预约(逻辑删除主表,物理删除关联表)
+     *
+     * @param id 预约ID
+     * @return 是否成功
+     */
+    boolean removeReservation(Integer id);
+
+    /**
+     * 查询预约详情(含关联桌号ID列表)
+     *
+     * @param id 预约ID
+     * @return 预约VO,不存在返回 null
+     */
+    UserReservationVo getDetail(Integer id);
+
+    /**
+     * 分页查询预约列表
+     *
+     * @param userId   用户ID(可选)
+     * @param storeId  门店ID(可选)
+     * @param status   预约状态(可选)
+     * @param dateFrom 预约日期起(可选)
+     * @param dateTo   预约日期止(可选)
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页结果
+     */
+    IPage<UserReservationVo> pageList(Integer userId, Integer storeId, Integer status,
+                                      Date dateFrom, Date dateTo,
+                                      Integer pageNum, Integer pageSize);
+
+    /**
+     * 列表查询预约(不分页)
+     *
+     * @param userId  用户ID(可选)
+     * @param storeId 门店ID(可选)
+     * @param status  预约状态(可选)
+     * @return 预约VO列表
+     */
+    List<UserReservationVo> list(Integer userId, Integer storeId, Integer status);
+}

+ 187 - 0
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -0,0 +1,187 @@
+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.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationTable;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.mapper.UserReservationMapper;
+import shop.alien.mapper.UserReservationTableMapper;
+import shop.alien.store.service.UserReservationService;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+/**
+ * 用户预约 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class UserReservationServiceImpl extends ServiceImpl<UserReservationMapper, UserReservation> implements UserReservationService {
+
+    private final UserReservationTableMapper userReservationTableMapper;
+
+    private static final int STATUS_PENDING = 0;
+
+    @Override
+    public Integer add(UserReservationDTO dto) {
+        if (dto.getUserId() == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        if (dto.getStoreId() == null) {
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (dto.getReservationDate() == null) {
+            throw new RuntimeException("预约日期不能为空");
+        }
+        if (dto.getGuestCount() == null || dto.getGuestCount() < 1) {
+            throw new RuntimeException("预约人数至少为1");
+        }
+
+        UserReservation entity = new UserReservation();
+        BeanUtils.copyProperties(dto, entity, "id", "tableIds");
+        entity.setId(null);
+        entity.setReservationNo(generateReservationNo());
+        if (entity.getStatus() == null) {
+            entity.setStatus(STATUS_PENDING);
+        }
+        this.save(entity);
+
+        saveReservationTables(entity.getId(), dto.getTableIds());
+        return entity.getId();
+    }
+
+    @Override
+    public boolean updateReservation(UserReservationDTO dto) {
+        if (dto.getId() == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+        UserReservation existing = this.getById(dto.getId());
+        if (existing == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        UserReservation entity = new UserReservation();
+        BeanUtils.copyProperties(dto, entity, "tableIds", "reservationNo");
+        entity.setReservationNo(null);
+        this.updateById(entity);
+
+        saveReservationTables(existing.getId(), dto.getTableIds());
+        return true;
+    }
+
+    @Override
+    public boolean removeReservation(Integer id) {
+        UserReservation one = this.getById(id);
+        if (one == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        userReservationTableMapper.physicalDeleteByReservationId(id);
+        return this.removeById(id);
+    }
+
+    @Override
+    public UserReservationVo getDetail(Integer id) {
+        UserReservation one = this.getById(id);
+        if (one == null) {
+            return null;
+        }
+        UserReservationVo vo = new UserReservationVo();
+        BeanUtils.copyProperties(one, vo);
+        vo.setTableIds(listTableIdsByReservationId(id));
+        return vo;
+    }
+
+    @Override
+    public IPage<UserReservationVo> pageList(Integer userId, Integer storeId, Integer status,
+                                             Date dateFrom, Date dateTo,
+                                             Integer pageNum, Integer pageSize) {
+        int current = pageNum == null || pageNum <= 0 ? 1 : pageNum;
+        int size = pageSize == null || pageSize <= 0 ? 10 : pageSize;
+        Page<UserReservation> page = new Page<>(current, size);
+        LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, dateFrom, dateTo);
+        wrapper.orderByDesc(UserReservation::getCreatedTime);
+        IPage<UserReservation> entityPage = this.page(page, wrapper);
+        return entityPage.convert(this::toVoWithTableIds);
+    }
+
+    @Override
+    public List<UserReservationVo> list(Integer userId, Integer storeId, Integer status) {
+        LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, null, null);
+        wrapper.orderByDesc(UserReservation::getCreatedTime);
+        List<UserReservation> list = this.list(wrapper);
+        return list.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
+    }
+
+    private LambdaQueryWrapper<UserReservation> buildListWrapper(Integer userId, Integer storeId, Integer status,
+                                                                  Date dateFrom, Date dateTo) {
+        LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+        if (userId != null) {
+            wrapper.eq(UserReservation::getUserId, userId);
+        }
+        if (storeId != null) {
+            wrapper.eq(UserReservation::getStoreId, storeId);
+        }
+        if (status != null) {
+            wrapper.eq(UserReservation::getStatus, status);
+        }
+        if (dateFrom != null) {
+            wrapper.ge(UserReservation::getReservationDate, dateFrom);
+        }
+        if (dateTo != null) {
+            wrapper.le(UserReservation::getReservationDate, dateTo);
+        }
+        return wrapper;
+    }
+
+    private UserReservationVo toVoWithTableIds(UserReservation entity) {
+        UserReservationVo vo = new UserReservationVo();
+        BeanUtils.copyProperties(entity, vo);
+        vo.setTableIds(listTableIdsByReservationId(entity.getId()));
+        return vo;
+    }
+
+    private List<Integer> listTableIdsByReservationId(Integer reservationId) {
+        LambdaQueryWrapper<UserReservationTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservationTable::getReservationId, reservationId)
+                .orderByAsc(UserReservationTable::getSort)
+                .orderByAsc(UserReservationTable::getId);
+        List<UserReservationTable> list = userReservationTableMapper.selectList(wrapper);
+        return list.stream().map(UserReservationTable::getTableId).collect(Collectors.toList());
+    }
+
+    /**
+     * 保存预约与桌号关联:先物理删除该预约下原有关联再插入
+     */
+    private void saveReservationTables(Integer reservationId, List<Integer> tableIds) {
+        userReservationTableMapper.physicalDeleteByReservationId(reservationId);
+        if (tableIds != null && !tableIds.isEmpty()) {
+            int sort = 0;
+            for (Integer tableId : tableIds) {
+                UserReservationTable rt = new UserReservationTable();
+                rt.setReservationId(reservationId);
+                rt.setTableId(tableId);
+                rt.setSort(sort++);
+                userReservationTableMapper.insert(rt);
+            }
+        }
+    }
+
+    private static String generateReservationNo() {
+        return "RV" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
+    }
+}