Przeglądaj źródła

订单列表修改

zhangchen 1 miesiąc temu
rodzic
commit
f78b13f9ef
18 zmienionych plików z 714 dodań i 8 usunięć
  1. 17 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderCountsDto.java
  2. 39 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderListDto.java
  3. 25 0
      alien-entity/src/main/java/shop/alien/mapper/UserReservationOrderMapper.java
  4. 60 0
      alien-entity/src/main/resources/mapper/UserReservationOrderMapper.xml
  5. 3 1
      alien-store/src/main/java/shop/alien/store/controller/MerchantPaymentController.java
  6. 47 1
      alien-store/src/main/java/shop/alien/store/controller/ReservationOrderPageController.java
  7. 3 2
      alien-store/src/main/java/shop/alien/store/controller/UserReservationPaymentController.java
  8. 21 0
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentQueryService.java
  9. 23 0
      alien-store/src/main/java/shop/alien/store/service/ReservationOrderListService.java
  10. 8 0
      alien-store/src/main/java/shop/alien/store/service/UserReservationService.java
  11. 91 0
      alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentQueryServiceImpl.java
  12. 218 0
      alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderListServiceImpl.java
  13. 12 3
      alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPageServiceImpl.java
  14. 33 0
      alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java
  15. 1 0
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java
  16. 4 1
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java
  17. 31 0
      alien-store/src/main/java/shop/alien/store/vo/ReservationOrderListResultVo.java
  18. 78 0
      alien-store/src/main/java/shop/alien/store/vo/ReservationOrderListVo.java

+ 17 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderCountsDto.java

@@ -0,0 +1,17 @@
+package shop.alien.entity.store.vo;
+
+import lombok.Data;
+
+/**
+ * 预订订单各状态数量(全部/待使用/已完成/已退款)
+ *
+ * @author system
+ */
+@Data
+public class ReservationOrderCountsDto {
+
+    private long countAll;
+    private long countToUse;
+    private long countCompleted;
+    private long countRefunded;
+}

+ 39 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderListDto.java

@@ -0,0 +1,39 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 预订订单列表查询结果 DTO(Mapper 层用,含门店名与入口图)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "ReservationOrderListDto", description = "预订订单列表查询行")
+public class ReservationOrderListDto {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+    @ApiModelProperty(value = "订单编号")
+    private String orderSn;
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+    @ApiModelProperty(value = "门店名称")
+    private String storeName;
+    @ApiModelProperty(value = "门店入口图URL")
+    private String storeEntranceImageUrl;
+    @ApiModelProperty(value = "订单状态")
+    private Integer orderStatus;
+    @ApiModelProperty(value = "预约ID")
+    private Integer reservationId;
+    @ApiModelProperty(value = "订金金额")
+    private BigDecimal depositAmount;
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}

+ 25 - 0
alien-entity/src/main/java/shop/alien/mapper/UserReservationOrderMapper.java

@@ -1,10 +1,13 @@
 package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.ibatis.annotations.Delete;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.vo.ReservationOrderCountsDto;
+import shop.alien.entity.store.vo.ReservationOrderListDto;
 
 /**
  * 用户预订订单表 Mapper 接口
@@ -22,4 +25,26 @@ public interface UserReservationOrderMapper extends BaseMapper<UserReservationOr
      */
     @Delete("DELETE FROM user_reservation_order WHERE reservation_id = #{reservationId}")
     int physicalDeleteByReservationId(@Param("reservationId") Integer reservationId);
+
+    /**
+     * 预订订单列表分页:按用户、店铺名称模糊(限10字)、订单状态筛选
+     *
+     * @param page       分页对象
+     * @param userId     用户ID
+     * @param storeName  店铺名称(可选,前端限10字,模糊)
+     * @param orderStatus 订单状态(可选)
+     * @return 分页结果
+     */
+    IPage<ReservationOrderListDto> selectOrderListPage(IPage<ReservationOrderListDto> page,
+                                                        @Param("userId") Integer userId,
+                                                        @Param("storeName") String storeName,
+                                                        @Param("orderStatus") Integer orderStatus);
+
+    /**
+     * 按用户统计:全部/待使用(1)/已完成(2)/已退款(7) 数量
+     *
+     * @param userId 用户ID
+     * @return 各状态数量
+     */
+    ReservationOrderCountsDto selectOrderCountsByUserId(@Param("userId") Integer userId);
 }

+ 60 - 0
alien-entity/src/main/resources/mapper/UserReservationOrderMapper.xml

@@ -0,0 +1,60 @@
+<?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.UserReservationOrderMapper">
+
+    <resultMap id="ReservationOrderListDtoMap" type="shop.alien.entity.store.vo.ReservationOrderListDto">
+        <id column="orderId" property="orderId"/>
+        <result column="orderSn" property="orderSn"/>
+        <result column="storeId" property="storeId"/>
+        <result column="storeName" property="storeName"/>
+        <result column="storeEntranceImageUrl" property="storeEntranceImageUrl"/>
+        <result column="orderStatus" property="orderStatus"/>
+        <result column="reservationId" property="reservationId"/>
+        <result column="depositAmount" property="depositAmount"/>
+        <result column="createdTime" property="createdTime"/>
+    </resultMap>
+
+    <!-- 预订订单列表:店铺名称模糊搜索(限10字)、按状态筛选 -->
+    <select id="selectOrderListPage" resultMap="ReservationOrderListDtoMap">
+        SELECT
+            o.id AS orderId,
+            o.order_sn AS orderSn,
+            o.store_id AS storeId,
+            s.store_name AS storeName,
+            img.img_url AS storeEntranceImageUrl,
+            o.order_status AS orderStatus,
+            o.reservation_id AS reservationId,
+            o.deposit_amount AS depositAmount,
+            o.created_time AS createdTime
+        FROM user_reservation_order o
+        LEFT JOIN store_info s ON o.store_id = s.id AND s.delete_flag = 0
+        LEFT JOIN store_img img ON img.store_id = s.id AND img.img_type = 1 AND img.delete_flag = 0
+        WHERE o.delete_flag = 0
+          AND o.user_id = #{userId}
+        <if test="storeName != null and storeName != ''">
+          AND s.store_name LIKE CONCAT('%', #{storeName}, '%')
+        </if>
+        <if test="orderStatus != null">
+          AND o.order_status = #{orderStatus}
+        </if>
+        ORDER BY o.created_time DESC
+    </select>
+
+    <resultMap id="ReservationOrderCountsDtoMap" type="shop.alien.entity.store.vo.ReservationOrderCountsDto">
+        <result column="countAll" property="countAll"/>
+        <result column="countToUse" property="countToUse"/>
+        <result column="countCompleted" property="countCompleted"/>
+        <result column="countRefunded" property="countRefunded"/>
+    </resultMap>
+
+    <select id="selectOrderCountsByUserId" resultMap="ReservationOrderCountsDtoMap">
+        SELECT
+            COUNT(*) AS countAll,
+            COALESCE(SUM(CASE WHEN o.order_status = 1 THEN 1 ELSE 0 END), 0) AS countToUse,
+            COALESCE(SUM(CASE WHEN o.order_status = 2 THEN 1 ELSE 0 END), 0) AS countCompleted,
+            COALESCE(SUM(CASE WHEN o.order_status = 7 THEN 1 ELSE 0 END), 0) AS countRefunded
+        FROM user_reservation_order o
+        WHERE o.delete_flag = 0
+          AND o.user_id = #{userId}
+    </select>
+</mapper>

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

@@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
+import shop.alien.store.service.MerchantPaymentQueryService;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
 
 import java.util.Map;
@@ -26,6 +27,7 @@ import java.util.Map;
 public class MerchantPaymentController {
 
     private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+    private final MerchantPaymentQueryService merchantPaymentQueryService;
 
     @ApiOperation("预订订单-创建预支付(支付宝,使用门店支付配置)")
     @ApiImplicitParams({
@@ -60,7 +62,7 @@ public class MerchantPaymentController {
             @RequestParam String outTradeNo,
             @RequestParam(defaultValue = "alipay") String payType) {
         log.info("MerchantPaymentController.queryStatus storeId={}, outTradeNo={}", storeId, outTradeNo);
-        return merchantPaymentStrategyFactory.getStrategy(payType).queryPayStatus(storeId, outTradeNo);
+        return merchantPaymentQueryService.queryPayStatusWithRetry(storeId, outTradeNo, payType);
     }
 
     @ApiOperation("预订订单-退款")

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

@@ -2,16 +2,20 @@ package shop.alien.store.controller;
 
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
+import shop.alien.store.service.ReservationOrderListService;
 import shop.alien.store.service.ReservationOrderPageService;
+import shop.alien.store.service.UserReservationService;
+import shop.alien.store.vo.ReservationOrderListResultVo;
 import shop.alien.store.vo.ReservationOrderPageVo;
 
 /**
- * 预订订单页面展示接口(按订单ID查询)
+ * 预订订单页面展示接口(按订单ID查询)、预订订单列表(店铺名称模糊搜索)
  *
  * @author system
  */
@@ -23,6 +27,8 @@ import shop.alien.store.vo.ReservationOrderPageVo;
 public class ReservationOrderPageController {
 
     private final ReservationOrderPageService reservationOrderPageService;
+    private final ReservationOrderListService reservationOrderListService;
+    private final UserReservationService userReservationService;
 
     @ApiOperation("预订页面展示(按订单ID)")
     @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", required = true, paramType = "query", dataType = "int")
@@ -35,4 +41,44 @@ public class ReservationOrderPageController {
         }
         return R.data(vo);
     }
+
+    @ApiOperation("预订订单列表(店铺名称模糊搜索,限10字;展示店铺名/入口图/状态/预订信息;返回全部/待使用/已完成/已退款数量)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "storeName", value = "店铺名称,模糊搜索,最多10字", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "orderStatus", value = "订单状态 0待支付 1待使用 2已完成 3已过期 4已取消 5已关闭 6退款中 7已退款", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "current", value = "页码", paramType = "query", dataType = "long"),
+            @ApiImplicitParam(name = "size", value = "每页条数", paramType = "query", dataType = "long")
+    })
+    @GetMapping("/list")
+    public R<ReservationOrderListResultVo> list(
+            @RequestParam Integer userId,
+            @RequestParam(required = false) String storeName,
+            @RequestParam(required = false) Integer orderStatus,
+            @RequestParam(defaultValue = "1") long current,
+            @RequestParam(defaultValue = "10") long size) {
+        log.info("ReservationOrderPageController.list userId={}, storeName={}, orderStatus={}, current={}, size={}", userId, storeName, orderStatus, current, size);
+        if (userId == null) {
+            return R.fail("用户ID不能为空");
+        }
+        ReservationOrderListResultVo result = reservationOrderListService.listPage(userId, storeName, orderStatus, current, size);
+        return R.data(result);
+    }
+
+    @ApiOperation("预定订单删除(逻辑删除订单及预定信息)")
+    @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", required = true, paramType = "query", dataType = "int")
+    @PostMapping("/delete")
+    public R<String> delete(@RequestParam Integer orderId) {
+        log.info("ReservationOrderPageController.delete orderId={}", orderId);
+        if (orderId == null) {
+            return R.fail("订单ID不能为空");
+        }
+        try {
+            boolean ok = userReservationService.deleteOrderAndReservationByOrderId(orderId);
+            return ok ? R.success("删除成功") : R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("预定订单删除失败 orderId={}", orderId, e);
+            return R.fail(e.getMessage());
+        }
+    }
 }

+ 3 - 2
alien-store/src/main/java/shop/alien/store/controller/UserReservationPaymentController.java

@@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
+import shop.alien.store.service.MerchantPaymentQueryService;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
 
@@ -26,6 +27,7 @@ import java.util.Map;
 public class UserReservationPaymentController {
 
     private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+    private final MerchantPaymentQueryService merchantPaymentQueryService;
 
     @ApiOperation("预订订单-创建预支付")
     @ApiImplicitParams({
@@ -61,8 +63,7 @@ public class UserReservationPaymentController {
             @RequestParam String outTradeNo,
             @RequestParam(defaultValue = "alipay") String payType) {
         log.info("UserReservationPaymentController.queryStatus storeId={}, outTradeNo={}", storeId, outTradeNo);
-        MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(payType);
-        return strategy.queryPayStatus(storeId, outTradeNo);
+        return merchantPaymentQueryService.queryPayStatusWithRetry(storeId, outTradeNo, payType);
     }
 
     @ApiOperation("预订订单-退款")

+ 21 - 0
alien-store/src/main/java/shop/alien/store/service/MerchantPaymentQueryService.java

@@ -0,0 +1,21 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+
+/**
+ * 商户支付状态查询(带重试,用于缓解第三方查单慢/暂时失败)
+ *
+ * @author system
+ */
+public interface MerchantPaymentQueryService {
+
+    /**
+     * 查询支付状态,对「查询失败/查询异常」类结果自动重试,减少前端轮询到失败的概率。
+     *
+     * @param storeId    门店ID
+     * @param outTradeNo 商户订单号
+     * @param payType    支付类型 alipay / wechatPay
+     * @return 与 strategy.queryPayStatus 一致;重试用尽仍失败时返回「查询失败:网络异常,请稍后重试」
+     */
+    R<Object> queryPayStatusWithRetry(Integer storeId, String outTradeNo, String payType);
+}

+ 23 - 0
alien-store/src/main/java/shop/alien/store/service/ReservationOrderListService.java

@@ -0,0 +1,23 @@
+package shop.alien.store.service;
+
+import shop.alien.store.vo.ReservationOrderListResultVo;
+
+/**
+ * 预订订单列表(店铺名称模糊搜索、展示店铺名/入口图/状态/预订信息,返回全部/待使用/已完成/已退款数量)
+ *
+ * @author system
+ */
+public interface ReservationOrderListService {
+
+    /**
+     * 分页查询当前用户的预订订单列表,并返回全部/待使用/已完成/已退款数量
+     *
+     * @param userId     用户ID(必填)
+     * @param storeName  店铺名称,模糊搜索,最多10字(可选)
+     * @param orderStatus 订单状态筛选(可选,0~7)
+     * @param current    页码,从1开始
+     * @param size       每页条数
+     * @return 分页列表 + countAll、countToUse、countCompleted、countRefunded
+     */
+    ReservationOrderListResultVo listPage(Integer userId, String storeName, Integer orderStatus, long current, long size);
+}

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

@@ -43,6 +43,14 @@ public interface UserReservationService extends IService<UserReservation> {
     boolean removeReservation(Integer id);
 
     /**
+     * 预定订单删除:按订单ID逻辑删除订单及预定信息(user_reservation_order、user_reservation_table、user_reservation 的 delete_flag 置为 1)
+     *
+     * @param orderId 预订订单ID(user_reservation_order.id)
+     * @return 是否成功
+     */
+    boolean deleteOrderAndReservationByOrderId(Integer orderId);
+
+    /**
      * 查询预约详情(含关联桌号ID列表)
      *
      * @param id 预约ID

+ 91 - 0
alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentQueryServiceImpl.java

@@ -0,0 +1,91 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.MerchantPaymentQueryService;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+
+/**
+ * 商户支付状态查询:对「查询失败/查询异常」自动重试,缓解第三方查单慢或暂时不可用。
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MerchantPaymentQueryServiceImpl implements MerchantPaymentQueryService {
+
+    private static final int MAX_ATTEMPTS = 3;
+    private static final long RETRY_DELAY_MS = 1500L;
+
+    /** 不可重试的失败原因(业务明确结果,重试无意义) */
+    private static final String[] NON_RETRYABLE_MSGS = {
+            "交易已关闭",
+            "等待用户付款",
+            "等待买家付款",
+            "交易不存在",
+            "预订订单不存在",
+            "支付单不存在",
+            "该门店未配置支付参数",
+            "门店ID和商户订单号不能为空",
+            "支付成功"
+    };
+
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+
+    @Override
+    public R<Object> queryPayStatusWithRetry(Integer storeId, String outTradeNo, String payType) {
+        MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(payType);
+        R<Object> last = null;
+        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
+            last = strategy.queryPayStatus(storeId, outTradeNo);
+            if (last == null) {
+                if (attempt < MAX_ATTEMPTS) {
+                    sleep(RETRY_DELAY_MS);
+                }
+                continue;
+            }
+            if (R.isSuccess(last)) {
+                return last;
+            }
+            String msg = last.getMsg();
+            if (msg != null && isNonRetryable(msg)) {
+                return last;
+            }
+            if (attempt < MAX_ATTEMPTS) {
+                log.warn("商户支付查单可重试失败 attempt={}/{} storeId={} outTradeNo={} msg={}",
+                        attempt, MAX_ATTEMPTS, storeId, outTradeNo, msg);
+                sleep(RETRY_DELAY_MS);
+            }
+        }
+        if (last != null && !R.isSuccess(last)) {
+            String msg = last.getMsg();
+            if (msg != null && (msg.contains("查询失败") || msg.contains("查询异常"))) {
+                return R.fail("查询失败:网络异常,请稍后重试或到订单列表查看");
+            }
+        }
+        return last != null ? last : R.fail("查询失败:请稍后重试");
+    }
+
+    private static boolean isNonRetryable(String msg) {
+        for (String s : NON_RETRYABLE_MSGS) {
+            if (StringUtils.contains(msg, s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void sleep(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("queryPayStatusWithRetry sleep interrupted");
+        }
+    }
+}

+ 218 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderListServiceImpl.java

@@ -0,0 +1,218 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationTable;
+import shop.alien.entity.store.vo.ReservationOrderCountsDto;
+import shop.alien.entity.store.vo.ReservationOrderListDto;
+import shop.alien.mapper.UserReservationOrderMapper;
+import shop.alien.mapper.UserReservationTableMapper;
+import shop.alien.store.service.ReservationOrderListService;
+import shop.alien.store.service.StoreBookingCategoryService;
+import shop.alien.store.service.StoreBookingTableService;
+import shop.alien.store.service.UserReservationService;
+import shop.alien.store.vo.ReservationOrderListResultVo;
+import shop.alien.store.vo.ReservationOrderListVo;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 预订订单列表:店铺名称模糊搜索(限10字)、展示店铺名/入口图/状态/预订信息
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReservationOrderListServiceImpl implements ReservationOrderListService {
+
+    private static final int STORE_NAME_SEARCH_MAX_LEN = 10;
+    private static final int ORDER_STATUS_UNPAID = 0;
+    private static final int ORDER_STATUS_TO_USE = 1;
+    private static final int ORDER_STATUS_COMPLETED = 2;
+    private static final int ORDER_STATUS_EXPIRED = 3;
+    private static final int ORDER_STATUS_CANCELLED = 4;
+    private static final int ORDER_STATUS_CLOSED = 5;
+    private static final int ORDER_STATUS_REFUNDING = 6;
+    private static final int ORDER_STATUS_REFUNDED = 7;
+
+    private final UserReservationOrderMapper userReservationOrderMapper;
+    private final UserReservationService userReservationService;
+    private final UserReservationTableMapper userReservationTableMapper;
+    private final StoreBookingTableService storeBookingTableService;
+    private final StoreBookingCategoryService storeBookingCategoryService;
+
+    @Override
+    public ReservationOrderListResultVo listPage(Integer userId, String storeName, Integer orderStatus, long current, long size) {
+        ReservationOrderListResultVo result = new ReservationOrderListResultVo();
+        if (userId == null) {
+            result.setList(new Page<>(current, size));
+            result.setCountAll(0);
+            result.setCountToUse(0);
+            result.setCountCompleted(0);
+            result.setCountRefunded(0);
+            return result;
+        }
+        ReservationOrderCountsDto counts = userReservationOrderMapper.selectOrderCountsByUserId(userId);
+        result.setCountAll(counts.getCountAll());
+        result.setCountToUse(counts.getCountToUse());
+        result.setCountCompleted(counts.getCountCompleted());
+        result.setCountRefunded(counts.getCountRefunded());
+
+        String searchStoreName = null;
+        if (StringUtils.isNotBlank(storeName)) {
+            searchStoreName = storeName.trim();
+            if (searchStoreName.length() > STORE_NAME_SEARCH_MAX_LEN) {
+                searchStoreName = searchStoreName.substring(0, STORE_NAME_SEARCH_MAX_LEN);
+            }
+        }
+        Page<ReservationOrderListDto> page = new Page<>(current, size);
+        IPage<ReservationOrderListDto> dtoPage = userReservationOrderMapper.selectOrderListPage(page, userId, searchStoreName, orderStatus);
+        List<ReservationOrderListDto> records = dtoPage.getRecords();
+        if (records == null || records.isEmpty()) {
+            IPage<ReservationOrderListVo> voPage = new Page<>(dtoPage.getCurrent(), dtoPage.getSize(), dtoPage.getTotal());
+            voPage.setRecords(Collections.emptyList());
+            result.setList(voPage);
+            return result;
+        }
+        Set<Integer> reservationIds = records.stream()
+                .map(ReservationOrderListDto::getReservationId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Integer, UserReservation> reservationMap = new HashMap<>();
+        if (!reservationIds.isEmpty()) {
+            for (UserReservation r : userReservationService.listByIds(reservationIds)) {
+                reservationMap.put(r.getId(), r);
+            }
+        }
+        Map<Integer, String> categoryNameMap = new HashMap<>();
+        Map<Integer, String> reservationTableTextMap = new HashMap<>();
+        for (Integer rid : reservationIds) {
+            UserReservation r = reservationMap.get(rid);
+            if (r == null) continue;
+            List<UserReservationTable> utList = userReservationTableMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<UserReservationTable>()
+                            .eq(UserReservationTable::getReservationId, rid)
+                            .orderByAsc(UserReservationTable::getSort)
+                            .orderByAsc(UserReservationTable::getId));
+            List<StoreBookingTable> tables = new ArrayList<>();
+            String categoryName = null;
+            for (UserReservationTable ut : utList) {
+                StoreBookingTable t = storeBookingTableService.getById(ut.getTableId());
+                if (t != null) {
+                    tables.add(t);
+                    if (categoryName == null && t.getCategoryId() != null) {
+                        StoreBookingCategory cat = storeBookingCategoryService.getById(t.getCategoryId());
+                        if (cat != null) categoryName = cat.getCategoryName();
+                    }
+                }
+            }
+            String tableNumbersText = tables.stream().map(StoreBookingTable::getTableNumber).filter(Objects::nonNull).collect(Collectors.joining(","));
+            reservationTableTextMap.put(rid, tableNumbersText.isEmpty() ? null : tableNumbersText);
+            categoryNameMap.put(rid, categoryName != null ? categoryName : "大厅");
+        }
+
+        List<ReservationOrderListVo> voList = new ArrayList<>();
+        for (ReservationOrderListDto dto : records) {
+            ReservationOrderListVo vo = new ReservationOrderListVo();
+            vo.setOrderId(dto.getOrderId());
+            vo.setOrderSn(dto.getOrderSn());
+            vo.setStoreId(dto.getStoreId());
+            vo.setStoreName(dto.getStoreName());
+            vo.setStoreEntranceImageUrl(dto.getStoreEntranceImageUrl());
+            vo.setOrderStatus(dto.getOrderStatus());
+            UserReservation resForStatus = dto.getReservationId() != null ? reservationMap.get(dto.getReservationId()) : null;
+            vo.setStatusText(buildStatusText(dto.getOrderStatus(), resForStatus));
+            vo.setDepositAmount(dto.getDepositAmount());
+            vo.setCreatedTime(dto.getCreatedTime());
+            vo.setCanContinuePay(false);
+            vo.setCanCancelReservation(false);
+            vo.setCanModifyReservation(false);
+            vo.setCanViewVoucher(false);
+            vo.setCanDelete(false);
+            vo.setCanBookAgain(false);
+
+            Integer status = dto.getOrderStatus();
+            if (status != null) {
+                vo.setCanContinuePay(ORDER_STATUS_UNPAID == status);
+                vo.setCanCancelReservation(ORDER_STATUS_UNPAID == status || ORDER_STATUS_TO_USE == status);
+                vo.setCanModifyReservation(ORDER_STATUS_TO_USE == status);
+                vo.setCanViewVoucher(ORDER_STATUS_TO_USE == status);
+                boolean endState = status == ORDER_STATUS_COMPLETED || status == ORDER_STATUS_EXPIRED
+                        || status == ORDER_STATUS_CANCELLED || status == ORDER_STATUS_CLOSED
+                        || status == ORDER_STATUS_REFUNDING || status == ORDER_STATUS_REFUNDED;
+                vo.setCanDelete(endState);
+                vo.setCanBookAgain(endState);
+            }
+
+            UserReservation res = resForStatus;
+            if (res != null) {
+                vo.setReservationDateText(formatReservationDateWithWeekday(res.getReservationDate()));
+                String catName = categoryNameMap.get(res.getId());
+                int guestCount = res.getGuestCount() != null ? res.getGuestCount() : 0;
+                vo.setGuestAndCategoryText((catName != null ? catName : "大厅") + " " + guestCount + "人");
+                vo.setTableNumbersText(reservationTableTextMap.get(res.getId()));
+                vo.setDiningTimeSlotText(buildDiningTimeSlot(res.getStartTime(), res.getEndTime()));
+            }
+
+            voList.add(vo);
+        }
+
+        IPage<ReservationOrderListVo> voPage = new Page<>(dtoPage.getCurrent(), dtoPage.getSize(), dtoPage.getTotal());
+        voPage.setRecords(voList);
+        result.setList(voPage);
+        return result;
+    }
+
+    /**
+     * 状态文案。orderStatus=4 时根据 user_reservation.reason 区分:reason 不为空为「商家取消」,否则「用户取消」
+     */
+    private static String buildStatusText(Integer orderStatus, UserReservation reservation) {
+        if (orderStatus == null) return "";
+        if (orderStatus == 4) {
+            if (reservation!=null&&StringUtils.isNotBlank(reservation.getReason())){
+                return "商家取消";
+            } else {
+                return "用户取消";
+            }
+        }
+        switch (orderStatus) {
+            case 0: return "待支付";
+            case 1: return "待使用";
+            case 2: return "已完成";
+            case 3: return "已过期";
+            case 5: return "已关闭";
+            case 6: return "退款中";
+            case 7: return "已退款";
+            default: return "";
+        }
+    }
+
+    private static String formatReservationDateWithWeekday(Date date) {
+        if (date == null) return null;
+        SimpleDateFormat f = new SimpleDateFormat("MM月dd日", Locale.CHINA);
+        String s = f.format(date);
+        Calendar c = Calendar.getInstance(Locale.CHINA);
+        c.setTime(date);
+        String[] weekDays = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
+        int w = c.get(Calendar.DAY_OF_WEEK) - 1;
+        if (w < 0) w = 0;
+        s += "(" + weekDays[w] + ")";
+        return s;
+    }
+
+    private static String buildDiningTimeSlot(String startTime, String endTime) {
+        if (startTime == null && endTime == null) return null;
+        if (startTime != null && endTime != null) return startTime + "-" + endTime;
+        return startTime != null ? startTime : endTime;
+    }
+}

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

@@ -79,9 +79,9 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         vo.setPaymentSecondsLeft(secondsLeft);
         vo.setCanContinuePay(Boolean.valueOf(ORDER_STATUS_UNPAID == order.getOrderStatus() && secondsLeft > 0));
 
-        // 页面标题与后缀(全状态)
+        // 页面标题与后缀(全状态);已取消时在加载 reservation 后根据 reason 区分为用户取消/商家取消
         Integer orderStatus = order.getOrderStatus();
-        vo.setPageTitle(buildPageTitle(orderStatus));
+        vo.setPageTitle(buildPageTitle(orderStatus, null));
         vo.setPageTitleSuffix(buildPageTitleSuffixByStatus(orderStatus, order.getCancellationPolicyType(), order.getOrderCostType()));
         vo.setStatusSubtitle(buildStatusSubtitle(order));
 
@@ -143,6 +143,9 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
                 vo.setTableNumbersText(tableNumbersText.isEmpty() ? null : tableNumbersText);
                 vo.setGuestAndCategoryText(buildGuestAndCategoryText(categoryName, reservation.getGuestCount(), tables));
                 vo.setLocationTableText(buildLocationTableText(categoryName, tableNumbersText));
+                if (orderStatus != null && orderStatus == 4) {
+                    vo.setPageTitle(buildPageTitle(4, reservation));
+                }
             }
         }
 
@@ -197,8 +200,14 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         return diff > 0 ? (int) diff : 0;
     }
 
-    private String buildPageTitle(Integer orderStatus) {
+    /**
+     * 页面主标题。orderStatus=4 时若传入 reservation,根据 user_reservation.reason 区分:reason 不为空为「商家取消」,否则「用户取消」
+     */
+    private String buildPageTitle(Integer orderStatus, UserReservation reservation) {
         if (orderStatus == null) return null;
+        if (orderStatus == 4 && reservation != null) {
+            return org.apache.commons.lang3.StringUtils.isNotBlank(reservation.getReason()) ? "商家取消" : "用户取消";
+        }
         switch (orderStatus) {
             case 0: return "待支付";
             case 1: return "预订成功";

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

@@ -241,6 +241,39 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     }
 
     @Override
+    public boolean deleteOrderAndReservationByOrderId(Integer orderId) {
+        if (orderId == null) {
+            throw new RuntimeException("预订订单ID不能为空");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            throw new RuntimeException("预订订单不存在");
+        }
+        Integer reservationId = order.getReservationId();
+        // 1. 逻辑删除订单
+        boolean orderDeleted = userReservationOrderService.removeById(orderId);
+        if (!orderDeleted) {
+            throw new RuntimeException("删除订单失败");
+        }
+        if (reservationId == null) {
+            log.info("预定订单删除成功(无关联预约),orderId={}", orderId);
+            return true;
+        }
+        // 2. 逻辑删除预约与桌号关联
+        UserReservationTable tableFlag = new UserReservationTable();
+        tableFlag.setDeleteFlag(1);
+        userReservationTableMapper.update(tableFlag, new LambdaUpdateWrapper<UserReservationTable>()
+                .eq(UserReservationTable::getReservationId, reservationId));
+        // 3. 逻辑删除预约
+        boolean reservationDeleted = this.removeById(reservationId);
+        if (!reservationDeleted) {
+            throw new RuntimeException("删除预约信息失败");
+        }
+        log.info("预定订单删除成功(逻辑删除订单及预定信息),orderId={}, reservationId={}", orderId, reservationId);
+        return true;
+    }
+
+    @Override
     public UserReservationVo getDetail(Integer id) {
         UserReservation one = this.getById(id);
         if (one == null) {

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

@@ -400,6 +400,7 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
         if (config.getAlipayRootCert() != null && config.getAlipayRootCert().length > 0) {
             alipayConfig.setRootCertContent(new String(config.getAlipayRootCert(), StandardCharsets.UTF_8));
         }
+        // 若使用的支付宝 SDK 支持,可设置查单超时以减轻「查询太慢」:alipayConfig.setConnectTimeout(10000); alipayConfig.setReadTimeout(15000);
 
         return alipayConfig;
     }

+ 4 - 1
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

@@ -464,7 +464,10 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
         reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, GETMETHOD, uri, null));
         reqBuilder.method(GETMETHOD, null);
-        OkHttpClient client = new OkHttpClient.Builder().build();
+        OkHttpClient client = new OkHttpClient.Builder()
+                .connectTimeout(10, TimeUnit.SECONDS)
+                .readTimeout(15, TimeUnit.SECONDS)
+                .build();
         try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
             String respBody = WXPayUtility.extractBody(httpResponse);
             if (httpResponse.code() >= 200 && httpResponse.code() < 300) {

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

@@ -0,0 +1,31 @@
+package shop.alien.store.vo;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 预订订单列表接口返回:分页列表 + 全部/待使用/已完成/已退款数量
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "ReservationOrderListResultVo", description = "预订订单列表返回(分页+各状态数量)")
+public class ReservationOrderListResultVo {
+
+    @ApiModelProperty(value = "分页列表")
+    private IPage<ReservationOrderListVo> list;
+
+    @ApiModelProperty(value = "全部数量")
+    private long countAll;
+
+    @ApiModelProperty(value = "待使用数量")
+    private long countToUse;
+
+    @ApiModelProperty(value = "已完成数量")
+    private long countCompleted;
+
+    @ApiModelProperty(value = "已退款数量")
+    private long countRefunded;
+}

+ 78 - 0
alien-store/src/main/java/shop/alien/store/vo/ReservationOrderListVo.java

@@ -0,0 +1,78 @@
+package shop.alien.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 预订订单列表项(店铺名称、入口图、状态、预订信息,点击店铺名进入店铺详情)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "ReservationOrderListVo", description = "预订订单列表项")
+public class ReservationOrderListVo {
+
+    @ApiModelProperty(value = "订单ID,点击进入订单详情")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单编号")
+    private String orderSn;
+
+    @ApiModelProperty(value = "门店ID,点击店铺名进入店铺详情页")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "店铺入口图URL")
+    private String storeEntranceImageUrl;
+
+    @ApiModelProperty(value = "订单状态 0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "状态文案:待支付/待使用/已完成/已过期/已取消/已关闭/退款中/已退款")
+    private String statusText;
+
+    @ApiModelProperty(value = "预订日期展示 如 01月01日(周三)")
+    private String reservationDateText;
+
+    @ApiModelProperty(value = "区域与人数 如 大厅 4人")
+    private String guestAndCategoryText;
+
+    @ApiModelProperty(value = "桌号 如 A01,A02")
+    private String tableNumbersText;
+
+    @ApiModelProperty(value = "就餐时间段 如 10:00-12:05")
+    private String diningTimeSlotText;
+
+    @ApiModelProperty(value = "预订金额(元),免费时为 null 或 0")
+    private BigDecimal depositAmount;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    // --------- 操作按钮(与详情页一致,便于列表展示操作) ---------
+    @ApiModelProperty(value = "是否显示「继续支付」")
+    private Boolean canContinuePay;
+
+    @ApiModelProperty(value = "是否显示「取消预订」")
+    private Boolean canCancelReservation;
+
+    @ApiModelProperty(value = "是否显示「修改预订」")
+    private Boolean canModifyReservation;
+
+    @ApiModelProperty(value = "是否显示「查看券码」等(待使用时为 true)")
+    private Boolean canViewVoucher;
+
+    @ApiModelProperty(value = "是否显示「删除」")
+    private Boolean canDelete;
+
+    @ApiModelProperty(value = "是否显示「再次预订」")
+    private Boolean canBookAgain;
+}