浏览代码

商家端 预订信息 商家加时

qinxuyang 1 月之前
父节点
当前提交
95219e8e7c

+ 36 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreReservationMapper.java

@@ -0,0 +1,36 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商家端预约管理 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreReservationMapper extends BaseMapper<UserReservation> {
+
+    /**
+     * 查询商家端预约信息列表
+     *
+     * @param storeId    门店ID(必填)
+     * @param status     预约状态(可选,0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时)
+     * @param dateFrom   预约日期起(可选)
+     * @param dateTo     预约日期止(可选)
+     * @param orderStatus 订单状态(可选,0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订)
+     * @return 预约信息列表
+     */
+    List<StoreReservationListVo> getStoreReservationList(
+            @Param("storeId") Integer storeId,
+            @Param("status") Integer status,
+            @Param("dateFrom") Date dateFrom,
+            @Param("dateTo") Date dateTo,
+            @Param("orderStatus") Integer orderStatus
+    );
+}

+ 114 - 0
alien-entity/src/main/resources/mapper/StoreReservationMapper.xml

@@ -0,0 +1,114 @@
+<?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.StoreReservationMapper">
+
+    <!-- 商家端预约信息列表结果映射 -->
+    <resultMap id="StoreReservationListVoMap" type="shop.alien.entity.store.vo.StoreReservationListVo">
+        <id column="id" property="id" />
+        <result column="reservation_no" property="reservationNo" />
+        <result column="reservation_date" property="reservationDate" />
+        <result column="week_day" property="weekDay" />
+        <result column="category_id" property="categoryId" />
+        <result column="category_name" property="categoryName" />
+        <result column="guest_count" property="guestCount" />
+        <result column="table_numbers" property="tableNumbers" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="time_slot" property="timeSlot" />
+        <result column="user_id" property="userId" />
+        <result column="customer_name" property="customerName" />
+        <result column="contact_phone" property="contactPhone" />
+        <result column="remark" property="remark" />
+        <result column="status" property="status" />
+        <result column="status_text" property="statusText" />
+        <result column="refund_amount" property="refundAmount" />
+        <result column="deposit_amount" property="depositAmount" />
+        <result column="order_status" property="orderStatus" />
+        <result column="order_status_text" property="orderStatusText" />
+        <result column="actual_arrival_time" property="actualArrivalTime" />
+        <result column="created_time" property="createdTime" />
+    </resultMap>
+
+    <!-- 查询商家端预约信息列表 -->
+    <select id="getStoreReservationList" resultMap="StoreReservationListVoMap">
+        SELECT
+            ur.id,
+            ur.reservation_no,
+            ur.reservation_date,
+            CASE DAYOFWEEK(ur.reservation_date)
+                WHEN 1 THEN '周日'
+                WHEN 2 THEN '周一'
+                WHEN 3 THEN '周二'
+                WHEN 4 THEN '周三'
+                WHEN 5 THEN '周四'
+                WHEN 6 THEN '周五'
+                WHEN 7 THEN '周六'
+            END AS week_day,
+            ur.category_id,
+            sbc.category_name,
+            ur.guest_count,
+            GROUP_CONCAT(sbt.table_number ORDER BY urt.sort ASC SEPARATOR ',') AS table_numbers,
+            ur.start_time,
+            ur.end_time,
+            CONCAT(IFNULL(ur.start_time, ''), '-', IFNULL(ur.end_time, '')) AS time_slot,
+            ur.user_id,
+            IFNULL(lu.real_name, lu.user_name) AS customer_name,
+            lu.user_phone AS contact_phone,
+            ur.remark,
+            ur.status,
+            CASE ur.status
+                WHEN 0 THEN '待确认'
+                WHEN 1 THEN '待使用'
+                WHEN 2 THEN '已完成'
+                WHEN 3 THEN '已取消'
+                WHEN 4 THEN '未到店'
+                ELSE '未知'
+            END AS status_text,
+            uro.refund_amount,
+            uro.deposit_amount,
+            uro.order_status,
+            CASE uro.order_status
+                WHEN 0 THEN '待支付'
+                WHEN 1 THEN '待使用'
+                WHEN 2 THEN '已完成'
+                WHEN 3 THEN '已过期'
+                WHEN 4 THEN '已取消'
+                WHEN 5 THEN '已关闭'
+                WHEN 6 THEN '退款中'
+                WHEN 7 THEN '已退款'
+                WHEN 8 THEN '商家预订'
+                ELSE '未知'
+            END AS order_status_text,
+            ur.actual_arrival_time,
+            ur.created_time
+        FROM
+            user_reservation ur
+        LEFT JOIN store_booking_category sbc ON ur.category_id = sbc.id AND sbc.delete_flag = 0
+        LEFT JOIN user_reservation_table urt ON ur.id = urt.reservation_id AND urt.delete_flag = 0
+        LEFT JOIN store_booking_table sbt ON urt.table_id = sbt.id AND sbt.delete_flag = 0
+        LEFT JOIN life_user lu ON ur.user_id = lu.id AND lu.delete_flag = 0
+        LEFT JOIN user_reservation_order uro ON ur.id = uro.reservation_id AND uro.delete_flag = 0
+        WHERE
+            ur.delete_flag = 0
+            AND ur.store_id = #{storeId}
+        <if test="status != null">
+            AND ur.status = #{status}
+        </if>
+        <if test="dateFrom != null">
+            AND ur.reservation_date &gt;= #{dateFrom}
+        </if>
+        <if test="dateTo != null">
+            AND ur.reservation_date &lt;= #{dateTo}
+        </if>
+        <if test="orderStatus != null">
+            AND uro.order_status = #{orderStatus}
+        </if>
+        GROUP BY
+            ur.id
+        ORDER BY
+            ur.reservation_date DESC,
+            ur.start_time ASC,
+            ur.created_time DESC
+    </select>
+
+</mapper>

+ 58 - 6
alien-store/src/main/java/shop/alien/store/controller/StoreReservationController.java

@@ -7,7 +7,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.vo.StoreReservationListVo;
-import shop.alien.store.service.UserReservationService;
+import shop.alien.store.service.StoreReservationService;
 
 import java.util.Date;
 import java.util.List;
@@ -27,7 +27,7 @@ import java.util.List;
 @RequiredArgsConstructor
 public class StoreReservationController {
 
-    private final UserReservationService userReservationService;
+    private final StoreReservationService storeReservationService;
 
     @ApiOperationSupport(order = 1)
     @ApiOperation("查询商家端预约信息列表")
@@ -53,7 +53,7 @@ public class StoreReservationController {
         }
 
         try {
-            List<StoreReservationListVo> list = userReservationService.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
+            List<StoreReservationListVo> list = storeReservationService.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
             return R.data(list);
         } catch (Exception e) {
             log.error("查询商家端预约信息列表失败", e);
@@ -75,7 +75,7 @@ public class StoreReservationController {
         }
 
         try {
-            boolean result = userReservationService.cancelReservationByStore(reservationId);
+            boolean result = storeReservationService.cancelReservationByStore(reservationId);
             if (result) {
                 return R.success("取消预约成功");
             } else {
@@ -101,7 +101,7 @@ public class StoreReservationController {
         }
 
         try {
-            boolean result = userReservationService.deleteReservationByStore(reservationId);
+            boolean result = storeReservationService.deleteReservationByStore(reservationId);
             if (result) {
                 return R.success("删除预订信息成功");
             } else {
@@ -141,7 +141,7 @@ public class StoreReservationController {
         }
 
         try {
-            boolean result = userReservationService.addTimeByStore(reservationId, addTimeStart, addTimeMinutes);
+            boolean result = storeReservationService.addTimeByStore(reservationId, addTimeStart, addTimeMinutes);
             if (result) {
                 return R.success("加时成功");
             } else {
@@ -152,4 +152,56 @@ public class StoreReservationController {
             return R.fail("加时失败:" + e.getMessage());
         }
     }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("商家端核销预约订单")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationNo", value = "预约号(券码)", dataType = "String", paramType = "query", required = true)
+    })
+    @PostMapping("/verify")
+    public R<String> verifyReservation(@RequestParam String reservationNo) {
+        log.info("StoreReservationController.verifyReservation?reservationNo={}", reservationNo);
+
+        if (reservationNo == null || reservationNo.trim().isEmpty()) {
+            return R.fail("预约号不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.verifyReservationByNo(reservationNo);
+            if (result) {
+                return R.success("核销成功");
+            } else {
+                return R.fail("核销失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端核销预约订单失败", e);
+            return R.fail("核销失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("商家端退款(预留)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationId", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/refund")
+    public R<String> refundReservation(@RequestParam Integer reservationId) {
+        log.info("StoreReservationController.refundReservation?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            return R.fail("预约ID不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.refundReservation(reservationId);
+            if (result) {
+                return R.success("退款成功");
+            } else {
+                return R.fail("退款失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端退款失败", e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
 }

+ 74 - 0
alien-store/src/main/java/shop/alien/store/service/StoreReservationService.java

@@ -0,0 +1,74 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.StoreReservationListVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商家端预约管理 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreReservationService {
+
+    /**
+     * 查询商家端预约信息列表
+     *
+     * @param storeId    门店ID(必填)
+     * @param status     预约状态(可选,0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时)
+     * @param dateFrom   预约日期起(可选)
+     * @param dateTo     预约日期止(可选)
+     * @param orderStatus 订单状态(可选,0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订)
+     * @return 预约信息列表
+     */
+    List<StoreReservationListVo> getStoreReservationList(
+            Integer storeId, Integer status, Date dateFrom, Date dateTo, Integer orderStatus);
+
+    /**
+     * 商家端取消预约
+     *
+     * @param reservationId 预约ID
+     * @return 是否成功
+     */
+    boolean cancelReservationByStore(Integer reservationId);
+
+    /**
+     * 商家端删除预订信息
+     * 只有订单状态为已取消(4)、已退款(7)、已完成(2)时才能删除
+     *
+     * @param reservationId 预约ID
+     * @return 是否成功
+     */
+    boolean deleteReservationByStore(Integer reservationId);
+
+    /**
+     * 商家端加时
+     * 更新预订信息的结束时间,新的结束时间 = 加时开始时间 + 加时分钟数
+     *
+     * @param reservationId 预约ID
+     * @param addTimeStart 加时开始时间(HH:mm格式)
+     * @param addTimeMinutes 加时分钟数
+     * @return 是否成功
+     */
+    boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes);
+
+    /**
+     * 商家端核销预约订单
+     * 根据预约号核销,校验是否过期,更新状态,并存储到Redis
+     *
+     * @param reservationNo 预约号
+     * @return 是否成功
+     */
+    boolean verifyReservationByNo(String reservationNo);
+
+    /**
+     * 商家端退款
+     * 根据预约ID进行退款处理
+     *
+     * @param reservationId 预约ID
+     * @return 是否成功
+     */
+    boolean refundReservation(Integer reservationId);
+}

+ 592 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreReservationServiceImpl.java

@@ -0,0 +1,592 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+import shop.alien.mapper.StoreReservationMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.listener.RedisKeyExpirationHandler;
+import shop.alien.store.service.StoreReservationService;
+import shop.alien.store.service.UserReservationOrderService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import javax.annotation.PostConstruct;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商家端预约管理 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMapper, UserReservation> implements StoreReservationService {
+
+    private final UserReservationOrderService userReservationOrderService;
+    private final BaseRedisService baseRedisService;
+    private final RedisKeyExpirationHandler expirationHandler;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /** 预约状态:已取消 */
+    private static final int STATUS_CANCELLED = 3;
+    /** Redis key前缀:预订核销 */
+    private static final String RESERVATION_VERIFY_PREFIX = "reservation:verify:";
+
+    /**
+     * 初始化时注册预订核销过期处理器
+     */
+    @PostConstruct
+    public void initReservationExpirationHandler() {
+        // 注册预订核销过期处理器
+        expirationHandler.registerHandler(RESERVATION_VERIFY_PREFIX, this::handleExpiredReservationKey);
+        log.info("预订核销过期处理器注册完成,前缀: {}", RESERVATION_VERIFY_PREFIX);
+    }
+
+    @Override
+    public List<StoreReservationListVo> getStoreReservationList(Integer storeId, Integer status, Date dateFrom, Date dateTo, Integer orderStatus) {
+        log.info("StoreReservationServiceImpl.getStoreReservationList?storeId={}, status={}, dateFrom={}, dateTo={}, orderStatus={}",
+                storeId, status, dateFrom, dateTo, orderStatus);
+
+        if (storeId == null) {
+            throw new RuntimeException("门店ID不能为空");
+        }
+
+        return baseMapper.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
+    }
+
+    @Override
+    public boolean cancelReservationByStore(Integer reservationId) {
+        log.info("StoreReservationServiceImpl.cancelReservationByStore?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 检查预约状态,已取消的不能再次取消
+        if (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
+            throw new RuntimeException("预约已取消,不能重复取消");
+        }
+
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            // 如果没有订单,直接更新预约状态为3(已取消)
+            reservation.setStatus(STATUS_CANCELLED);
+            boolean updateResult = this.updateById(reservation);
+            if (!updateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+            log.info("商家端取消预约成功(无订单),reservationId={}", reservationId);
+            return true;
+        }
+
+        // 判断订单费用类型:0-免费, 1-收费
+        Integer orderCostType = order.getOrderCostType();
+        if (orderCostType == null) {
+            orderCostType = 0;
+        }
+
+        if (orderCostType == 0) {
+            // 免费订单:更新订单状态为4(已取消),更新预约状态为3(已取消)
+            order.setOrderStatus(4); // 4:已取消
+            boolean orderUpdateResult = userReservationOrderService.updateById(order);
+            if (!orderUpdateResult) {
+                throw new RuntimeException("更新订单状态失败");
+            }
+
+            reservation.setStatus(STATUS_CANCELLED);
+            boolean reservationUpdateResult = this.updateById(reservation);
+            if (!reservationUpdateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+
+            log.info("商家端取消预约成功(免费订单),reservationId={}, orderId={}", reservationId, order.getId());
+            return true;
+        } else if (orderCostType == 1) {
+            // 付费订单:功能预留(暂不更新状态,等待后续实现退款逻辑)
+            throw new RuntimeException("付费订单取消功能暂未实现,请稍后再试");
+        } else {
+            throw new RuntimeException("订单费用类型异常,orderCostType=" + orderCostType);
+        }
+    }
+
+    @Override
+    public boolean deleteReservationByStore(Integer reservationId) {
+        log.info("StoreReservationServiceImpl.deleteReservationByStore?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            // 如果没有订单,直接删除预约记录
+            boolean deleteResult = this.removeById(reservationId);
+            if (!deleteResult) {
+                throw new RuntimeException("删除预约记录失败");
+            }
+            log.info("商家端删除预订信息成功(无订单),reservationId={}", reservationId);
+            return true;
+        }
+
+        // 判断订单状态:只有已取消(4)、已退款(7)、已完成(2)状态才能删除
+        Integer orderStatus = order.getOrderStatus();
+        if (orderStatus == null) {
+            throw new RuntimeException("订单状态异常,无法删除");
+        }
+
+        // 定义可删除的订单状态:2:已完成, 4:已取消, 7:已退款
+        boolean canDelete = orderStatus == 2 || orderStatus == 4 || orderStatus == 7;
+
+        if (!canDelete) {
+            String statusText = getOrderStatusText(orderStatus);
+            throw new RuntimeException("订单状态为" + statusText + ",不允许删除。只有已取消、已退款、已完成状态的订单可以删除");
+        }
+
+        // 删除订单记录(逻辑删除)
+        boolean orderDeleteResult = userReservationOrderService.removeById(order.getId());
+        if (!orderDeleteResult) {
+            throw new RuntimeException("删除订单记录失败");
+        }
+
+        // 删除预约记录(逻辑删除)
+        boolean reservationDeleteResult = this.removeById(reservationId);
+        if (!reservationDeleteResult) {
+            throw new RuntimeException("删除预约记录失败");
+        }
+
+        log.info("商家端删除预订信息成功,reservationId={}, orderId={}, orderStatus={}",
+                reservationId, order.getId(), orderStatus);
+        return true;
+    }
+
+    @Override
+    public boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes) {
+        log.info("StoreReservationServiceImpl.addTimeByStore?reservationId={}, addTimeStart={}, addTimeMinutes={}",
+                reservationId, addTimeStart, addTimeMinutes);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        if (addTimeStart == null || addTimeStart.trim().isEmpty()) {
+            throw new RuntimeException("加时开始时间不能为空");
+        }
+
+        if (addTimeMinutes == null || addTimeMinutes <= 0) {
+            throw new RuntimeException("加时分钟数必须大于0");
+        }
+
+        // 验证时间格式 HH:mm
+        if (!addTimeStart.matches("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")) {
+            throw new RuntimeException("加时开始时间格式错误,应为HH:mm格式");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 保存原结束时间用于日志
+        String oldEndTime = reservation.getEndTime();
+
+        // 计算新的结束时间
+        // 如果加时开始时间在当前预订结束时间之内,新的结束时间 = 当前结束时间 + 加时分钟数
+        // 如果加时开始时间超过了当前预订结束时间,新的结束时间 = 加时开始时间 + 加时分钟数
+        String newEndTime;
+        if (compareTime(addTimeStart, reservation.getEndTime()) <= 0) {
+            // 加时开始时间在预订结束时间之内,从当前结束时间开始加时
+            newEndTime = calculateNewEndTime(reservation.getEndTime(), addTimeMinutes);
+        } else {
+            // 加时开始时间超过了预订结束时间,从加时开始时间开始加时
+            newEndTime = calculateNewEndTime(addTimeStart, addTimeMinutes);
+        }
+
+        // 校验:不能超过下一个已确认预约的开始时间
+        validateAddTimeNotExceedNextReservation(reservation, addTimeStart, newEndTime);
+
+        // 更新预约结束时间
+        reservation.setEndTime(newEndTime);
+        boolean updateResult = this.updateById(reservation);
+        if (!updateResult) {
+            throw new RuntimeException("更新预约结束时间失败");
+        }
+
+        log.info("商家端加时成功,reservationId={}, 原结束时间={}, 加时开始时间={}, 加时分钟数={}, 新结束时间={}",
+                reservationId, oldEndTime, addTimeStart, addTimeMinutes, newEndTime);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean verifyReservationByNo(String reservationNo) {
+        log.info("StoreReservationServiceImpl.verifyReservationByNo?reservationNo={}", reservationNo);
+
+        if (reservationNo == null || reservationNo.trim().isEmpty()) {
+            throw new RuntimeException("预约号不能为空");
+        }
+
+        // 根据预约号查询预约信息
+        LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservation::getReservationNo, reservationNo)
+                .eq(UserReservation::getDeleteFlag, 0)
+                .last("LIMIT 1");
+        UserReservation reservation = this.getOne(wrapper);
+
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在或已被删除");
+        }
+
+        // 校验预约状态(必须是已确认状态才能核销)
+        if (reservation.getStatus() == null || reservation.getStatus() != 1) {
+            throw new RuntimeException("预约状态不正确,只有已确认的预约才能核销");
+        }
+
+        // 校验是否过期:当前时间是否超过预约结束时间
+        if (isReservationExpired(reservation)) {
+            throw new RuntimeException("预约已过期,无法核销");
+        }
+
+        // 查询关联的订单
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservation.getId())
+                .eq(UserReservationOrder::getDeleteFlag, 0)
+                .last("LIMIT 1");
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            throw new RuntimeException("未找到关联的订单信息");
+        }
+
+        // 更新预约状态为已到店(status = 2)
+        reservation.setStatus(2); // 已到店
+        reservation.setActualArrivalTime(new Date());
+        boolean updateReservation = this.updateById(reservation);
+        if (!updateReservation) {
+            throw new RuntimeException("更新预约状态失败");
+        }
+
+        // 更新订单状态为已完成(order_status = 2)
+        order.setOrderStatus(2); // 已完成
+        boolean updateOrder = userReservationOrderService.updateById(order);
+        if (!updateOrder) {
+            throw new RuntimeException("更新订单状态失败");
+        }
+
+        // 计算Redis过期时间(预订结束时间 + 3小时)
+        long expireSeconds = calculateExpireSecondsWithBuffer(reservation);
+
+        // 将预订信息存储到Redis
+        try {
+            String redisKey = RESERVATION_VERIFY_PREFIX + reservationNo;
+            String reservationJson = objectMapper.writeValueAsString(reservation);
+            baseRedisService.setString(redisKey, reservationJson, expireSeconds);
+            log.info("预订信息已存储到Redis,key={}, expireSeconds={}(预订结束时间+3小时)", redisKey, expireSeconds);
+        } catch (Exception e) {
+            log.error("存储预订信息到Redis失败", e);
+            // Redis存储失败不影响核销流程,只记录日志
+        }
+
+        log.info("核销预约订单成功,reservationNo={}, reservationId={}, orderId={}",
+                reservationNo, reservation.getId(), order.getId());
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean refundReservation(Integer reservationId) {
+        log.info("StoreReservationServiceImpl.refundReservation?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 查询关联的订单
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId)
+                .eq(UserReservationOrder::getDeleteFlag, 0)
+                .last("LIMIT 1");
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            throw new RuntimeException("未找到关联的订单信息");
+        }
+
+        // 检查订单状态,只有已完成的订单才能退款
+        if (order.getOrderStatus() == null || order.getOrderStatus() != 2) {
+            log.warn("订单状态不正确,无法退款,订单ID: {}, 订单状态: {}", order.getId(), order.getOrderStatus());
+            throw new RuntimeException("订单状态不正确,只有已完成的订单才能退款");
+        }
+
+        // 检查订单费用类型,只有付费订单才需要退款
+        if (order.getOrderCostType() == null || order.getOrderCostType() == 0) {
+            log.info("免费订单无需退款,订单ID: {}, 预约ID: {}", order.getId(), reservationId);
+            return true;
+        }
+
+        // TODO: 调用支付退款接口
+        // 1. 调用第三方支付退款接口(微信支付、支付宝等)
+        // 2. 根据退款结果更新订单状态
+
+        // 更新订单状态为已退款(order_status = 7)
+        order.setOrderStatus(7); // 已退款
+        order.setRefundTime(new Date());
+        boolean updateOrder = userReservationOrderService.updateById(order);
+        if (!updateOrder) {
+            throw new RuntimeException("更新订单状态失败");
+        }
+
+        // 更新预约状态为已取消(status = 3)
+        reservation.setStatus(3); // 已取消
+        boolean updateReservation = this.updateById(reservation);
+        if (!updateReservation) {
+            throw new RuntimeException("更新预约状态失败");
+        }
+
+        log.info("预订退款处理成功,预约ID: {}, 订单ID: {}", reservationId, order.getId());
+        return true;
+    }
+
+    /**
+     * 校验加时不能超过下一个已确认预约的开始时间
+     */
+    private void validateAddTimeNotExceedNextReservation(UserReservation reservation, String addTimeStart, String newEndTime) {
+        // 确定查询基准时间:取当前结束时间和加时开始时间的较大值
+        String queryBaseTime = reservation.getEndTime();
+        if (compareTime(addTimeStart, reservation.getEndTime()) > 0) {
+            queryBaseTime = addTimeStart;
+        }
+
+        // 查询同一门店、同一日期、状态为已确认(status=1)的预约
+        LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservation::getStoreId, reservation.getStoreId())
+                .apply("DATE(user_reservation.reservation_date) = DATE({0})", reservation.getReservationDate())
+                .eq(UserReservation::getStatus, 1) // 已确认
+                .ge(UserReservation::getStartTime, queryBaseTime)
+                .ne(UserReservation::getId, reservation.getId())
+                .eq(UserReservation::getDeleteFlag, 0)
+                .orderByAsc(UserReservation::getStartTime)
+                .last("LIMIT 1");
+
+        UserReservation nextReservation = this.getOne(wrapper);
+
+        if (nextReservation != null) {
+            if (compareTime(newEndTime, nextReservation.getStartTime()) > 0) {
+                throw new RuntimeException(
+                        String.format("新的结束时间 %s 超过了下一个已确认预约的开始时间 %s(预约号:%s)",
+                                newEndTime, nextReservation.getStartTime(), nextReservation.getReservationNo()));
+            }
+        }
+    }
+
+    /**
+     * 比较两个时间字符串(HH:mm格式)
+     */
+    private int compareTime(String time1, String time2) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
+            Date date1 = sdf.parse(time1);
+            Date date2 = sdf.parse(time2);
+            return date1.compareTo(date2);
+        } catch (ParseException e) {
+            log.error("比较时间失败,time1={}, time2={}", time1, time2, e);
+            throw new RuntimeException("时间比较失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 计算新的结束时间:加时开始时间 + 加时分钟数
+     */
+    private String calculateNewEndTime(String addTimeStart, Integer addTimeMinutes) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
+            Date startDate = sdf.parse(addTimeStart);
+
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(startDate);
+            calendar.add(Calendar.MINUTE, addTimeMinutes);
+
+            return sdf.format(calendar.getTime());
+        } catch (ParseException e) {
+            log.error("计算新的结束时间失败,addTimeStart={}, addTimeMinutes={}", addTimeStart, addTimeMinutes, e);
+            throw new RuntimeException("时间计算失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 校验预约是否过期
+     */
+    private boolean isReservationExpired(UserReservation reservation) {
+        try {
+            Date now = new Date();
+            String endTime = reservation.getEndTime();
+
+            if (endTime == null || endTime.trim().isEmpty()) {
+                return true;
+            }
+
+            SimpleDateFormat[] dateTimeFormats = {
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm")
+            };
+
+            Date endDateTime = null;
+            for (SimpleDateFormat format : dateTimeFormats) {
+                try {
+                    endDateTime = format.parse(endTime);
+                    break;
+                } catch (ParseException e) {
+                    // 继续尝试下一个格式
+                }
+            }
+
+            if (endDateTime == null) {
+                log.error("无法解析结束时间格式,endTime={}", endTime);
+                return true;
+            }
+
+            return now.after(endDateTime);
+        } catch (Exception e) {
+            log.error("校验预约是否过期失败", e);
+            return true;
+        }
+    }
+
+    /**
+     * 计算Redis过期时间(秒):从当前时间到预订结束时间 + 3小时的秒数
+     */
+    private long calculateExpireSecondsWithBuffer(UserReservation reservation) {
+        try {
+            Date now = new Date();
+            String endTime = reservation.getEndTime();
+
+            if (endTime == null || endTime.trim().isEmpty()) {
+                return 0;
+            }
+
+            SimpleDateFormat[] dateTimeFormats = {
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm")
+            };
+
+            Date endDateTime = null;
+            for (SimpleDateFormat format : dateTimeFormats) {
+                try {
+                    endDateTime = format.parse(endTime);
+                    break;
+                } catch (ParseException e) {
+                    // 继续尝试下一个格式
+                }
+            }
+
+            if (endDateTime == null) {
+                log.error("无法解析结束时间格式,endTime={}", endTime);
+                return 0;
+            }
+
+            // 在结束时间基础上加3小时
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(endDateTime);
+            calendar.add(Calendar.HOUR_OF_DAY, 3);
+            Date expireDateTime = calendar.getTime();
+
+            long diff = expireDateTime.getTime() - now.getTime();
+            return diff > 0 ? diff / 1000 : 0;
+        } catch (Exception e) {
+            log.error("计算Redis过期时间失败", e);
+            return 0;
+        }
+    }
+
+    /**
+     * 处理过期的预订核销key
+     */
+    private void handleExpiredReservationKey(String expiredKey) {
+        try {
+            String reservationNo = expiredKey.replace(RESERVATION_VERIFY_PREFIX, "");
+
+            log.info("检测到预订核销key过期,预约号: {}, key: {}", reservationNo, expiredKey);
+
+            LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(UserReservation::getReservationNo, reservationNo)
+                    .eq(UserReservation::getDeleteFlag, 0)
+                    .last("LIMIT 1");
+            UserReservation reservation = this.getOne(wrapper);
+
+            if (reservation == null) {
+                log.warn("预订核销key过期,但未找到对应的预约信息,预约号: {}", reservationNo);
+                return;
+            }
+
+            log.info("开始处理预订退款,预约号: {}, 预约ID: {}", reservationNo, reservation.getId());
+            refundReservation(reservation.getId());
+
+        } catch (Exception e) {
+            log.error("处理过期预订核销key失败,key: {}", expiredKey, e);
+        }
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private String getOrderStatusText(Integer orderStatus) {
+        if (orderStatus == null) {
+            return "未知";
+        }
+        switch (orderStatus) {
+            case 0: return "待支付";
+            case 1: return "待使用";
+            case 2: return "已完成";
+            case 3: return "已过期";
+            case 4: return "已取消";
+            case 5: return "已关闭";
+            case 6: return "退款中";
+            case 7: return "已退款";
+            case 8: return "商家预订";
+            default: return "未知";
+        }
+    }
+}