Sfoglia il codice sorgente

feat(reservation): 完善预订系统功能实现

- 在支付宝和微信支付策略中集成预约订单支付超时服务
- 扩展用户预约实体类,增加预约人姓名、性别、电话字段
- 新增控制器方法支持按日期查询定桌情况、订单详情查询等功能
- 实现预约订单支付超时机制,设置15分钟自动取消未支付订单
- 重构预约可用日期查询逻辑,改用桌位维度判断约满状态
- 添加预约订单详情查询功能,整合门店、预订、定桌信息
- 完善预约服务层接口定义,补充多种查询和超时处理方法
fcw 1 mese fa
parent
commit
5f2d5351e3

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

@@ -70,6 +70,18 @@ public class UserReservation {
     @TableField("remark")
     private String remark;
 
+    @ApiModelProperty(value = "预约人姓名")
+    @TableField("reservation_user_name")
+    private String reservationUserName;
+
+    @ApiModelProperty(value = "预约人性别:0:男,1:女")
+    @TableField("reservation_user_gender")
+    private String reservationUserGender;
+
+    @ApiModelProperty(value = "预约人电话")
+    @TableField("reservation_user_phone")
+    private String reservationUserPhone;
+
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic

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

@@ -48,6 +48,15 @@ public class UserReservationDTO {
     @ApiModelProperty(value = "备注")
     private String remark;
 
+    @ApiModelProperty(value = "预约人姓名")
+    private String reservationUserName;
+
+    @ApiModelProperty(value = "预约人性别:0:男,1:女")
+    private String reservationUserGender;
+
+    @ApiModelProperty(value = "预约人电话")
+    private String reservationUserPhone;
+
     @ApiModelProperty(value = "关联的桌号ID列表(store_booking_table.id)")
     private List<Integer> tableIds;
 }

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

@@ -55,6 +55,15 @@ public class UserReservationVo {
     @ApiModelProperty(value = "备注")
     private String remark;
 
+    @ApiModelProperty(value = "预约人姓名")
+    private String reservationUserName;
+
+    @ApiModelProperty(value = "预约人性别:0:男,1:女")
+    private String reservationUserGender;
+
+    @ApiModelProperty(value = "预约人电话")
+    private String reservationUserPhone;
+
     @ApiModelProperty(value = "关联的桌号ID列表")
     private List<Integer> tableIds;
 

+ 83 - 6
alien-store/src/main/java/shop/alien/store/controller/UserReservationController.java

@@ -10,7 +10,10 @@ 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 shop.alien.store.service.ReservationOrderPaymentTimeoutService;
+import shop.alien.store.vo.ReservationOrderDetailVo;
 
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -29,6 +32,7 @@ import java.util.Map;
 public class UserReservationController {
 
     private final UserReservationService userReservationService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
 
     @ApiOperation("新增预约")
     @ApiOperationSupport(order = 1)
@@ -151,18 +155,91 @@ public class UserReservationController {
     }
 
     /**
-     * 获取首个未约满日期的预约数据:从今天起判断是否约满,约满则顺延到下一天,直到找到未约满的日期并返回该日的预约列表。
+     * 获取定桌情况。不传预定日期时:从今天起找首个存在未约满选座的日期并返回该日数据;
+     * 传入预定日期时:直接查询该日期的定桌情况(该日预约列表 + 每个选座约满标识 full)。
      */
-    @ApiOperation("获取首个未约满日期的预约数据")
+    @ApiOperation("获取首个未约满日期的预约数据 / 按预定日期查询指定日期的定桌情况")
     @ApiOperationSupport(order = 8)
-    @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "reservationDate", value = "预定日期,yyyy-MM-dd;不传则从今天起找首个未约满日期", dataType = "String", paramType = "query", required = false)
+    })
     @GetMapping("/firstAvailableDay")
-    public R<Map<String, Object>> firstAvailableDay(@RequestParam Integer storeId) {
-        log.info("UserReservationController.firstAvailableDay?storeId={}", storeId);
+    public R<Map<String, Object>> firstAvailableDay(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date reservationDate) {
+        log.info("UserReservationController.firstAvailableDay?storeId={}, reservationDate={}", storeId, reservationDate);
         if (storeId == null) {
             return R.fail("门店ID不能为空");
         }
-        Map<String, Object> data = userReservationService.findFirstAvailableDayReservations(storeId);
+        Map<String, Object> data = userReservationService.findFirstAvailableDayReservations(storeId, reservationDate);
         return R.data(data);
     }
+
+    /**
+     * 根据门店ID和多个 user_reservation_table 主键ID,查询该店铺这些桌记录对应的用户预约信息列表。
+     */
+    @ApiOperation("根据门店与预约桌记录ID列表查询预约详情列表")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "userReservationTableId", value = "user_reservation_table 表主键ID(或餐桌ID)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "reservationDate", value = "预订日期 yyyy-MM-dd,不传则不按日期过滤", dataType = "String", paramType = "query", required = false)
+    })
+    @GetMapping("/detailByStoreAndTableRecord")
+    public R<List<UserReservationVo>> detailByStoreAndTableRecord(
+            @RequestParam Integer storeId,
+            @RequestParam Integer userReservationTableId,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date reservationDate) {
+        log.info("UserReservationController.detailByStoreAndTableRecord?storeId={}, userReservationTableId={}, reservationDate={}", storeId, userReservationTableId, reservationDate);
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        if (userReservationTableId == null) {
+            return R.fail("预约桌记录ID不能为空");
+        }
+        List<UserReservationVo> list = userReservationService.listDetailByStoreIdAndReservationTableIds(storeId, userReservationTableId, reservationDate);
+        return R.data(list);
+    }
+
+    /**
+     * 为预订订单设置支付超时 Redis 缓存(15 分钟)。
+     * 订单创建后调用此接口,超时未支付将自动关闭订单;支付成功后由支付回调自动取消计时。
+     */
+    @ApiOperation("设置预订订单支付超时(15分钟)")
+    @ApiOperationSupport(order = 10)
+    @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", dataType = "Integer", paramType = "query", required = true)
+    @PostMapping("/setOrderPaymentTimeout")
+    public R<String> setOrderPaymentTimeout(@RequestParam Integer orderId) {
+        log.info("UserReservationController.setOrderPaymentTimeout?orderId={}", orderId);
+        if (orderId == null) {
+            return R.fail("订单ID不能为空");
+        }
+        try {
+            userReservationService.setReservationOrderPaymentTimeoutByOrderId(orderId);
+            return R.success("已设置支付超时,请在15分钟内完成支付");
+        } catch (Exception e) {
+            log.error("设置订单支付超时失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 根据订单ID查询详情:门店信息、预订信息、定桌信息
+     */
+    @ApiOperation("根据订单ID查询详情(门店+预订+定桌)")
+    @ApiOperationSupport(order = 11)
+    @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", dataType = "Integer", paramType = "query", required = true)
+    @GetMapping("/orderDetail")
+    public R<ReservationOrderDetailVo> orderDetail(@RequestParam Integer orderId) {
+        log.info("UserReservationController.orderDetail?orderId={}", orderId);
+        if (orderId == null) {
+            return R.fail("订单ID不能为空");
+        }
+        ReservationOrderDetailVo vo = userReservationService.getOrderDetailByOrderId(orderId);
+        if (vo == null) {
+            return R.fail("订单不存在");
+        }
+        return R.data(vo);
+    }
 }

+ 31 - 0
alien-store/src/main/java/shop/alien/store/service/ReservationOrderPaymentTimeoutService.java

@@ -0,0 +1,31 @@
+package shop.alien.store.service;
+
+/**
+ * 预订订单支付超时服务(Redis 缓存 + 过期自动关闭)
+ *
+ * @author system
+ */
+public interface ReservationOrderPaymentTimeoutService {
+
+    /**
+     * 设置预订订单支付超时监听(写入 Redis,过期后自动关闭订单)
+     *
+     * @param orderSn        订单编号 user_reservation_order.order_sn
+     * @param timeoutSeconds 超时秒数,建议 15*60
+     */
+    void setReservationOrderPaymentTimeout(String orderSn, long timeoutSeconds);
+
+    /**
+     * 取消预订订单支付超时监听(订单已支付时调用,删除 Redis key)
+     *
+     * @param orderSn 订单编号
+     */
+    void cancelReservationOrderPaymentTimeout(String orderSn);
+
+    /**
+     * 处理过期 key 对应的订单(关闭待支付订单)。由 Redis 过期监听回调,一般不需要业务直接调用。
+     *
+     * @param orderSn 订单编号
+     */
+    void handleReservationOrderPaymentTimeout(String orderSn);
+}

+ 49 - 3
alien-store/src/main/java/shop/alien/store/service/UserReservationService.java

@@ -5,6 +5,7 @@ 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 shop.alien.store.vo.ReservationOrderDetailVo;
 
 import java.util.Date;
 import java.util.List;
@@ -78,11 +79,11 @@ public interface UserReservationService extends IService<UserReservation> {
     Map<String, Object> getBookingsByStoreId(Integer storeId);
 
     /**
-     * 从今天起查找第一个未约满的日期,并返回该日的预约数据。
-     * 若当天约满则顺延到下一天再判断,直到找到未约满的日期
+     * 从今天起查找第一个存在未约满选座的日期,并返回该日的预约数据及每个选座的约满标识
+     * 不按商户设定计算,仅根据 UserReservation、UserReservationTable 数据:以日期为集合,每个选座(桌)当日若被任意有效预约占用则为已约满,否则未约满;同一标识字段 full(true=已约满,false=未约满)
      *
      * @param storeId 门店ID
-     * @return 含 date(yyyy-MM-dd)与 reservations(该日预约列表)的 Map;未找到则 reservations 为空且 date 为最后检查的日期
+     * @return Map:date、reservations、tableStatusList(每项含 tableId/tableNumber/seatingCapacity/full)
      */
     Map<String, Object> findFirstAvailableDayReservations(Integer storeId);
 
@@ -126,4 +127,49 @@ public interface UserReservationService extends IService<UserReservation> {
      * @return 是否成功
      */
     boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes);
+
+    /**
+     * 按预定日期查询指定日期的定桌情况(该日预约列表 + 每个选座的约满标识 full)。
+     * 传参 reservationDate 不为空时使用;与 findFirstAvailableDayReservations(storeId) 同结构。
+     *
+     * @param storeId         门店ID
+     * @param reservationDate 预定日期,格式 yyyy-MM-dd
+     * @return Map:date、reservations、tableStatusList
+     */
+    Map<String, Object> findFirstAvailableDayReservations(Integer storeId, Date reservationDate);
+
+    /**
+     * 根据门店ID和 user_reservation_table 主键ID,查询该店铺该桌对应的用户预约详情。
+     *
+     * @param storeId              门店ID
+     * @param userReservationTableId user_reservation_table 表主键 id
+     * @return 预约详情 VO,不存在或门店不匹配返回 null
+     */
+    UserReservationVo getDetailByStoreIdAndReservationTableId(Integer storeId, Integer userReservationTableId);
+
+    /**
+     * 根据门店ID和 user_reservation_table 主键ID(或餐桌ID),查询该店铺这些桌记录对应的用户预约详情列表;可选按预订日期过滤。
+     *
+     * @param storeId               门店ID
+     * @param userReservationTableId user_reservation_table 表主键或餐桌 id
+     * @param reservationDate       预订日期,为 null 时不按日期过滤
+     * @return 预约详情 VO 列表(仅包含存在且属于该门店的预约,去重)
+     */
+    List<UserReservationVo> listDetailByStoreIdAndReservationTableIds(Integer storeId, Integer userReservationTableId, Date reservationDate);
+
+    /**
+     * 为预订订单设置支付超时 Redis 缓存(15 分钟),超时未支付将自动关闭订单。
+     * 订单创建后调用;支付成功后由支付回调取消计时。
+     *
+     * @param orderId 预订订单ID user_reservation_order.id
+     */
+    void setReservationOrderPaymentTimeoutByOrderId(Integer orderId);
+
+    /**
+     * 根据订单ID查询详情:门店信息、预订信息、定桌信息
+     *
+     * @param orderId 预订订单ID user_reservation_order.id
+     * @return 详情 VO,订单不存在返回 null
+     */
+    ReservationOrderDetailVo getOrderDetailByOrderId(Integer orderId);
 }

+ 101 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPaymentTimeoutServiceImpl.java

@@ -0,0 +1,101 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.listener.RedisKeyExpirationHandler;
+import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
+import shop.alien.store.service.UserReservationOrderService;
+
+import javax.annotation.PostConstruct;
+import java.util.Date;
+
+/**
+ * 预订订单支付超时服务实现:Redis 缓存 15 分钟,过期自动关闭订单
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReservationOrderPaymentTimeoutServiceImpl implements ReservationOrderPaymentTimeoutService {
+
+    private static final String REDIS_KEY_PREFIX = "reservation:order:payment:timeout:";
+    /** 默认支付超时时间:15 分钟 */
+    private static final long DEFAULT_TIMEOUT_SECONDS = 15 * 60;
+
+    private final BaseRedisService redisService;
+    private final RedisKeyExpirationHandler expirationHandler;
+    private final UserReservationOrderService userReservationOrderService;
+
+    @PostConstruct
+    public void init() {
+        expirationHandler.registerHandler(REDIS_KEY_PREFIX, this::onExpiredKey);
+        log.info("预订订单支付超时处理器注册完成,前缀: {},超时: {} 分钟", REDIS_KEY_PREFIX, DEFAULT_TIMEOUT_SECONDS / 60);
+    }
+
+    private void onExpiredKey(String expiredKey) {
+        try {
+            String orderSn = expiredKey.replace(REDIS_KEY_PREFIX, "");
+            log.info("检测到预订订单支付超时,orderSn={}", orderSn);
+            handleReservationOrderPaymentTimeout(orderSn);
+        } catch (Exception e) {
+            log.error("处理预订订单支付超时 key 失败,key={}", expiredKey, e);
+        }
+    }
+
+    @Override
+    public void setReservationOrderPaymentTimeout(String orderSn, long timeoutSeconds) {
+        if (orderSn == null || orderSn.trim().isEmpty()) {
+            log.warn("订单编号为空,无法设置支付超时");
+            return;
+        }
+        long timeout = timeoutSeconds > 0 ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS;
+        String key = REDIS_KEY_PREFIX + orderSn;
+        redisService.setString(key, orderSn, timeout);
+        log.info("设置预订订单支付超时,orderSn={}, 超时秒数={}, key={}", orderSn, timeout, key);
+    }
+
+    @Override
+    public void cancelReservationOrderPaymentTimeout(String orderSn) {
+        if (orderSn == null || orderSn.trim().isEmpty()) {
+            return;
+        }
+        String key = REDIS_KEY_PREFIX + orderSn;
+        redisService.delete(key);
+        log.info("取消预订订单支付超时监听,orderSn={}, key={}", orderSn, key);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handleReservationOrderPaymentTimeout(String orderSn) {
+        if (orderSn == null || orderSn.trim().isEmpty()) {
+            return;
+        }
+        UserReservationOrder order = userReservationOrderService.getByOrderSn(orderSn);
+        if (order == null) {
+            log.warn("预订订单不存在,orderSn={}", orderSn);
+            return;
+        }
+        // 仅对待支付(0)状态关闭
+        if (order.getOrderStatus() != null && order.getOrderStatus() == 0) {
+            LambdaUpdateWrapper<UserReservationOrder> update = new LambdaUpdateWrapper<>();
+            update.eq(UserReservationOrder::getOrderSn, orderSn)
+                    .set(UserReservationOrder::getOrderStatus, 5) // 5:已关闭
+                    .set(UserReservationOrder::getUpdatedTime, new Date());
+            boolean ok = userReservationOrderService.update(update);
+            if (ok) {
+                log.info("预订订单支付超时已自动关闭,orderSn={}", orderSn);
+            } else {
+                log.error("关闭预订订单失败,orderSn={}", orderSn);
+            }
+        } else {
+            log.info("预订订单非待支付状态,无需关闭,orderSn={}, status={}", orderSn, order.getOrderStatus());
+        }
+    }
+}

+ 440 - 26
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -1,6 +1,7 @@
 package shop.alien.store.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -14,10 +15,13 @@ import shop.alien.entity.store.dto.UserReservationDTO;
 import shop.alien.entity.store.vo.StoreMainInfoVo;
 import shop.alien.entity.store.vo.StoreReservationListVo;
 import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.store.vo.BookingTableItemVo;
+import shop.alien.store.vo.ReservationOrderDetailVo;
 import shop.alien.mapper.UserReservationMapper;
 import shop.alien.mapper.UserReservationTableMapper;
 import shop.alien.store.service.*;
 
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
@@ -47,12 +51,16 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
 
     private final UserReservationOrderService userReservationOrderService;
 
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
+
     /** 预约状态:待确认 */
     private static final int STATUS_PENDING = 0;
     /** 预约状态:已取消(不参与约满统计与展示) */
     private static final int STATUS_CANCELLED = 3;
     /** 查找首个未约满日期时,最多往后检查的天数 */
     private static final int MAX_DAYS_TO_CHECK = 366;
+    /** 全天预订时使用的结束分钟数(24*60,即到次日0点) */
+    private static final int MINUTES_DAY_END = 24 * 60;
 
     @Override
     public Integer add(UserReservationDTO dto) {
@@ -79,7 +87,49 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         this.save(entity);
 
         saveReservationTables(entity.getId(), dto.getTableIds());
-        return entity.getId();
+
+        // 同步创建预订订单(user_reservation_order),待支付时写入 Redis 15 分钟超时
+        UserReservationOrder order = buildReservationOrder(entity);
+        userReservationOrderService.save(order);
+        if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
+            reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
+        }
+
+        return order.getId();
+    }
+
+    /**
+     * 根据预约及门店预订配置构建预订订单
+     */
+    private UserReservationOrder buildReservationOrder(UserReservation reservation) {
+        UserReservationOrder order = new UserReservationOrder();
+        order.setOrderSn(userReservationOrderService.generateOrderSn());
+        order.setReservationId(reservation.getId());
+        order.setUserId(reservation.getUserId());
+        order.setStoreId(reservation.getStoreId());
+        order.setOrderStatus(0);
+        order.setPaymentStatus(0);
+        order.setIsMerchantReservation(0);
+
+        StoreBookingSettings settings = storeBookingSettingsService.getByStoreId(reservation.getStoreId());
+        if (settings != null && "1".equals(settings.getReservation()) && settings.getReservationMoney() != null && settings.getReservationMoney() > 0) {
+            order.setOrderCostType(1);
+            order.setDepositAmount(BigDecimal.valueOf(settings.getReservationMoney()));
+            Calendar cal = Calendar.getInstance();
+            cal.add(Calendar.MINUTE, 15);
+            order.setPaymentDeadline(cal.getTime());
+            order.setCancellationPolicyType(1);
+            if (settings.getRetainPositionFlag() != null && settings.getRetainPositionFlag() == 1 && settings.getRetentionDuration() != null) {
+                order.setLateArrivalGraceMinutes(settings.getRetentionDuration());
+            }
+            order.setDepositRefundRule("到店就餐24小时后自动原路返回");
+        } else {
+            order.setOrderCostType(0);
+            order.setDepositAmount(BigDecimal.ZERO);
+            order.setCancellationPolicyType(0);
+        }
+
+        return order;
     }
 
     @Override
@@ -124,6 +174,58 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     }
 
     @Override
+    public UserReservationVo getDetailByStoreIdAndReservationTableId(Integer storeId, Integer userReservationTableId) {
+        if (storeId == null || userReservationTableId == null) {
+            return null;
+        }
+        UserReservationTable link = userReservationTableMapper.selectById(userReservationTableId);
+        if (link == null) {
+            return null;
+        }
+        UserReservation reservation = this.getById(link.getReservationId());
+        if (reservation == null || !storeId.equals(reservation.getStoreId())) {
+            return null;
+        }
+        return getDetail(reservation.getId());
+    }
+
+    @Override
+    public List<UserReservationVo> listDetailByStoreIdAndReservationTableIds(Integer storeId, Integer userReservationTableId, Date reservationDate) {
+        if (storeId == null || userReservationTableId == null) {
+            return Collections.emptyList();
+        }
+        LambdaQueryWrapper<UserReservationTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservationTable::getTableId, userReservationTableId);
+        List<UserReservationTable> userReservationTables = userReservationTableMapper.selectList(wrapper);
+        List<UserReservationVo> list = new ArrayList<>();
+        Calendar calReservation = reservationDate != null ? calendarOf(reservationDate) : null;
+        for (UserReservationTable userReservationTable : userReservationTables) {
+            UserReservation reservation = this.getById(userReservationTable.getReservationId());
+            if (reservation == null || !storeId.equals(reservation.getStoreId())) {
+                continue;
+            }
+            if (calReservation != null && reservation.getReservationDate() != null) {
+                Calendar cal = calendarOf(reservation.getReservationDate());
+                if (cal.get(Calendar.YEAR) != calReservation.get(Calendar.YEAR)
+                        || cal.get(Calendar.DAY_OF_YEAR) != calReservation.get(Calendar.DAY_OF_YEAR)) {
+                    continue;
+                }
+            }
+            UserReservationVo vo = getDetail(reservation.getId());
+            if (vo != null) {
+                list.add(vo);
+            }
+        }
+        return list;
+    }
+
+    private static Calendar calendarOf(Date date) {
+        Calendar c = Calendar.getInstance();
+        c.setTime(date);
+        return c;
+    }
+
+    @Override
     public IPage<UserReservationVo> pageList(Integer userId, Integer storeId, Integer status,
                                              Date dateFrom, Date dateTo,
                                              Integer pageNum, Integer pageSize) {
@@ -161,29 +263,256 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     }
 
     /**
-     * 从今天起查找第一个未约满的日期,并返回该日的预约数据。
-     * 判断逻辑:若当天已预约人数(guest_count 之和)>= 门店单时段最大容纳人数,则视为约满,顺延到下一天再判断。
+     * 按预定日期查询指定日期的定桌情况。传入 reservationDate 时仅查询该日,不进行“首个未约满日期”的顺延逻辑。
+     *
+     * @param storeId         门店ID
+     * @param reservationDate 预定日期,为 null 时行为同 findFirstAvailableDayReservations(storeId)
+     * @return Map:date、reservations、tableStatusList
+     */
+    @Override
+    public Map<String, Object> findFirstAvailableDayReservations(Integer storeId, Date reservationDate) {
+        if (reservationDate != null) {
+            return getDayBookingStatus(storeId, reservationDate);
+        }
+        return findFirstAvailableDayReservations(storeId);
+    }
+
+    /**
+     * 将 "HH:mm" 解析为当日 0 点起的分钟数,解析失败返回 -1。
+     */
+    private static int timeToMinutes(String hhmm) {
+        if (hhmm == null) {
+            return -1;
+        }
+        String[] parts = hhmm.trim().split(":");
+        if (parts.length < 2) {
+            return -1;
+        }
+        try {
+            int h = Integer.parseInt(parts[0].trim());
+            int m = Integer.parseInt(parts[1].trim());
+            if (h < 0 || h > 24 || m < 0 || m > 59) {
+                return -1;
+            }
+            return h * 60 + m;
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    /** 商户营业时间类型:正常营业时间 */
+    private static final int BUSINESS_TYPE_NORMAL = 1;
+
+    /**
+     * 从 store_booking_settings 取当日可预订时段 [开始分钟, 结束分钟]。
+     * 非全天时用 booking_start_time、booking_end_time;若二者为空则取商户运营时间(store_business_info 正常营业的 start_time/end_time);全天、未配置或仍无效时用 0 到 24*60。
+     */
+    private int[] getBookingRangeMinutes(Integer storeId) {
+        int[] range = new int[]{0, MINUTES_DAY_END};
+        List<StoreBookingSettings> list = storeBookingSettingsService.list(
+                new LambdaQueryWrapper<StoreBookingSettings>().eq(StoreBookingSettings::getStoreId, storeId));
+        if (!list.isEmpty()) {
+            StoreBookingSettings settings = list.get(0);
+            if (settings.getBookingTimeType() != null && settings.getBookingTimeType() == 1) {
+                return range;
+            }
+            int start = timeToMinutes(settings.getBookingStartTime());
+            int end = timeToMinutes(settings.getBookingEndTime());
+            if (start >= 0 && end > start) {
+                range[0] = start;
+                range[1] = end;
+                return range;
+            }
+        }
+        // 预订开始/结束时间为空或无效时,取商户运营时间(营业时间)
+        StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);
+        if (storeInfo != null && storeInfo.getStoreBusinessInfo() != null && !storeInfo.getStoreBusinessInfo().isEmpty()) {
+            List<StoreBusinessInfo> normalHours = storeInfo.getStoreBusinessInfo().stream()
+                    .filter(b -> b.getBusinessType() != null && b.getBusinessType() == BUSINESS_TYPE_NORMAL)
+                    .collect(Collectors.toList());
+            if (!normalHours.isEmpty()) {
+                int minStart = MINUTES_DAY_END;
+                int maxEnd = 0;
+                for (StoreBusinessInfo b : normalHours) {
+                    int s = timeToMinutes(b.getStartTime());
+                    int e = timeToMinutes(b.getEndTime());
+                    if (s >= 0) {
+                        minStart = Math.min(minStart, s);
+                    }
+                    if (e > 0) {
+                        maxEnd = Math.max(maxEnd, e);
+                    }
+                }
+                if (minStart < MINUTES_DAY_END && maxEnd > 0 && maxEnd > minStart) {
+                    range[0] = minStart;
+                    range[1] = maxEnd;
+                }
+            }
+        }
+        return range;
+    }
+
+    /**
+     * 合并重叠/相邻时间段,并判断是否完全覆盖 [rangeStart, rangeEnd]。
+     * 将各段限制在 range 内后合并,若覆盖总长度等于 (rangeEnd - rangeStart) 则返回 true。
+     */
+    private static boolean isFullCoverage(List<int[]> segments, int rangeStart, int rangeEnd) {
+        if (segments.isEmpty()) {
+            return rangeStart >= rangeEnd;
+        }
+        List<int[]> clipped = new ArrayList<>();
+        for (int[] seg : segments) {
+            int a = Math.max(seg[0], rangeStart);
+            int b = Math.min(seg[1], rangeEnd);
+            if (a < b) {
+                clipped.add(new int[]{a, b});
+            }
+        }
+        if (clipped.isEmpty()) {
+            return rangeStart >= rangeEnd;
+        }
+        clipped.sort(Comparator.comparingInt(s -> s[0]));
+        List<int[]> merged = new ArrayList<>();
+        merged.add(clipped.get(0).clone());
+        for (int i = 1; i < clipped.size(); i++) {
+            int[] cur = clipped.get(i);
+            int[] last = merged.get(merged.size() - 1);
+            // 允许最多 1 分钟间隙视为连续(如 09:00 结束与 09:01 开始),避免因微小间隙判为未约满
+            if (cur[0] <= last[1] + 1) {
+                last[1] = Math.max(last[1], cur[1]);
+            } else {
+                merged.add(cur.clone());
+            }
+        }
+        int total = 0;
+        for (int[] m : merged) {
+            total += (m[1] - m[0]);
+        }
+        // 允许总覆盖与目标时长差 1 分钟仍视为约满(兼容边界或舍入误差)
+        return total >= (rangeEnd - rangeStart) - 1;
+    }
+
+    /**
+     * 按“商户可预订时段 + 该桌当日所有预约时间段拼接后是否完全覆盖”计算每桌的 full。
+     * 使用 store_booking_settings 的 booking_start_time、booking_end_time 作为当日可预订起止,与 user_reservation 的 start_time、end_time 及 user_reservation_table 的 table_id 对比,只有全部约满才返回 true。
+     */
+    private List<Map<String, Object>> buildTableStatusListForDay(Integer storeId, Date dayStart, Date dayEnd,
+                                                                 List<StoreBookingTable> storeTables,
+                                                                 List<UserReservation> dayReservations,
+                                                                 List<UserReservationTable> dayTableLinks,
+                                                                 int bookingStartMin, int bookingEndMin) {
+        Map<Integer, List<UserReservation>> reservationsByTableId = new HashMap<>();
+        Map<Integer, UserReservation> resMap = dayReservations.stream().collect(Collectors.toMap(UserReservation::getId, r -> r, (a, b) -> a));
+        for (UserReservationTable rt : dayTableLinks) {
+            UserReservation r = resMap.get(rt.getReservationId());
+            if (r != null) {
+                reservationsByTableId.computeIfAbsent(rt.getTableId(), k -> new ArrayList<>()).add(r);
+            }
+        }
+
+        return storeTables.stream().map(t -> {
+            Map<String, Object> row = new HashMap<>();
+            row.put("tableId", t.getId());
+            row.put("tableNumber", t.getTableNumber());
+            row.put("seatingCapacity", t.getSeatingCapacity());
+            List<UserReservation> tableDayReservations = reservationsByTableId.getOrDefault(t.getId(), Collections.emptyList());
+            List<int[]> segments = new ArrayList<>();
+            for (UserReservation r : tableDayReservations) {
+                int s = timeToMinutes(r.getStartTime());
+                int e = timeToMinutes(r.getEndTime());
+                if (s >= 0 && e > s) {
+                    segments.add(new int[]{s, e});
+                }
+            }
+            boolean full = isFullCoverage(segments, bookingStartMin, bookingEndMin);
+            row.put("full", full);
+            return row;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 查询指定日期的定桌情况:该日预约列表 + 每个选座的约满标识(full)。
+     * 以 store_booking_settings 的 booking_start_time、booking_end_time 为当日可预订时段,结合 user_reservation(reservation_date、start_time、end_time)与 user_reservation_table(table_id)找出该日每桌所有预约,时间段拼接后与商户时段对比,仅当商户时段被完全覆盖时 full=true。
+     */
+    private Map<String, Object> getDayBookingStatus(Integer storeId, Date reservationDate) {
+        Map<String, Object> result = new HashMap<>();
+        if (storeId == null) {
+            result.put("date", null);
+            result.put("reservations", null);
+            result.put("tableStatusList", null);
+            return result;
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(reservationDate);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        Date dayStart = cal.getTime();
+        cal.add(Calendar.DAY_OF_MONTH, 1);
+        Date dayEnd = cal.getTime();
+
+        List<StoreBookingTable> storeTables = storeBookingTableService.getTableList(storeId, null);
+        int[] range = getBookingRangeMinutes(storeId);
+        int bookingStartMin = range[0];
+        int bookingEndMin = range[1];
+
+        LambdaQueryWrapper<UserReservation> dayResWrapper = new LambdaQueryWrapper<>();
+        dayResWrapper.eq(UserReservation::getStoreId, storeId)
+                .ne(UserReservation::getStatus, STATUS_CANCELLED)
+                .ge(UserReservation::getReservationDate, dayStart)
+                .lt(UserReservation::getReservationDate, dayEnd);
+        List<UserReservation> dayReservations = this.list(dayResWrapper);
+        List<Integer> dayReservationIds = dayReservations.stream().map(UserReservation::getId).collect(Collectors.toList());
+        List<UserReservationTable> dayTableLinks = new ArrayList<>();
+        if (!dayReservationIds.isEmpty()) {
+            LambdaQueryWrapper<UserReservationTable> rtWrapper = new LambdaQueryWrapper<>();
+            rtWrapper.in(UserReservationTable::getReservationId, dayReservationIds);
+            dayTableLinks = userReservationTableMapper.selectList(rtWrapper);
+        }
+
+        List<Map<String, Object>> tableStatusList = buildTableStatusListForDay(storeId, dayStart, dayEnd, storeTables, dayReservations, dayTableLinks, bookingStartMin, bookingEndMin);
+
+        LambdaQueryWrapper<UserReservation> listWrapper = new LambdaQueryWrapper<>();
+        listWrapper.eq(UserReservation::getStoreId, storeId)
+                .ge(UserReservation::getReservationDate, dayStart)
+                .lt(UserReservation::getReservationDate, dayEnd)
+                .orderByAsc(UserReservation::getReservationDate)
+                .orderByAsc(UserReservation::getStartTime);
+        List<UserReservation> reservations = this.list(listWrapper);
+        List<UserReservationVo> voList = reservations.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
+        result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(dayStart));
+        result.put("reservations", voList);
+        result.put("tableStatusList", tableStatusList);
+        return result;
+    }
+
+    /**
+     * 从今天起查找第一个“存在未约满选座”的日期,并返回该日的预约数据及每个选座(桌)的约满状态。
+     * 以 store_booking_settings 的 booking_start_time、booking_end_time 为当日可预订时段,结合 user_reservation(reservation_date、start_time、end_time)与 user_reservation_table(table_id)找出该日每桌所有预约,将用户预约时间段拼接合并后与商户时段对比:仅当商户可预订时段被完全覆盖时该桌 full=true,否则 full=false。
      *
      * @param storeId 门店ID
-     * @return Map:date 为 yyyy-MM-dd 格式的日期,reservations 为该日的预约 VO 列表;未配置或 storeId 为空时 date 可能为 null、reservations 为空列表
+     * @return Map:date 日期(yyyy-MM-dd),reservations 该日预约列表,tableStatusList 该日每个选座的约满标识(full)及桌信息
      */
     @Override
     public Map<String, Object> findFirstAvailableDayReservations(Integer storeId) {
         Map<String, Object> result = new HashMap<>();
         if (storeId == null) {
             result.put("date", null);
-//            result.put("reservations", List.of());
+            result.put("reservations", null);
+            result.put("tableStatusList", null);
             return result;
         }
-        // 取门店预订设置中的单时段最大容纳人数,未配置或为 0 则视为不设上限
-        int maxCapacity = Integer.MAX_VALUE;
-        List<StoreBookingSettings> settingsList = storeBookingSettingsService.list(
-                new LambdaQueryWrapper<StoreBookingSettings>().eq(StoreBookingSettings::getStoreId, storeId));
-        if (!settingsList.isEmpty() && settingsList.get(0).getMaxCapacityPerSlot() != null
-                && settingsList.get(0).getMaxCapacityPerSlot() > 0) {
-            maxCapacity = settingsList.get(0).getMaxCapacityPerSlot();
-        }
-        // 从今天 00:00:00 开始,按天往后检查
+        // 门店下所有选座(桌),用于按“选座”维度计算约满
+        List<StoreBookingTable> storeTables = storeBookingTableService.getTableList(storeId, null);
+//        if (storeTables == null) {
+//            storeTables = List.of();
+//        }
+        // 商户可预订时段(分钟),用于与用户预约时间段对比判断是否约满
+        int[] range = getBookingRangeMinutes(storeId);
+        int bookingStartMin = range[0];
+        int bookingEndMin = range[1];
+        // 从今天 00:00:00 开始,按天往后检查;以“日期”为一个集合
         Calendar cal = Calendar.getInstance();
         cal.set(Calendar.HOUR_OF_DAY, 0);
         cal.set(Calendar.MINUTE, 0);
@@ -193,18 +522,25 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
             Date dayStart = cal.getTime();
             cal.add(Calendar.DAY_OF_MONTH, 1);
             Date dayEnd = cal.getTime();
-            // 统计该日该门店下非取消状态的预约总人数
-            LambdaQueryWrapper<UserReservation> countWrapper = new LambdaQueryWrapper<>();
-            countWrapper.eq(UserReservation::getStoreId, storeId)
+            // 当日、该门店、非取消的预约
+            LambdaQueryWrapper<UserReservation> dayResWrapper = new LambdaQueryWrapper<>();
+            dayResWrapper.eq(UserReservation::getStoreId, storeId)
                     .ne(UserReservation::getStatus, STATUS_CANCELLED)
                     .ge(UserReservation::getReservationDate, dayStart)
                     .lt(UserReservation::getReservationDate, dayEnd);
-            List<UserReservation> dayList = this.list(countWrapper);
-            int totalGuests = dayList.stream()
-                    .mapToInt(r -> r.getGuestCount() == null ? 0 : r.getGuestCount())
-                    .sum();
-            // 未约满:总人数小于上限,返回该日及该日全部预约数据
-            if (totalGuests < maxCapacity) {
+            List<UserReservation> dayReservations = this.list(dayResWrapper);
+            List<Integer> dayReservationIds = dayReservations.stream().map(UserReservation::getId).collect(Collectors.toList());
+            List<UserReservationTable> dayTableLinks = new ArrayList<>();
+            if (!dayReservationIds.isEmpty()) {
+                LambdaQueryWrapper<UserReservationTable> rtWrapper = new LambdaQueryWrapper<>();
+                rtWrapper.in(UserReservationTable::getReservationId, dayReservationIds);
+                dayTableLinks = userReservationTableMapper.selectList(rtWrapper);
+            }
+            // 按商户 booking_start_time/booking_end_time 与每桌当日预约时间段拼接对比,仅当全部约满时 full=true
+            List<Map<String, Object>> tableStatusList = buildTableStatusListForDay(storeId, dayStart, dayEnd, storeTables, dayReservations, dayTableLinks, bookingStartMin, bookingEndMin);
+            // 若存在至少一个选座未约满,则返回该日数据
+            boolean hasAvailable = tableStatusList.stream().anyMatch(m -> !Boolean.TRUE.equals(m.get("full")));
+            if (hasAvailable) {
                 LambdaQueryWrapper<UserReservation> listWrapper = new LambdaQueryWrapper<>();
                 listWrapper.eq(UserReservation::getStoreId, storeId)
                         .ge(UserReservation::getReservationDate, dayStart)
@@ -215,13 +551,24 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
                 List<UserReservationVo> voList = reservations.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
                 result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(dayStart));
                 result.put("reservations", voList);
+                result.put("tableStatusList", tableStatusList);
                 return result;
             }
         }
-        // 连续 MAX_DAYS_TO_CHECK 天都约满时,返回最后检查的日期,预约列表为空
+        // 连续 MAX_DAYS_TO_CHECK 天所有选座均已约满时,返回最后一天的日期及该日选座状态(全部 full=true),预约列表为空
         cal.add(Calendar.DAY_OF_MONTH, -1);
-        result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
-//        result.put("reservations", List.of());
+        Date lastDayStart = cal.getTime();
+        result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(lastDayStart));
+        result.put("reservations", null);
+        List<Map<String, Object>> allFullList = storeTables.stream().map(t -> {
+            Map<String, Object> row = new HashMap<>();
+            row.put("tableId", t.getId());
+            row.put("tableNumber", t.getTableNumber());
+            row.put("seatingCapacity", t.getSeatingCapacity());
+            row.put("full", true);
+            return row;
+        }).collect(Collectors.toList());
+        result.put("tableStatusList", allFullList);
         return result;
     }
 
@@ -510,6 +857,73 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         }
     }
 
+    @Override
+    public void setReservationOrderPaymentTimeoutByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return;
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null || order.getOrderSn() == null) {
+            log.warn("预订订单不存在或无订单编号,orderId={}", orderId);
+            return;
+        }
+        reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
+    }
+
+    @Override
+    public ReservationOrderDetailVo getOrderDetailByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return null;
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return null;
+        }
+        ReservationOrderDetailVo vo = new ReservationOrderDetailVo();
+        vo.setOrder(order);
+
+        if (order.getStoreId() != null) {
+            vo.setStoreInfo(storeInfoService.getById(order.getStoreId()));
+            vo.setStoreBookingSettings(storeBookingSettingsService.getByStoreId(order.getStoreId()));
+        }
+        if (order.getReservationId() != null) {
+            vo.setReservation(getDetail(order.getReservationId()));
+            vo.setTableList(buildBookingTableList(order.getReservationId()));
+        } else {
+            vo.setTableList(Collections.emptyList());
+        }
+        return vo;
+    }
+
+    private List<BookingTableItemVo> buildBookingTableList(Integer reservationId) {
+        if (reservationId == null) {
+            return Collections.emptyList();
+        }
+        List<Integer> tableIds = listTableIdsByReservationId(reservationId);
+        if (tableIds == null || tableIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<BookingTableItemVo> list = new ArrayList<>();
+        for (Integer tableId : tableIds) {
+            StoreBookingTable table = storeBookingTableService.getById(tableId);
+            if (table == null) {
+                continue;
+            }
+            BookingTableItemVo item = new BookingTableItemVo();
+            item.setTableId(table.getId());
+            item.setTableNumber(table.getTableNumber());
+            item.setSeatingCapacity(table.getSeatingCapacity());
+            if (table.getCategoryId() != null) {
+                StoreBookingCategory cat = storeBookingCategoryService.getById(table.getCategoryId());
+                if (cat != null) {
+                    item.setCategoryName(cat.getCategoryName());
+                }
+            }
+            list.add(item);
+        }
+        return list;
+    }
+
     private static String generateReservationNo() {
         return "RV" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
     }

+ 3 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java

@@ -27,6 +27,7 @@ import shop.alien.entity.store.UserReservationOrder;
 import shop.alien.store.service.MerchantPaymentOrderService;
 import shop.alien.store.service.RefundRecordService;
 import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
 import shop.alien.store.service.UserReservationOrderService;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
 import shop.alien.util.common.UniqueRandomNumGenerator;
@@ -66,6 +67,7 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
     private final UserReservationOrderService userReservationOrderService;
     private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordService refundRecordService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
     private final StringRedisTemplate stringRedisTemplate;
 
     @Override
@@ -223,6 +225,7 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
                 }
                 order.setUpdatedTime(now);
                 userReservationOrderService.updateById(order);
+                reservationOrderPaymentTimeoutService.cancelReservationOrderPaymentTimeout(order.getOrderSn());
                 return R.success("支付成功");
             }
             if ("TRADE_CLOSED".equals(tradeStatus)) {

+ 3 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

@@ -16,6 +16,7 @@ import shop.alien.entity.store.UserReservationOrder;
 import shop.alien.store.service.MerchantPaymentOrderService;
 import shop.alien.store.service.RefundRecordService;
 import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
 import shop.alien.store.service.UserReservationOrderService;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
 import shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl;
@@ -64,6 +65,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     private final UserReservationOrderService userReservationOrderService;
     private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordService refundRecordService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
     private final StringRedisTemplate stringRedisTemplate;
 
     @Override
@@ -235,6 +237,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
                 }
                 order.setUpdatedTime(now);
                 userReservationOrderService.updateById(order);
+                reservationOrderPaymentTimeoutService.cancelReservationOrderPaymentTimeout(order.getOrderSn());
                 return R.success("支付成功");
             }
             if ("CLOSED".equals(response.tradeState)) {

+ 27 - 0
alien-store/src/main/java/shop/alien/store/vo/BookingTableItemVo.java

@@ -0,0 +1,27 @@
+package shop.alien.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 定桌项(桌号+分类名)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "BookingTableItemVo", description = "定桌项")
+public class BookingTableItemVo {
+
+    @ApiModelProperty(value = "桌位ID")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "座位数")
+    private Integer seatingCapacity;
+
+    @ApiModelProperty(value = "区域/分类名称 如 大厅、包间")
+    private String categoryName;
+}

+ 36 - 0
alien-store/src/main/java/shop/alien/store/vo/ReservationOrderDetailVo.java

@@ -0,0 +1,36 @@
+package shop.alien.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.vo.UserReservationVo;
+
+import java.util.List;
+
+/**
+ * 按订单ID查询的详情:订单 + 门店信息 + 预订设置 + 预订信息 + 定桌信息
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "ReservationOrderDetailVo", description = "预订订单详情(订单+门店+预订设置+预订+定桌)")
+public class ReservationOrderDetailVo {
+
+    @ApiModelProperty(value = "预订订单(user_reservation_order)")
+    private UserReservationOrder order;
+
+    @ApiModelProperty(value = "门店信息")
+    private StoreInfo storeInfo;
+
+    @ApiModelProperty(value = "门店预订设置(store_booking_settings)")
+    private StoreBookingSettings storeBookingSettings;
+
+    @ApiModelProperty(value = "预订/预约信息")
+    private UserReservationVo reservation;
+
+    @ApiModelProperty(value = "定桌信息(桌号、区域等)")
+    private List<BookingTableItemVo> tableList;
+}