zhangchen 1 mēnesi atpakaļ
vecāks
revīzija
e70ca7d48a

+ 87 - 0
alien-store/src/main/java/shop/alien/store/controller/UserReservationPaymentController.java

@@ -0,0 +1,87 @@
+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.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+
+import java.util.Map;
+
+/**
+ * 用户端-预订订单支付接口(调用 MerchantPaymentStrategy 预支付/查单/退款)
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"用户预约-支付"})
+@RestController
+@RequestMapping("/user/reservation/payment")
+@RequiredArgsConstructor
+public class UserReservationPaymentController {
+
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+
+    @ApiOperation("预订订单-创建预支付")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "orderId", value = "预订订单ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "amountYuan", value = "支付金额(元)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "subject", value = "订单描述/商品标题", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "userId", value = "用户ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay/wechatPay", paramType = "query", dataType = "String")
+    })
+    @PostMapping("/prePay")
+    public R<Map<String, Object>> prePay(
+            @RequestParam Integer storeId,
+            @RequestParam Integer orderId,
+            @RequestParam String amountYuan,
+            @RequestParam String subject,
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("UserReservationPaymentController.prePay storeId={}, orderId={}, payType={}", storeId, orderId, payType);
+        MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(payType);
+        return strategy.createPrePay(storeId, orderId, amountYuan, subject, userId);
+    }
+
+    @ApiOperation("预订订单-查询支付状态")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "outTradeNo", value = "商户订单号(预支付返回)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay/wechatPay", paramType = "query", dataType = "String")
+    })
+    @GetMapping("/queryStatus")
+    public R<Object> queryStatus(
+            @RequestParam Integer storeId,
+            @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);
+    }
+
+    @ApiOperation("预订订单-退款")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "outTradeNo", value = "商户订单号", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundAmount", value = "退款金额(元)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundReason", value = "退款原因", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay/wechatPay", paramType = "query", dataType = "String")
+    })
+    @PostMapping("/refund")
+    public R<String> refund(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam String refundAmount,
+            @RequestParam(required = false) String refundReason,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("UserReservationPaymentController.refund storeId={}, outTradeNo={}", storeId, outTradeNo);
+        MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(payType);
+        return strategy.refund(storeId, outTradeNo, refundAmount, refundReason);
+    }
+}

+ 18 - 0
alien-store/src/main/java/shop/alien/store/service/MerchantPaymentOrderService.java

@@ -3,6 +3,8 @@ package shop.alien.store.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.MerchantPaymentOrder;
 
+import java.util.List;
+
 /**
  * 商户支付单表 服务类
  *
@@ -19,6 +21,22 @@ public interface MerchantPaymentOrderService extends IService<MerchantPaymentOrd
     MerchantPaymentOrder getByOutTradeNo(String outTradeNo);
 
     /**
+     * 根据业务订单ID查询待支付支付单(pay_status=0),用于预订页展示 outTradeNo 供前端轮询
+     *
+     * @param orderId user_reservation_order.id
+     * @return 待支付单,不存在或已支付返回 null
+     */
+    MerchantPaymentOrder getUnpaidByOrderId(Integer orderId);
+
+    /**
+     * 查询近期创建的待支付单(用于无异步回调时的后端轮询同步)
+     *
+     * @param withinMinutes 在最近多少分钟内创建的
+     * @return 待支付且未删除的支付单列表,按创建时间升序
+     */
+    List<MerchantPaymentOrder> listUnpaidRecent(int withinMinutes);
+
+    /**
      * 生成支付单号,格式:PAY + yyyyMMddHHmmss + 4位随机
      *
      * @return 支付单号

+ 63 - 0
alien-store/src/main/java/shop/alien/store/service/MerchantPaymentSyncScheduler.java

@@ -0,0 +1,63 @@
+package shop.alien.store.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商户支付状态定时同步(无异步回调时的兜底)
+ * 定时扫描近期待支付单,主动向支付宝/微信查单并更新订单状态。
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "payment.sync.enabled", havingValue = "true", matchIfMissing = false)
+public class MerchantPaymentSyncScheduler {
+
+    /** 扫描最近多少分钟内创建的待支付单 */
+    private static final int WITHIN_MINUTES = 30;
+
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+
+    /**
+     * 每 2 分钟执行一次:拉取近期待支付单,逐笔查第三方并更新状态
+     */
+    @Scheduled(fixedDelayString = "${payment.sync.interval-ms:120000}")
+    public void syncPaymentStatus() {
+        //暂时不使用定时
+         log.info("商户支付同步:本批时间={}", new Date().getTime());
+
+//        List<MerchantPaymentOrder> list = merchantPaymentOrderService.listUnpaidRecent(WITHIN_MINUTES);
+//        if (list == null || list.isEmpty()) {
+//            return;
+//        }
+//        log.debug("商户支付同步:本批待支付单数={}", list.size());
+//        for (MerchantPaymentOrder po : list) {
+//            try {
+//                if (po.getPayType() == null || !merchantPaymentStrategyFactory.supports(po.getPayType())) {
+//                    log.warn("商户支付同步:不支持的 payType={}, outTradeNo={}", po.getPayType(), po.getOutTradeNo());
+//                    continue;
+//                }
+//                MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(po.getPayType());
+//                R<Object> r = strategy.queryPayStatus(po.getStoreId(), po.getOutTradeNo());
+//                if (r != null && R.isSuccess(r)) {
+//                    log.info("商户支付同步:已更新为已支付,outTradeNo={}", po.getOutTradeNo());
+//                }
+//            } catch (Exception e) {
+//                log.warn("商户支付同步:查单异常 outTradeNo={}", po.getOutTradeNo(), e);
+//            }
+//        }
+    }
+}

+ 32 - 0
alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentOrderServiceImpl.java

@@ -12,7 +12,10 @@ import shop.alien.mapper.MerchantPaymentOrderMapper;
 import shop.alien.store.service.MerchantPaymentOrderService;
 
 import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 
 /**
@@ -53,4 +56,33 @@ public class MerchantPaymentOrderServiceImpl extends ServiceImpl<MerchantPayment
         wrapper.set(MerchantPaymentOrder::getDeleteFlag, 1);
         return baseMapper.update(null, wrapper);
     }
+
+    @Override
+    public MerchantPaymentOrder getUnpaidByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<MerchantPaymentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOrderId, orderId);
+        wrapper.eq(MerchantPaymentOrder::getPayStatus, 0);
+        wrapper.eq(MerchantPaymentOrder::getOrderType, "reservation_order");
+        wrapper.last("LIMIT 1");
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public List<MerchantPaymentOrder> listUnpaidRecent(int withinMinutes) {
+        if (withinMinutes <= 0) {
+            return Collections.emptyList();
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.MINUTE, -withinMinutes);
+        Date since = cal.getTime();
+        LambdaQueryWrapper<MerchantPaymentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getPayStatus, 0);
+        wrapper.eq(MerchantPaymentOrder::getOrderType, "reservation_order");
+        wrapper.ge(MerchantPaymentOrder::getCreatedTime, since);
+        wrapper.orderByAsc(MerchantPaymentOrder::getCreatedTime);
+        return this.list(wrapper);
+    }
 }

+ 84 - 16
alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPageServiceImpl.java

@@ -4,7 +4,13 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.stereotype.Service;
-import shop.alien.entity.store.*;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.UserReservationTable;
 import shop.alien.mapper.UserReservationTableMapper;
 import shop.alien.store.service.*;
 import shop.alien.store.vo.ReservationOrderPageVo;
@@ -31,12 +37,20 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
     private static final int CANCELLATION_NOT_FREE = 1;
     private static final int CANCELLATION_CONDITIONAL = 2;
 
+    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 UserReservationOrderService userReservationOrderService;
     private final StoreInfoService storeInfoService;
     private final UserReservationService userReservationService;
     private final UserReservationTableMapper userReservationTableMapper;
     private final StoreBookingTableService storeBookingTableService;
     private final StoreBookingCategoryService storeBookingCategoryService;
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
 
     @Override
     public ReservationOrderPageVo getPageByOrderId(Integer orderId) {
@@ -55,7 +69,7 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         vo.setOrderId(order.getId());
         vo.setOrderSn(order.getOrderSn());
         vo.setOrderStatus(order.getOrderStatus());
-        vo.setPaymentStatus(derivePaymentStatus(order.getOrderStatus()));
+        vo.setPaymentStatus(order.getPaymentStatus() != null ? order.getPaymentStatus() : derivePaymentStatus(order.getOrderStatus()));
         vo.setPayAmount(order.getDepositAmount() != null ? order.getDepositAmount() : BigDecimal.ZERO);
         vo.setPaymentDeadlineMinutes(PAYMENT_DEADLINE_MINUTES);
 
@@ -65,22 +79,28 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         vo.setPaymentSecondsLeft(secondsLeft);
         vo.setCanContinuePay(Boolean.valueOf(ORDER_STATUS_UNPAID == order.getOrderStatus() && secondsLeft > 0));
 
-        // 页面标题:待支付 / 预订成功(待使用兼容预订成功页
+        // 页面标题与后缀(全状态
         Integer orderStatus = order.getOrderStatus();
-        if (ORDER_STATUS_UNPAID == orderStatus) {
-            vo.setPageTitle("待支付");
-            vo.setPageTitleSuffix(buildPendingPaymentTitleSuffix(order.getCancellationPolicyType()));
-        } else if (ORDER_STATUS_TO_USE == orderStatus) {
-            vo.setPageTitle("预订成功");
-            vo.setPageTitleSuffix(buildSuccessPageTitleSuffix(order.getCancellationPolicyType()));
-        } else {
-            vo.setPageTitle(null);
-            vo.setPageTitleSuffix(buildPendingPaymentTitleSuffix(order.getCancellationPolicyType()));
-        }
+        vo.setPageTitle(buildPageTitle(orderStatus));
+        vo.setPageTitleSuffix(buildPageTitleSuffixByStatus(orderStatus, order.getCancellationPolicyType(), order.getOrderCostType()));
+        vo.setStatusSubtitle(buildStatusSubtitle(order));
 
-        // 操作按钮:取消预订、修改预订
+        // 操作按钮
         vo.setCanCancelReservation(Boolean.valueOf(ORDER_STATUS_UNPAID == orderStatus || ORDER_STATUS_TO_USE == orderStatus));
         vo.setCanModifyReservation(Boolean.valueOf(ORDER_STATUS_TO_USE == orderStatus));
+        boolean endState = orderStatus != null && (orderStatus == ORDER_STATUS_COMPLETED || orderStatus == ORDER_STATUS_EXPIRED
+                || orderStatus == ORDER_STATUS_CANCELLED || orderStatus == ORDER_STATUS_CLOSED
+                || orderStatus == ORDER_STATUS_REFUNDING || orderStatus == ORDER_STATUS_REFUNDED);
+        vo.setCanDelete(endState);
+        vo.setCanBookAgain(endState);
+
+        // 待支付时返回当前支付单的 outTradeNo(用于轮询 queryStatus)
+        if (orderStatus != null && ORDER_STATUS_UNPAID == orderStatus) {
+            MerchantPaymentOrder unpaid = merchantPaymentOrderService.getUnpaidByOrderId(order.getId());
+            if (unpaid != null) {
+                vo.setOutTradeNo(unpaid.getOutTradeNo());
+            }
+        }
 
         // 门店
         if (storeId != null) {
@@ -177,7 +197,56 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         return diff > 0 ? (int) diff : 0;
     }
 
-    /** 待支付页标题后缀 */
+    private String buildPageTitle(Integer orderStatus) {
+        if (orderStatus == null) return null;
+        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 "已退款";
+            default: return null;
+        }
+    }
+
+    private String buildPageTitleSuffixByStatus(Integer orderStatus, Integer cancellationPolicyType, Integer orderCostType) {
+        if (orderStatus == null) return "不可免责取消";
+        if (orderStatus == 0) return buildPendingPaymentTitleSuffix(cancellationPolicyType);
+        if (orderStatus == 1) return buildSuccessPageTitleSuffix(cancellationPolicyType);
+        if (orderStatus == 2 || orderStatus == 3 || orderStatus == 4 || orderStatus == 5 || orderStatus == 6 || orderStatus == 7) {
+            if (orderCostType != null && orderCostType == 0) return "免费预订";
+            if (cancellationPolicyType == null) return "不可免责取消";
+            if (cancellationPolicyType == CANCELLATION_FREE) return "免费预订";
+            if (cancellationPolicyType == CANCELLATION_CONDITIONAL) return "分情况是否免责";
+            return "不可免责取消";
+        }
+        return buildPendingPaymentTitleSuffix(cancellationPolicyType);
+    }
+
+    private String buildStatusSubtitle(UserReservationOrder order) {
+        if (order == null) return null;
+        Integer status = order.getOrderStatus();
+        if (ORDER_STATUS_COMPLETED == status && order.getPaymentStatus() != null && order.getPaymentStatus() == 2) {
+            return "退款成功,已按原支付路径返回";
+        }
+        if (ORDER_STATUS_REFUNDING == status) {
+            return "正在为您发起退款,请耐心等待";
+        }
+        if (ORDER_STATUS_CANCELLED == status) {
+            Integer refundType = order.getRefundType();
+            if (refundType != null && refundType == 0) {
+                if (order.getOrderCostType() != null && order.getOrderCostType() == 0) return "用户取消-免费预订";
+                if (order.getCancellationPolicyType() != null && order.getCancellationPolicyType() == CANCELLATION_NOT_FREE) return "用户取消-不可免费";
+                if (order.getCancellationPolicyType() != null && order.getCancellationPolicyType() == CANCELLATION_CONDITIONAL) return "用户取消-分情况是否免责";
+            }
+            if (order.getPaymentStatus() != null && order.getPaymentStatus() == 2) return "取消成功,已将订单费用原路退还";
+        }
+        return null;
+    }
+
     private String buildPendingPaymentTitleSuffix(Integer cancellationPolicyType) {
         if (cancellationPolicyType == null) return "不可免责取消";
         if (cancellationPolicyType == CANCELLATION_FREE) return "免费预订";
@@ -185,7 +254,6 @@ public class ReservationOrderPageServiceImpl implements ReservationOrderPageServ
         return "不可免责取消";
     }
 
-    /** 预订成功页标题后缀(待使用状态) */
     private String buildSuccessPageTitleSuffix(Integer cancellationPolicyType) {
         if (cancellationPolicyType == null) return "不可免责";
         if (cancellationPolicyType == CANCELLATION_FREE) return "免费预订";

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

@@ -8,7 +8,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.BeanUtils;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.store.*;
@@ -19,6 +21,7 @@ import shop.alien.entity.store.vo.UserReservationVo;
 import shop.alien.mapper.UserReservationOrderMapper;
 import shop.alien.store.vo.BookingTableItemVo;
 import shop.alien.store.vo.ReservationOrderDetailVo;
+import shop.alien.store.vo.ReservationOrderPageVo;
 import shop.alien.mapper.UserReservationMapper;
 import shop.alien.mapper.UserReservationTableMapper;
 import shop.alien.store.service.*;
@@ -57,6 +60,13 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
 
     private final UserReservationOrderMapper userReservationOrderMapper;
 
+    private ReservationOrderPageService reservationOrderPageService;
+
+    @Autowired
+    public void setReservationOrderPageService(@Lazy ReservationOrderPageService reservationOrderPageService) {
+        this.reservationOrderPageService = reservationOrderPageService;
+    }
+
     /** 预约状态:待确认 */
     private static final int STATUS_PENDING = 0;
     /** 预约状态:已取消(不参与约满统计与展示) */
@@ -937,9 +947,66 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         } else {
             vo.setTableList(Collections.emptyList());
         }
+
+        // 将 /store/reservationOrder/page 的页面展示逻辑写入详情:复用 page 接口数据
+        ReservationOrderPageVo pageVo = reservationOrderPageService.getPageByOrderId(orderId);
+        if (pageVo != null) {
+            copyPageVoToDetailVo(pageVo, vo);
+        }
+        vo.setRefundType(order.getRefundType());
         return vo;
     }
 
+    /** 将页面 VO 字段复制到详情 VO,供前端订单详情页与 page 接口一致展示 */
+    private void copyPageVoToDetailVo(ReservationOrderPageVo page, ReservationOrderDetailVo detail) {
+        detail.setOrderId(page.getOrderId());
+        detail.setOrderSn(page.getOrderSn());
+        detail.setOrderStatus(page.getOrderStatus());
+        detail.setPaymentStatus(page.getPaymentStatus());
+        detail.setPageTitle(page.getPageTitle());
+        detail.setPageTitleSuffix(page.getPageTitleSuffix());
+        detail.setStatusSubtitle(page.getStatusSubtitle());
+        detail.setPaymentDeadline(page.getPaymentDeadline());
+        detail.setPaymentSecondsLeft(page.getPaymentSecondsLeft());
+        detail.setPaymentDeadlineMinutes(page.getPaymentDeadlineMinutes());
+        detail.setStoreId(page.getStoreId());
+        detail.setStoreName(page.getStoreName());
+        detail.setStoreAddress(page.getStoreAddress());
+        detail.setStoreTel(page.getStoreTel());
+        detail.setStorePosition(page.getStorePosition());
+        detail.setReservationDate(page.getReservationDate());
+        detail.setReservationDateText(page.getReservationDateText());
+        detail.setGuestAndCategoryText(page.getGuestAndCategoryText());
+        detail.setTableNumbersText(page.getTableNumbersText());
+        detail.setDiningTimeSlotText(page.getDiningTimeSlotText());
+        detail.setVerificationCode(page.getVerificationCode());
+        detail.setVerificationUrl(page.getVerificationUrl());
+        detail.setLateArrivalGraceMinutes(page.getLateArrivalGraceMinutes());
+        detail.setSeatRetentionText(page.getSeatRetentionText());
+        detail.setDepositAmount(page.getDepositAmount());
+        detail.setDepositText(page.getDepositText());
+        detail.setDepositRefundRule(page.getDepositRefundRule());
+        detail.setCancellationPolicyType(page.getCancellationPolicyType());
+        detail.setFreeCancellationDeadline(page.getFreeCancellationDeadline());
+        detail.setFreeCancellationDeadlineText(page.getFreeCancellationDeadlineText());
+        detail.setCancellationPolicyText(page.getCancellationPolicyText());
+        detail.setOrderCreatedTime(page.getOrderCreatedTime());
+        detail.setOrderCreatedTimeText(page.getOrderCreatedTimeText());
+        detail.setGuestCount(page.getGuestCount());
+        detail.setLocationTableText(page.getLocationTableText());
+        detail.setDiningTimeText(page.getDiningTimeText());
+        detail.setContactName(page.getContactName());
+        detail.setContactPhone(page.getContactPhone());
+        detail.setContactText(page.getContactText());
+        detail.setPayAmount(page.getPayAmount());
+        detail.setCanContinuePay(page.getCanContinuePay());
+        detail.setCanCancelReservation(page.getCanCancelReservation());
+        detail.setCanModifyReservation(page.getCanModifyReservation());
+        detail.setCanDelete(page.getCanDelete());
+        detail.setCanBookAgain(page.getCanBookAgain());
+        detail.setOutTradeNo(page.getOutTradeNo());
+    }
+
     private List<BookingTableItemVo> buildBookingTableList(Integer reservationId) {
         if (reservationId == null) {
             return Collections.emptyList();

+ 14 - 2
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java

@@ -168,7 +168,10 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             // 预支付单有效时间 15 分钟(相对超时,格式 1m~15d)
             model.setTimeoutExpress("15m");
             request.setBizModel(model);
+            // 必须用 certificateExecute 请求支付宝网关预创建交易,否则仅 sdkExecute 本地签名不会在支付宝侧落单,查单会报「交易不存在」
+            //AlipayTradeAppPayResponse response = client.certificateExecute(request);
             AlipayTradeAppPayResponse response = client.sdkExecute(request);
+
             String orderStr = response.isSuccess() ? response.getBody() : "";
 
             if (!response.isSuccess()) {
@@ -182,7 +185,7 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             data.put("orderId", order.getId());
             data.put("paymentNo", paymentOrder.getPaymentNo());
             stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(data), REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
-            log.info("商户预订订单预支付成功并写入缓存,storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
+            log.info("商户预订订单预支付成功并写入缓存,storeId={}, orderSn={}, outTradeNo={}, appId={}, aliPayHost={}", storeId, order.getOrderSn(), outTradeNo, config.getAppId(), aliPayHost);
             return R.data(data);
         } catch (AlipayApiException e) {
             log.error("商户预订订单预支付异常,storeId={}, orderId={}", storeId, orderId, e);
@@ -223,7 +226,16 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             request.setBizContent(bizContent.toJSONString());
             AlipayTradeQueryResponse response = client.certificateExecute(request);
             if (!response.isSuccess()) {
-                return R.fail("查询失败:" + response.getSubMsg());
+                String subMsg = response.getSubMsg();
+                String subCode = response.getSubCode();
+                // 支付宝返回「交易不存在」多为:预支付与查询 app_id/环境不一致、或沙箱未落单
+                if (StringUtils.isNotBlank(subCode) && subCode.contains("TRADE_NOT_EXIST")
+                        || StringUtils.isNotBlank(subMsg) && subMsg.contains("交易不存在")) {
+                    log.warn("支付宝查询返回交易不存在,请核对预支付与查询是否同一门店、同一环境。storeId={}, outTradeNo={}, appId={}, aliPayHost={}, subCode={}, subMsg={}",
+                            storeId, outTradeNo, config.getAppId(), aliPayHost, subCode, subMsg);
+                    return R.fail("交易不存在或已过期,请重新发起支付");
+                }
+                return R.fail("查询失败:" + subMsg);
             }
             String tradeStatus = response.getTradeStatus();
             if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {

+ 114 - 2
alien-store/src/main/java/shop/alien/store/vo/ReservationOrderDetailVo.java

@@ -1,5 +1,6 @@
 package shop.alien.store.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -8,15 +9,17 @@ import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.UserReservationOrder;
 import shop.alien.entity.store.vo.UserReservationVo;
 
+import java.math.BigDecimal;
+import java.util.Date;
 import java.util.List;
 
 /**
- * 按订单ID查询的详情:订单 + 门店信息 + 预订设置 + 预订信息 + 定桌信息
+ * 按订单ID查询的详情:订单 + 门店信息 + 预订设置 + 预订信息 + 定桌信息 + 页面展示字段(与 /store/reservationOrder/page 一致)
  *
  * @author system
  */
 @Data
-@ApiModel(value = "ReservationOrderDetailVo", description = "预订订单详情(订单+门店+预订设置+预订+定桌)")
+@ApiModel(value = "ReservationOrderDetailVo", description = "预订订单详情(订单+门店+预订设置+预订+定桌+页面展示)")
 public class ReservationOrderDetailVo {
 
     @ApiModelProperty(value = "预订订单(user_reservation_order)")
@@ -33,4 +36,113 @@ public class ReservationOrderDetailVo {
 
     @ApiModelProperty(value = "定桌信息(桌号、区域等)")
     private List<BookingTableItemVo> tableList;
+
+    // --------- 以下为页面展示字段(与 ReservationOrderPageVo 一致,供前端订单详情/待支付/预订成功等页统一使用) ---------
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+    @ApiModelProperty(value = "订单编号")
+    private String orderSn;
+    @ApiModelProperty(value = "订单状态 0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订")
+    private Integer orderStatus;
+    @ApiModelProperty(value = "支付状态 0:未支付 1:已支付 2:已退款")
+    private Integer paymentStatus;
+    @ApiModelProperty(value = "页面主标题:待支付/预订成功/待使用/已完成/已过期/已取消/已关闭/退款中")
+    private String pageTitle;
+    @ApiModelProperty(value = "页面标题后缀:免费预订/不可免责取消/分情况是否免责")
+    private String pageTitleSuffix;
+    @ApiModelProperty(value = "状态副标题")
+    private String statusSubtitle;
+
+    @ApiModelProperty(value = "支付截止时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date paymentDeadline;
+    @ApiModelProperty(value = "剩余支付秒数")
+    private Integer paymentSecondsLeft;
+    @ApiModelProperty(value = "待支付有效时长(分钟)")
+    private Integer paymentDeadlineMinutes;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+    @ApiModelProperty(value = "门店名称")
+    private String storeName;
+    @ApiModelProperty(value = "门店地址")
+    private String storeAddress;
+    @ApiModelProperty(value = "门店电话")
+    private String storeTel;
+    @ApiModelProperty(value = "门店坐标(经度,纬度)")
+    private String storePosition;
+
+    @ApiModelProperty(value = "就餐日期 yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+    @ApiModelProperty(value = "就餐日期展示 如 02月02日 今天")
+    private String reservationDateText;
+    @ApiModelProperty(value = "人数/桌型描述 如 包间6人")
+    private String guestAndCategoryText;
+    @ApiModelProperty(value = "桌号列表 如 A01,A02")
+    private String tableNumbersText;
+    @ApiModelProperty(value = "就餐时间段 如 10:00-12:03")
+    private String diningTimeSlotText;
+
+    @ApiModelProperty(value = "核销码")
+    private String verificationCode;
+    @ApiModelProperty(value = "核销二维码内容或URL")
+    private String verificationUrl;
+
+    @ApiModelProperty(value = "未按时到店座位保留时长(分钟)")
+    private Integer lateArrivalGraceMinutes;
+    @ApiModelProperty(value = "座位保留说明")
+    private String seatRetentionText;
+    @ApiModelProperty(value = "订金金额(元)")
+    private BigDecimal depositAmount;
+    @ApiModelProperty(value = "订金说明 如 需支付¥50订金")
+    private String depositText;
+    @ApiModelProperty(value = "订金退还规则描述")
+    private String depositRefundRule;
+    @ApiModelProperty(value = "取消政策类型 0:免费预订 1:不可免费取消 2:分情况免责")
+    private Integer cancellationPolicyType;
+    @ApiModelProperty(value = "免费取消截止时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date freeCancellationDeadline;
+    @ApiModelProperty(value = "免费取消截止展示 如 12月31日 09:00前可免责取消")
+    private String freeCancellationDeadlineText;
+    @ApiModelProperty(value = "取消政策说明文案")
+    private String cancellationPolicyText;
+
+    @ApiModelProperty(value = "预订/下单时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date orderCreatedTime;
+    @ApiModelProperty(value = "预订时间展示 如 01月01日 今天")
+    private String orderCreatedTimeText;
+    @ApiModelProperty(value = "预约人数")
+    private Integer guestCount;
+    @ApiModelProperty(value = "位置桌型 如 大厅|A01,A02")
+    private String locationTableText;
+    @ApiModelProperty(value = "就餐时间 如 10:00-12:00")
+    private String diningTimeText;
+    @ApiModelProperty(value = "联系人姓名")
+    private String contactName;
+    @ApiModelProperty(value = "联系人电话")
+    private String contactPhone;
+    @ApiModelProperty(value = "联系方式展示 如 张三 13847859088")
+    private String contactText;
+
+    @ApiModelProperty(value = "订金金额,用于「¥50 继续支付」按钮")
+    private BigDecimal payAmount;
+    @ApiModelProperty(value = "是否可继续支付(待支付且未超时)")
+    private Boolean canContinuePay;
+    @ApiModelProperty(value = "是否显示「取消预订」按钮")
+    private Boolean canCancelReservation;
+    @ApiModelProperty(value = "是否显示「修改预订」按钮")
+    private Boolean canModifyReservation;
+    @ApiModelProperty(value = "是否显示「删除」按钮")
+    private Boolean canDelete;
+    @ApiModelProperty(value = "是否显示「再次预订」按钮")
+    private Boolean canBookAgain;
+    @ApiModelProperty(value = "当前待支付对应的商户订单号 outTradeNo,用于轮询 queryStatus")
+    private String outTradeNo;
+
+    @ApiModelProperty(value = "退款类型(与 user_reservation_order.refund_type 一致)")
+    private Integer refundType;
 }

+ 14 - 2
alien-store/src/main/java/shop/alien/store/vo/ReservationOrderPageVo.java

@@ -30,12 +30,15 @@ public class ReservationOrderPageVo {
     @ApiModelProperty(value = "支付状态 0:未支付 1:已支付 2:已退款")
     private Integer paymentStatus;
 
-    @ApiModelProperty(value = "页面主标题:待支付 / 预订成功(前端据此区分待支付页与预订成功页)")
+    @ApiModelProperty(value = "页面主标题:待支付/预订成功/待使用/已完成/已过期/已取消/已关闭/退款中")
     private String pageTitle;
 
-    @ApiModelProperty(value = "页面标题后缀:待支付时为「不可免责取消/分情况是否免责」;预订成功时为「免费预订/不可免责/分情况是否免责")
+    @ApiModelProperty(value = "页面标题后缀:免费预订/不可免责取消/分情况是否免责")
     private String pageTitleSuffix;
 
+    @ApiModelProperty(value = "状态副标题:如「退款成功,已按原支付路径返回」「正在为您发起退款,请耐心等待」「用户取消-不可免费」")
+    private String statusSubtitle;
+
     // --------- 支付倒计时(待支付时有效) ---------
     @ApiModelProperty(value = "支付截止时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -154,4 +157,13 @@ public class ReservationOrderPageVo {
 
     @ApiModelProperty(value = "是否显示「修改预订」按钮(待使用时为 true)")
     private Boolean canModifyReservation;
+
+    @ApiModelProperty(value = "是否显示「删除」按钮(已完成/已过期/已取消/已关闭/退款中为 true)")
+    private Boolean canDelete;
+
+    @ApiModelProperty(value = "是否显示「再次预订」按钮(已完成/已过期/已取消/已关闭/退款中为 true)")
+    private Boolean canBookAgain;
+
+    @ApiModelProperty(value = "当前待支付对应的商户订单号 outTradeNo,仅待支付且存在支付单时返回,用于轮询 queryStatus")
+    private String outTradeNo;
 }