Selaa lähdekoodia

Merge remote-tracking branch 'origin/sit' into sit

刘云鑫 1 kuukausi sitten
vanhempi
commit
46e425a115
100 muutettua tiedostoa jossa 11249 lisäystä ja 29 poistoa
  1. 107 0
      alien-entity/src/main/java/shop/alien/entity/store/MerchantPaymentOrder.java
  2. 95 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingBusinessHours.java
  3. 74 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingCategory.java
  4. 102 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingSettings.java
  5. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingTable.java
  6. 3 1
      alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java
  7. 111 0
      alien-entity/src/main/java/shop/alien/entity/store/UserReservation.java
  8. 157 0
      alien-entity/src/main/java/shop/alien/entity/store/UserReservationOrder.java
  9. 61 0
      alien-entity/src/main/java/shop/alien/entity/store/UserReservationTable.java
  10. 59 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingBusinessHoursDTO.java
  11. 39 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingCategoryDTO.java
  12. 24 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingCategorySortDTO.java
  13. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingSettingsDTO.java
  14. 37 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingTableBatchDTO.java
  15. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingTableDTO.java
  16. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoDto.java
  17. 62 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/UserReservationDTO.java
  18. 17 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderCountsDto.java
  19. 51 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/ReservationOrderListDto.java
  20. 28 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBookingBusinessHoursVo.java
  21. 54 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBookingTableVo.java
  22. 9 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreInfoVo.java
  23. 9 3
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java
  24. 107 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreReservationListVo.java
  25. 77 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/UserReservationVo.java
  26. 15 0
      alien-entity/src/main/java/shop/alien/mapper/MerchantPaymentOrderMapper.java
  27. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreBookingBusinessHoursMapper.java
  28. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreBookingCategoryMapper.java
  29. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreBookingSettingsMapper.java
  30. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreBookingTableMapper.java
  31. 36 0
      alien-entity/src/main/java/shop/alien/mapper/StoreReservationMapper.java
  32. 51 0
      alien-entity/src/main/java/shop/alien/mapper/UserReservationMapper.java
  33. 50 0
      alien-entity/src/main/java/shop/alien/mapper/UserReservationOrderMapper.java
  34. 21 0
      alien-entity/src/main/java/shop/alien/mapper/UserReservationTableMapper.java
  35. 35 0
      alien-entity/src/main/resources/mapper/StoreBookingBusinessHoursMapper.xml
  36. 27 0
      alien-entity/src/main/resources/mapper/StoreBookingCategoryMapper.xml
  37. 37 0
      alien-entity/src/main/resources/mapper/StoreBookingSettingsMapper.xml
  38. 25 0
      alien-entity/src/main/resources/mapper/StoreBookingTableMapper.xml
  39. 127 0
      alien-entity/src/main/resources/mapper/StoreReservationMapper.xml
  40. 154 0
      alien-entity/src/main/resources/mapper/UserReservationMapper.xml
  41. 73 0
      alien-entity/src/main/resources/mapper/UserReservationOrderMapper.xml
  42. 9 0
      alien-entity/src/main/resources/mapper/UserReservationTableMapper.xml
  43. 8 0
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  44. 35 0
      alien-job/src/main/java/shop/alien/job/store/ReservationTimeoutJob.java
  45. 68 3
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StoreBusinessServiceImpl.java
  46. 54 6
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StoreManageServiceImpl.java
  47. 53 1
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StorePlatformInfoServiceImpl.java
  48. 1 1
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StorePlatformRenovationServiceImpl.java
  49. 12 0
      alien-store/pom.xml
  50. 1 1
      alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java
  51. 88 0
      alien-store/src/main/java/shop/alien/store/controller/MerchantPaymentController.java
  52. 33 0
      alien-store/src/main/java/shop/alien/store/controller/ReservationJobController.java
  53. 84 0
      alien-store/src/main/java/shop/alien/store/controller/ReservationOrderPageController.java
  54. 222 0
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingBusinessHoursController.java
  55. 227 0
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingCategoryController.java
  56. 161 0
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingSettingsController.java
  57. 194 0
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingTableController.java
  58. 1 1
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  59. 235 0
      alien-store/src/main/java/shop/alien/store/controller/StoreReservationController.java
  60. 251 0
      alien-store/src/main/java/shop/alien/store/controller/UserReservationController.java
  61. 90 0
      alien-store/src/main/java/shop/alien/store/controller/UserReservationPaymentController.java
  62. 62 0
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentOrderService.java
  63. 21 0
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentQueryService.java
  64. 63 0
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentSyncScheduler.java
  65. 15 0
      alien-store/src/main/java/shop/alien/store/service/RefundRecordAsyncService.java
  66. 23 0
      alien-store/src/main/java/shop/alien/store/service/ReservationOrderListService.java
  67. 19 0
      alien-store/src/main/java/shop/alien/store/service/ReservationOrderPageService.java
  68. 31 0
      alien-store/src/main/java/shop/alien/store/service/ReservationOrderPaymentTimeoutService.java
  69. 58 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingBusinessHoursService.java
  70. 65 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingCategoryService.java
  71. 46 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingSettingsService.java
  72. 68 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingTableService.java
  73. 11 0
      alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java
  74. 21 0
      alien-store/src/main/java/shop/alien/store/service/StorePaymentConfigCertService.java
  75. 75 0
      alien-store/src/main/java/shop/alien/store/service/StoreReservationService.java
  76. 43 0
      alien-store/src/main/java/shop/alien/store/service/UserReservationOrderService.java
  77. 193 0
      alien-store/src/main/java/shop/alien/store/service/UserReservationService.java
  78. 100 0
      alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentOrderServiceImpl.java
  79. 91 0
      alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentQueryServiceImpl.java
  80. 40 0
      alien-store/src/main/java/shop/alien/store/service/impl/RefundRecordAsyncServiceImpl.java
  81. 224 0
      alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderListServiceImpl.java
  82. 332 0
      alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPageServiceImpl.java
  83. 101 0
      alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPaymentTimeoutServiceImpl.java
  84. 289 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingBusinessHoursServiceImpl.java
  85. 310 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingCategoryServiceImpl.java
  86. 379 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java
  87. 390 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingTableServiceImpl.java
  88. 75 12
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  89. 168 0
      alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigCertServiceImpl.java
  90. 1279 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreReservationServiceImpl.java
  91. 68 0
      alien-store/src/main/java/shop/alien/store/service/impl/UserReservationOrderServiceImpl.java
  92. 1193 0
      alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java
  93. 53 0
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategy.java
  94. 47 0
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategyFactory.java
  95. 403 0
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java
  96. 505 0
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java
  97. 198 0
      alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java
  98. 27 0
      alien-store/src/main/java/shop/alien/store/vo/BookingTableItemVo.java
  99. 151 0
      alien-store/src/main/java/shop/alien/store/vo/ReservationOrderDetailVo.java
  100. 31 0
      alien-store/src/main/java/shop/alien/store/vo/ReservationOrderListResultVo.java

+ 107 - 0
alien-entity/src/main/java/shop/alien/entity/store/MerchantPaymentOrder.java

@@ -0,0 +1,107 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 商户支付单表(关联 user_reservation_order,记录每笔支付与支付宝/微信流水)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("merchant_payment_order")
+@ApiModel(value = "MerchantPaymentOrder对象", description = "商户支付单表")
+public class MerchantPaymentOrder {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "支付单号(业务侧唯一)")
+    @TableField("payment_no")
+    private String paymentNo;
+
+    @ApiModelProperty(value = "业务订单类型: reservation_order-预订订单")
+    @TableField("order_type")
+    private String orderType;
+
+    @ApiModelProperty(value = "业务订单主键 user_reservation_order.id")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "业务订单编号 user_reservation_order.order_sn")
+    @TableField("order_sn")
+    private String orderSn;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "支付方式: alipay-支付宝, wechatPay-微信")
+    @TableField("pay_type")
+    private String payType;
+
+    @ApiModelProperty(value = "商户订单号(传给支付宝/微信)")
+    @TableField("out_trade_no")
+    private String outTradeNo;
+
+    @ApiModelProperty(value = "第三方交易号(支付宝trade_no/微信transaction_id)")
+    @TableField("trade_no")
+    private String tradeNo;
+
+    @ApiModelProperty(value = "支付金额(元)")
+    @TableField("pay_amount")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "支付状态: 0-待支付, 1-已支付, 2-已关闭, 3-已退款")
+    @TableField("pay_status")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付完成时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    @ApiModelProperty(value = "付款人用户ID")
+    @TableField("payer_user_id")
+    private Integer payerUserId;
+
+    @ApiModelProperty(value = "订单描述/商品标题")
+    @TableField("subject")
+    private String subject;
+
+    @ApiModelProperty(value = "删除标记 0:未删除 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 95 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingBusinessHours.java

@@ -0,0 +1,95 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务营业时间表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_booking_business_hours")
+@ApiModel(value = "StoreBookingBusinessHours对象", description = "预订服务营业时间表")
+public class StoreBookingBusinessHours {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+
+    @ApiModelProperty(value = "关联store_booking_settings表 id")
+    @TableField("settings_id")
+    private Integer settingsId;
+
+    @ApiModelProperty(value = "营业类型(1:正常营业时间, 2:特殊营业时间)")
+    @TableField("business_type")
+    private Integer businessType;
+
+    @ApiModelProperty(value = "节假日类型(当business_type=2时使用,如:春节、元旦、国庆节等,为空表示正常营业)")
+    @TableField("holiday_type")
+    private String holidayType;
+
+    @ApiModelProperty(value = "节假日日期(当business_type=2时使用,用于指定具体日期,可选,格式:yyyy-MM-dd)")
+    @TableField("holiday_date")
+    private String holidayDate;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)")
+    @TableField("booking_time_type")
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "开始时间(HH:mm格式,非全天时必填)")
+    @TableField("start_time")
+    private String startTime;
+
+    @ApiModelProperty(value = "结束时间(HH:mm格式,非全天时必填)")
+    @TableField("end_time")
+    private String endTime;
+
+    @ApiModelProperty(value = "排序(用于同一门店多条记录的排序)")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+
+    @ApiModelProperty(value = "关联essential_holiday_comparison id")
+    @TableField("essential_id")
+    private Integer essentialId;
+
+    @ApiModelProperty(value = "预约 0免费 1付费")
+    @TableField("reservation")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额")
+    @TableField("reservation_money")
+    private Double reservationMoney;
+}

+ 74 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingCategory.java

@@ -0,0 +1,74 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务分类表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_booking_category")
+@ApiModel(value = "StoreBookingCategory对象", description = "预订服务分类表")
+public class StoreBookingCategory {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类名称")
+    @TableField("category_name")
+    private String categoryName;
+
+    @ApiModelProperty(value = "平面图(包含桌号),最多9张图片,用逗号分隔")
+    @TableField("floor_plan_images")
+    private String floorPlanImages;
+
+    @ApiModelProperty(value = "是否显示(0:隐藏, 1:显示)")
+    @TableField("is_display")
+    private Integer isDisplay;
+
+    @ApiModelProperty(value = "最长预订时间(分钟)")
+    @TableField("max_booking_time")
+    private Integer maxBookingTime;
+
+    @ApiModelProperty(value = "排序")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 102 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingSettings.java

@@ -0,0 +1,102 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务信息设置表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_booking_settings")
+@ApiModel(value = "StoreBookingSettings对象", description = "预订服务信息设置表")
+public class StoreBookingSettings {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "未按时到店是否保留位置(0:不保留位置, 1:保留位置)")
+    @TableField("retain_position_flag")
+    private Integer retainPositionFlag;
+
+    @ApiModelProperty(value = "保留时长(分钟)")
+    @TableField("retention_duration")
+    private Integer retentionDuration;
+
+    @ApiModelProperty(value = "预订日期显示(天)")
+    @TableField("booking_date_display_days")
+    private Integer bookingDateDisplayDays;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)")
+    @TableField("booking_time_type")
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "预订开始时间(HH:mm格式,非全天时必填)")
+    @TableField("booking_start_time")
+    private String bookingStartTime;
+
+    @ApiModelProperty(value = "预订结束时间(HH:mm格式,非全天时必填)")
+    @TableField("booking_end_time")
+    private String bookingEndTime;
+
+    @ApiModelProperty(value = "单时段最大容纳人数")
+    @TableField("max_capacity_per_slot")
+    private Integer maxCapacityPerSlot;
+
+    @ApiModelProperty(value = "预约(0:免费, 1:付费)")
+    @TableField("reservation")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额(元)")
+    @TableField("reservation_money")
+    private Double reservationMoney;
+
+    @ApiModelProperty(value = "取消预订退费时长设置,需提前(小时)")
+    @TableField("off_unsubscribe_hours")
+    private Integer offUnsubscribeHours;
+
+    @ApiModelProperty(value = "营业时间结束前多少分钟不可预订")
+    @TableField("booking_not_available_time")
+    private Integer bookingNotAvailableTime;
+
+    @ApiModelProperty(value = "图片")
+    @TableField("img_url")
+    private String imgUrl;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingTable.java

@@ -0,0 +1,66 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务桌号表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_booking_table")
+@ApiModel(value = "StoreBookingTable对象", description = "预订服务桌号表")
+public class StoreBookingTable {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类ID(关联store_booking_category表)")
+    @TableField("category_id")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "桌号")
+    @TableField("table_number")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "座位数")
+    @TableField("seating_capacity")
+    private Integer seatingCapacity;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 3 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java

@@ -359,5 +359,7 @@ public class StoreInfo {
     @TableField("tableware_fee")
     private Integer tablewareFee;
 
-
+    @ApiModelProperty(value = "预约服务 0不提供 1提供")
+    @TableField("booking_service")
+    private Integer bookingService;
 }

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

@@ -0,0 +1,111 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户预约信息表(订桌时间可往后延续)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("user_reservation")
+@ApiModel(value = "UserReservation对象", description = "用户预约信息表")
+public class UserReservation {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "预约号(券码)")
+    @TableField("reservation_no")
+    private String reservationNo;
+
+    @ApiModelProperty(value = "用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "预约日期")
+    @TableField("reservation_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+
+    @ApiModelProperty(value = "预约开始时间 HH:mm")
+    @TableField("start_time")
+    private String startTime;
+
+    @ApiModelProperty(value = "预约结束时间 HH:mm(可往后延续)")
+    @TableField("end_time")
+    private String endTime;
+
+    @ApiModelProperty(value = "预约人数")
+    @TableField("guest_count")
+    private Integer guestCount;
+
+    @ApiModelProperty(value = "偏好区域/分类ID(如大厅/包间)")
+    @TableField("category_id")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "预约状态 0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "实际到店时间")
+    @TableField("actual_arrival_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date actualArrivalTime;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "预约人姓名")
+    @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 = "取消原因(商家端取消)")
+    @TableField("reason")
+    private String reason;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 157 - 0
alien-entity/src/main/java/shop/alien/entity/store/UserReservationOrder.java

@@ -0,0 +1,157 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 用户预订订单表
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("user_reservation_order")
+@ApiModel(value = "UserReservationOrder对象", description = "用户预订订单表")
+public class UserReservationOrder {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "订单编号(唯一,如 YS+序列)")
+    @TableField("order_sn")
+    private String orderSn;
+
+    @ApiModelProperty(value = "关联预约ID(user_reservation.id)")
+    @TableField("reservation_id")
+    private Integer reservationId;
+
+    @ApiModelProperty(value = "用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "订单状态 0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订")
+    @TableField("order_status")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "支付状态 0:未支付 1:已支付 2:已退款 3:部分退款")
+    @TableField("payment_status")
+    private Integer paymentStatus;
+
+    @ApiModelProperty(value = "订单费用类型 0-免费,1-收费")
+    @TableField("order_cost_type")
+    private Integer orderCostType;
+
+    @ApiModelProperty(value = "订金金额(元),0表示免费预订")
+    @TableField("deposit_amount")
+    private BigDecimal depositAmount;
+
+    @ApiModelProperty(value = "支付方式:微信支付等")
+    @TableField("payment_method")
+    private String paymentMethod;
+
+    @ApiModelProperty(value = "支付截止时间(待支付时剩余时间)")
+    @TableField("payment_deadline")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date paymentDeadline;
+
+    @ApiModelProperty(value = "实际支付时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    @ApiModelProperty(value = "使用凭证/券码(核销用)")
+    @TableField("verification_code")
+    private String verificationCode;
+
+    @ApiModelProperty(value = "使用凭证url")
+    @TableField("verification_url")
+    private String verificationUrl;
+
+    @ApiModelProperty(value = "取消政策类型 0:免费预订 1:不可免费取消 2:分情况免责")
+    @TableField("cancellation_policy_type")
+    private Integer cancellationPolicyType;
+
+    @ApiModelProperty(value = "免费取消截止时间(分情况免责时有值)")
+    @TableField("free_cancellation_deadline")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date freeCancellationDeadline;
+
+    @ApiModelProperty(value = "未按时到店座位保留时长(分钟),0或NULL表示不保留")
+    @TableField("late_arrival_grace_minutes")
+    private Integer lateArrivalGraceMinutes;
+
+    @ApiModelProperty(value = "订金退还规则描述")
+    @TableField("deposit_refund_rule")
+    private String depositRefundRule;
+
+    @ApiModelProperty(value = "退款金额(元)")
+    @TableField("refund_amount")
+    private BigDecimal refundAmount;
+
+    @ApiModelProperty(value = "退款时间")
+    @TableField("refund_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date refundTime;
+
+    @ApiModelProperty(value = "退款原因")
+    @TableField("refund_reason")
+    private String refundReason;
+
+    @ApiModelProperty(value = "退款类型 0:用户取消 1:商家退款 2:部分退款等")
+    @TableField("refund_type")
+    private Integer refundType;
+
+    @ApiModelProperty(value = "是否商家代订 0:否 1:是")
+    @TableField("is_merchant_reservation")
+    private Integer isMerchantReservation;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "删除标识(0:未删除, 1:已删除)")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+
+    @ApiModelProperty(value = "商户订单号(传给支付宝/微信")
+    @TableField("out_trade_no")
+    private String outTradeNo;
+}

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

@@ -0,0 +1,61 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户预约与桌号关联表(中间表:一个预约可对应多张桌)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("user_reservation_table")
+@ApiModel(value = "UserReservationTable对象", description = "用户预约与桌号关联表")
+public class UserReservationTable {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "预约ID")
+    @TableField("reservation_id")
+    private Integer reservationId;
+
+    @ApiModelProperty(value = "桌号信息ID(关联store_booking_table)")
+    @TableField("table_id")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "排序")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 59 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingBusinessHoursDTO.java

@@ -0,0 +1,59 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.EssentialHolidayComparison;
+
+/**
+ * 预订服务营业时间DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingBusinessHoursDTO", description = "预订服务营业时间DTO")
+public class StoreBookingBusinessHoursDTO {
+
+    @ApiModelProperty(value = "营业时间ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "关联store_booking_settings表 id", required = true)
+    private Integer settingsId;
+
+    @ApiModelProperty(value = "营业类型(1:正常营业时间, 2:特殊营业时间)", required = true)
+    private Integer businessType;
+
+    @ApiModelProperty(value = "节假日类型(当business_type=2时使用,如:春节、元旦、国庆节等)")
+    private String holidayType;
+
+    @ApiModelProperty(value = "节假日日期(当business_type=2时使用,用于指定具体日期,可选,格式:yyyy-MM-dd)")
+    private String holidayDate;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)", required = true)
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "开始时间(HH:mm格式,非全天时必填)")
+    private String startTime;
+
+    @ApiModelProperty(value = "结束时间(HH:mm格式,非全天时必填)")
+    private String endTime;
+
+    @ApiModelProperty(value = "排序(用于同一门店多条记录的排序)")
+    private Integer sort;
+
+    @ApiModelProperty(value = "关联essential_holiday_comparison id")
+    private Integer essentialId;
+
+    @ApiModelProperty(value = "预约 0免费 1付费")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额")
+    private Double reservationMoney;
+
+    @ApiModelProperty(value = "关联的节假日信息")
+    private EssentialHolidayComparison holidayInfo;
+}

+ 39 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingCategoryDTO.java

@@ -0,0 +1,39 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 预订服务分类DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingCategoryDTO", description = "预订服务分类DTO")
+public class StoreBookingCategoryDTO {
+
+    @ApiModelProperty(value = "分类ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类名称", required = true)
+    private String categoryName;
+
+    @ApiModelProperty(value = "平面图(包含桌号),最多9张图片,用逗号分隔", required = true)
+    private String floorPlanImages;
+
+    @ApiModelProperty(value = "是否显示(0:隐藏, 1:显示)", required = true)
+    private Integer isDisplay;
+
+    @ApiModelProperty(value = "最长预订时间(分钟)", required = true)
+    private Integer maxBookingTime;
+
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+}

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingCategorySortDTO.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 预订服务分类排序DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingCategorySortDTO", description = "预订服务分类排序DTO")
+public class StoreBookingCategorySortDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类ID列表(按顺序排列)", required = true)
+    private List<Integer> categoryIds;
+}

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingSettingsDTO.java

@@ -0,0 +1,66 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 预订服务信息设置DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingSettingsDTO", description = "预订服务信息设置DTO")
+public class StoreBookingSettingsDTO {
+
+    @ApiModelProperty(value = "设置ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "未按时到店是否保留位置(0:不保留位置, 1:保留位置)", required = true)
+    private Integer retainPositionFlag;
+
+    @ApiModelProperty(value = "保留时长(分钟)", required = true)
+    private Integer retentionDuration;
+
+    @ApiModelProperty(value = "预订日期显示(天)", required = true)
+    private Integer bookingDateDisplayDays;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)", required = true)
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "预订开始时间(HH:mm格式,非全天时必填)")
+    private String bookingStartTime;
+
+    @ApiModelProperty(value = "预订结束时间(HH:mm格式,非全天时必填)")
+    private String bookingEndTime;
+
+    @ApiModelProperty(value = "单时段最大容纳人数", required = true)
+    private Integer maxCapacityPerSlot;
+
+    @ApiModelProperty(value = "预约(0:免费, 1:付费)")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额(元)")
+    private Double reservationMoney;
+
+    @ApiModelProperty(value = "取消预订退费时长设置,需提前(小时)", required = true)
+    private Integer offUnsubscribeHours;
+
+    @ApiModelProperty(value = "营业时间结束前多少分钟不可预订", required = true)
+    private Integer bookingNotAvailableTime;
+
+    @ApiModelProperty(value = "图片")
+    private String imgUrl;
+
+    @ApiModelProperty(value = "正常营业时间(business_type=0)")
+    private StoreBookingBusinessHoursDTO normalBusinessHours;
+
+    @ApiModelProperty(value = "特殊营业时间列表(business_type=1,节假日营业时间)")
+    private List<StoreBookingBusinessHoursDTO> specialBusinessHoursList;
+}

+ 37 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingTableBatchDTO.java

@@ -0,0 +1,37 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 预订服务桌号批量添加DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingTableBatchDTO", description = "预订服务桌号批量添加DTO")
+public class StoreBookingTableBatchDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类ID(关联store_booking_category表)", required = true)
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "桌号列表", required = true)
+    private List<TableItem> tables;
+
+    @Data
+    @ApiModel(value = "TableItem", description = "桌号项")
+    public static class TableItem {
+        @ApiModelProperty(value = "桌号", required = true)
+        private String tableNumber;
+
+        @ApiModelProperty(value = "座位数", required = true)
+        private Integer seatingCapacity;
+    }
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingTableDTO.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 预订服务桌号DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingTableDTO", description = "预订服务桌号DTO")
+public class StoreBookingTableDTO {
+
+    @ApiModelProperty(value = "桌号ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类ID(关联store_booking_category表)", required = true)
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "桌号", required = true)
+    private String tableNumber;
+
+    @ApiModelProperty(value = "座位数", required = true)
+    private Integer seatingCapacity;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoDto.java

@@ -262,4 +262,7 @@ public class StoreInfoDto {
     @ApiModelProperty(value = "门店类型(0-其他,1-装修公司)")
     @TableField("store_tickets")
     private Integer storeTickets;
+
+    @ApiModelProperty(value = "预约服务 0不提供 1提供")
+    private Integer bookingService;
 }

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

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

+ 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;
+}

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

@@ -0,0 +1,51 @@
+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 = "支付状态 0:未支付 1:已支付 2:已退款 3:部分退款")
+    private Integer paymentStatus;
+    @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;
+    @ApiModelProperty(value = "核销码/券码")
+    private String verificationCode;
+    @ApiModelProperty(value = "商户订单号(out_trade_no)")
+    private String outTradeNo;
+    @ApiModelProperty(value = "支付方式/类型,如 wechatPay、alipay")
+    private String payType;
+    @ApiModelProperty(value = "商家取消原因(user_reservation.reason,商家取消时有值)")
+    private String merchantCancelReason;
+    @ApiModelProperty(value = "经营板块ID(store_info.business_section)")
+    private Integer businessSection;
+}

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBookingBusinessHoursVo.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.store.EssentialHolidayComparison;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+
+/**
+ * 预订服务营业时间Vo
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@JsonInclude
+@ApiModel(value = "StoreBookingBusinessHoursVo对象", description = "预订服务营业时间Vo")
+public class StoreBookingBusinessHoursVo extends StoreBookingBusinessHours {
+
+    @ApiModelProperty(value = "关联的节假日信息")
+    private EssentialHolidayComparison holidayInfo;
+
+    @ApiModelProperty(value = "营业日期(从 holidayDate 或 holidayInfo 中获取)")
+    private String businessDate;
+}

+ 54 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBookingTableVo.java

@@ -0,0 +1,54 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务桌号VO(包含分类名称)
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingTableVo", description = "预订服务桌号VO")
+public class StoreBookingTableVo {
+
+    @ApiModelProperty(value = "主键ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "分类ID(关联store_booking_category表)")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "分类名称")
+    private String categoryName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "座位数")
+    private Integer seatingCapacity;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    private Integer updatedUserId;
+}

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

@@ -75,6 +75,12 @@ public class StoreInfoVo extends StoreInfo {
     @ApiModelProperty(value = "门店坐标纬度")
     private String storePositionLatitude;
 
+    @ApiModelProperty(value = "门店经度")
+    private String longitude;
+
+    @ApiModelProperty(value = "门店纬度")
+    private String latitude;
+
     @ApiModelProperty(value = "token")
     private String token;
 
@@ -145,6 +151,9 @@ public class StoreInfoVo extends StoreInfo {
     @ApiModelProperty(value = "最近地铁站距离")
     private double distance2;
 
+    @ApiModelProperty(value = "门店位置/地址(与 getClientStoreDetail 一致,即门店地址)")
+    private String storeLocation;
+
     @ApiModelProperty(value = "是否收藏")
     private Integer collection;
 

+ 9 - 3
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java

@@ -3,10 +3,10 @@ package shop.alien.entity.store.vo;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
-import shop.alien.entity.store.StoreBusinessInfo;
 import shop.alien.entity.store.StoreImg;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreLabel;
+import shop.alien.entity.store.vo.StoreBusinessInfoVo;
 
 import java.util.Date;
 import java.util.List;
@@ -51,8 +51,11 @@ public class StoreMainInfoVo extends StoreInfo {
     @ApiModelProperty(value = "门店标签")
     StoreLabel storeLabel;
 
-    @ApiModelProperty(value = "营业时间")
-    List<StoreBusinessInfo> storeBusinessInfo;
+    @ApiModelProperty(value = "营业时间(包含节假日信息)")
+    List<StoreBusinessInfoVo> storeBusinessInfo;
+
+    @ApiModelProperty(value = "门店营业时间列表(包含节假日信息)")
+    private List<StoreBusinessInfoVo> storeBusinessInfoVos;
 
     @ApiModelProperty(value = "门店头像")
     String headImgUrl;
@@ -124,4 +127,7 @@ public class StoreMainInfoVo extends StoreInfo {
     @ApiModelProperty(value = "评价数量")
     private Integer ratingCount;
 
+    @ApiModelProperty(value = "是否提供预约")
+    private Integer bookingService;
+
 }

+ 107 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreReservationListVo.java

@@ -0,0 +1,107 @@
+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;
+
+/**
+ * 商家端预约信息列表 VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreReservationListVo", description = "商家端预约信息列表VO")
+public class StoreReservationListVo {
+
+    @ApiModelProperty(value = "预约ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "预约号(券码)")
+    private String reservationNo;
+
+    @ApiModelProperty(value = "预约日期")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reservationDate;
+
+    @ApiModelProperty(value = "星期(如:周一、周二)")
+    private String weekDay;
+
+    @ApiModelProperty(value = "分类ID")
+    private Integer categoryId;
+
+    @ApiModelProperty(value = "分类名称(如:大厅、包间)")
+    private String categoryName;
+
+    @ApiModelProperty(value = "预约人数")
+    private Integer guestCount;
+
+    @ApiModelProperty(value = "桌号列表(逗号分隔,如:A01,A02)")
+    private String tableNumbers;
+
+    @ApiModelProperty(value = "预约开始时间 HH:mm")
+    private String startTime;
+
+    @ApiModelProperty(value = "预约结束时间 HH:mm")
+    private String endTime;
+
+    @ApiModelProperty(value = "时间段(如:10:00-12:00)")
+    private String timeSlot;
+
+    @ApiModelProperty(value = "用户ID")
+    private Integer userId;
+
+    @ApiModelProperty(value = "客户姓名")
+    private String customerName;
+
+    @ApiModelProperty(value = "联系方式(手机号)")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注信息")
+    private String remark;
+
+    @ApiModelProperty(value = "取消原因(商家端取消)")
+    private String reason;
+
+    @ApiModelProperty(value = "预约状态 0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时")
+    private Integer status;
+
+    @ApiModelProperty(value = "状态文本(待使用、已完成、退款等)")
+    private String statusText;
+
+    @ApiModelProperty(value = "订单金额(元)")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "订金金额(元)")
+    private BigDecimal depositAmount;
+
+    @ApiModelProperty(value = "退款金额(元)")
+    private BigDecimal refundAmount;
+
+    @ApiModelProperty(value = "订单状态 0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "订单状态文本(待支付、待使用、已完成、退款等)")
+    private String orderStatusText;
+
+    @ApiModelProperty(value = "订单费用类型 0-免费,1-收费")
+    private Integer orderCostType;
+
+    @ApiModelProperty(value = "商户订单号")
+    private String outTradeNo;
+
+    @ApiModelProperty(value = "支付方式")
+    private String paymentMethod;
+
+    @ApiModelProperty(value = "实际到店时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date actualArrivalTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}

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

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

+ 15 - 0
alien-entity/src/main/java/shop/alien/mapper/MerchantPaymentOrderMapper.java

@@ -0,0 +1,15 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.MerchantPaymentOrder;
+
+/**
+ * 商户支付单表 Mapper 接口
+ *
+ * @author system
+ */
+@Mapper
+public interface MerchantPaymentOrderMapper extends BaseMapper<MerchantPaymentOrder> {
+
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreBookingBusinessHoursMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+
+/**
+ * 预订服务营业时间表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingBusinessHoursMapper extends BaseMapper<StoreBookingBusinessHours> {
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreBookingCategoryMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreBookingCategory;
+
+/**
+ * 预订服务分类表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingCategoryMapper extends BaseMapper<StoreBookingCategory> {
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreBookingSettingsMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreBookingSettings;
+
+/**
+ * 预订服务信息设置表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingSettingsMapper extends BaseMapper<StoreBookingSettings> {
+}

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreBookingTableMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreBookingTable;
+
+/**
+ * 预订服务桌号表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingTableMapper extends BaseMapper<StoreBookingTable> {
+}

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

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

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

@@ -0,0 +1,51 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户预约信息表 Mapper 接口
+ *
+ * @author system
+ */
+public interface UserReservationMapper extends BaseMapper<UserReservation> {
+
+    /**
+     * 查询商家端预约信息列表
+     *
+     * @param storeId    门店ID(必填)
+     * @param status     预约状态(可选,0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时)
+     * @param dateFrom   预约日期起(可选)
+     * @param dateTo     预约日期止(可选)
+     * @param orderStatus 订单状态(可选,0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订)
+     * @return 预约信息列表
+     */
+    List<StoreReservationListVo> getStoreReservationList(
+            @Param("storeId") Integer storeId,
+            @Param("status") Integer status,
+            @Param("dateFrom") Date dateFrom,
+            @Param("dateTo") Date dateTo,
+            @Param("orderStatus") Integer orderStatus
+    );
+
+    /**
+     * 按主键物理删除预约(与 add 新建的表对应,用于 delete 接口)
+     *
+     * @param id 预约ID
+     * @return 删除行数
+     */
+    int physicalDeleteById(@Param("id") Integer id);
+
+    /**
+     * 关联查询:订单为待使用且预约结束时间已过的 reservation_id 列表。
+     * 用于定时任务将「未到店超时」的预订与订单批量更新。
+     *
+     * @return 需要标记为已过期/未到店超时的 user_reservation.id 列表
+     */
+    List<Integer> listReservationIdsForTimeoutMark();
+}

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

@@ -0,0 +1,50 @@
+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 接口
+ *
+ * @author system
+ */
+@Mapper
+public interface UserReservationOrderMapper extends BaseMapper<UserReservationOrder> {
+
+    /**
+     * 按预约ID物理删除订单(与 add 新建的 user_reservation_order 对应,用于 delete 接口)
+     *
+     * @param reservationId 预约ID
+     * @return 删除行数
+     */
+    @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);
+}

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

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

+ 35 - 0
alien-entity/src/main/resources/mapper/StoreBookingBusinessHoursMapper.xml

@@ -0,0 +1,35 @@
+<?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.StoreBookingBusinessHoursMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreBookingBusinessHours">
+        <id column="id" property="id" />
+        <result column="store_id" property="storeId" />
+        <result column="settings_id" property="settingsId" />
+        <result column="business_type" property="businessType" />
+        <result column="holiday_type" property="holidayType" />
+        <result column="holiday_date" property="holidayDate" />
+        <result column="booking_time_type" property="bookingTimeType" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="sort" property="sort" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+        <result column="essential_id" property="essentialId" />
+        <result column="reservation" property="reservation" />
+        <result column="reservation_money" property="reservationMoney" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, store_id, settings_id, business_type, holiday_type, holiday_date, 
+        booking_time_type, start_time, end_time, sort, 
+        delete_flag, created_time, created_user_id, updated_time, updated_user_id,
+        essential_id, reservation, reservation_money
+    </sql>
+
+</mapper>

+ 27 - 0
alien-entity/src/main/resources/mapper/StoreBookingCategoryMapper.xml

@@ -0,0 +1,27 @@
+<?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.StoreBookingCategoryMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreBookingCategory">
+        <id column="id" property="id" />
+        <result column="store_id" property="storeId" />
+        <result column="category_name" property="categoryName" />
+        <result column="floor_plan_images" property="floorPlanImages" />
+        <result column="is_display" property="isDisplay" />
+        <result column="max_booking_time" property="maxBookingTime" />
+        <result column="sort" property="sort" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, store_id, category_name, floor_plan_images, is_display, max_booking_time, sort, 
+        delete_flag, created_time, created_user_id, updated_time, updated_user_id
+    </sql>
+
+</mapper>

+ 37 - 0
alien-entity/src/main/resources/mapper/StoreBookingSettingsMapper.xml

@@ -0,0 +1,37 @@
+<?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.StoreBookingSettingsMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreBookingSettings">
+        <id column="id" property="id" />
+        <result column="store_id" property="storeId" />
+        <result column="retain_position_flag" property="retainPositionFlag" />
+        <result column="retention_duration" property="retentionDuration" />
+        <result column="booking_date_display_days" property="bookingDateDisplayDays" />
+        <result column="booking_time_type" property="bookingTimeType" />
+        <result column="booking_start_time" property="bookingStartTime" />
+        <result column="booking_end_time" property="bookingEndTime" />
+        <result column="max_capacity_per_slot" property="maxCapacityPerSlot" />
+        <result column="reservation" property="reservation" />
+        <result column="reservation_money" property="reservationMoney" />
+        <result column="off_unsubscribe_hours" property="offUnsubscribeHours" />
+        <result column="booking_not_available_time" property="bookingNotAvailableTime" />
+        <result column="img_url" property="imgUrl" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, store_id, retain_position_flag, retention_duration, booking_date_display_days, 
+        booking_time_type, booking_start_time, booking_end_time, max_capacity_per_slot, 
+        reservation, reservation_money, off_unsubscribe_hours, booking_not_available_time, 
+        img_url, 
+        delete_flag, created_time, created_user_id, updated_time, updated_user_id
+    </sql>
+
+</mapper>

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

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.StoreBookingTableMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreBookingTable">
+        <id column="id" property="id" />
+        <result column="store_id" property="storeId" />
+        <result column="category_id" property="categoryId" />
+        <result column="table_number" property="tableNumber" />
+        <result column="seating_capacity" property="seatingCapacity" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, store_id, category_id, table_number, seating_capacity, 
+        delete_flag, created_time, created_user_id, updated_time, updated_user_id
+    </sql>
+
+</mapper>

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

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

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

@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.UserReservationMapper">
+
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.UserReservation">
+        <id column="id" property="id" />
+        <result column="reservation_no" property="reservationNo" />
+        <result column="user_id" property="userId" />
+        <result column="store_id" property="storeId" />
+        <result column="reservation_date" property="reservationDate" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="guest_count" property="guestCount" />
+        <result column="category_id" property="categoryId" />
+        <result column="status" property="status" />
+        <result column="actual_arrival_time" property="actualArrivalTime" />
+        <result column="remark" property="remark" />
+        <result column="reservation_user_name" property="reservationUserName" />
+        <result column="reservation_user_gender" property="reservationUserGender" />
+        <result column="reservation_user_phone" property="reservationUserPhone" />
+        <result column="reason" property="reason" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 商家端预约信息列表结果映射 -->
+    <resultMap id="StoreReservationListVoMap" type="shop.alien.entity.store.vo.StoreReservationListVo">
+        <id column="id" property="id" />
+        <result column="reservation_no" property="reservationNo" />
+        <result column="reservation_date" property="reservationDate" />
+        <result column="week_day" property="weekDay" />
+        <result column="category_id" property="categoryId" />
+        <result column="category_name" property="categoryName" />
+        <result column="guest_count" property="guestCount" />
+        <result column="table_numbers" property="tableNumbers" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="time_slot" property="timeSlot" />
+        <result column="user_id" property="userId" />
+        <result column="customer_name" property="customerName" />
+        <result column="contact_phone" property="contactPhone" />
+        <result column="remark" property="remark" />
+        <result column="status" property="status" />
+        <result column="status_text" property="statusText" />
+        <result column="total_amount" property="totalAmount" />
+        <result column="deposit_amount" property="depositAmount" />
+        <result column="order_status" property="orderStatus" />
+        <result column="order_status_text" property="orderStatusText" />
+        <result column="actual_arrival_time" property="actualArrivalTime" />
+        <result column="created_time" property="createdTime" />
+    </resultMap>
+
+    <!-- 查询商家端预约信息列表 -->
+    <select id="getStoreReservationList" resultMap="StoreReservationListVoMap">
+        SELECT
+            ur.id,
+            ur.reservation_no,
+            ur.reservation_date,
+            CASE DAYOFWEEK(ur.reservation_date)
+                WHEN 1 THEN '周日'
+                WHEN 2 THEN '周一'
+                WHEN 3 THEN '周二'
+                WHEN 4 THEN '周三'
+                WHEN 5 THEN '周四'
+                WHEN 6 THEN '周五'
+                WHEN 7 THEN '周六'
+            END AS week_day,
+            ur.category_id,
+            sbc.category_name,
+            ur.guest_count,
+            GROUP_CONCAT(sbt.table_number ORDER BY urt.sort ASC SEPARATOR ',') AS table_numbers,
+            ur.start_time,
+            ur.end_time,
+            CONCAT(IFNULL(ur.start_time, ''), '-', IFNULL(ur.end_time, '')) AS time_slot,
+            ur.user_id,
+            IFNULL(lu.real_name, lu.user_name) AS customer_name,
+            lu.user_phone AS contact_phone,
+            ur.remark,
+            ur.status,
+            CASE ur.status
+                WHEN 0 THEN '待确认'
+                WHEN 1 THEN '待使用'
+                WHEN 2 THEN '已完成'
+                WHEN 3 THEN '已取消'
+                WHEN 4 THEN '未到店'
+                ELSE '未知'
+            END AS status_text,
+            uro.refund_amount,
+            uro.deposit_amount,
+            uro.order_status,
+            CASE uro.order_status
+                WHEN 0 THEN '待支付'
+                WHEN 1 THEN '待使用'
+                WHEN 2 THEN '已完成'
+                WHEN 3 THEN '已过期'
+                WHEN 4 THEN '已取消'
+                WHEN 5 THEN '已关闭'
+                WHEN 6 THEN '退款中'
+                WHEN 7 THEN '已退款'
+                WHEN 8 THEN '商家预订'
+                ELSE '未知'
+            END AS order_status_text,
+            ur.actual_arrival_time,
+            ur.created_time
+        FROM
+            user_reservation ur
+        LEFT JOIN store_booking_category sbc ON ur.category_id = sbc.id AND sbc.delete_flag = 0
+        LEFT JOIN user_reservation_table urt ON ur.id = urt.reservation_id AND urt.delete_flag = 0
+        LEFT JOIN store_booking_table sbt ON urt.table_id = sbt.id AND sbt.delete_flag = 0
+        LEFT JOIN life_user lu ON ur.user_id = lu.id AND lu.delete_flag = 0
+        LEFT JOIN user_reservation_order uro ON ur.id = uro.reservation_id AND uro.delete_flag = 0
+        WHERE
+            ur.delete_flag = 0
+            AND ur.store_id = #{storeId}
+        <if test="status != null">
+            AND ur.status = #{status}
+        </if>
+        <if test="dateFrom != null">
+            AND ur.reservation_date &gt;= #{dateFrom}
+        </if>
+        <if test="dateTo != null">
+            AND ur.reservation_date &lt;= #{dateTo}
+        </if>
+        <if test="orderStatus != null">
+            AND uro.order_status = #{orderStatus}
+        </if>
+        GROUP BY
+            ur.id
+        ORDER BY
+            ur.reservation_date DESC,
+            ur.start_time ASC,
+            ur.created_time DESC
+    </select>
+
+    <delete id="physicalDeleteById">
+        DELETE FROM user_reservation WHERE id = #{id}
+    </delete>
+
+    <!-- 关联查询:订单待使用 + 预约结束时间已过,仅返回 reservation_id -->
+    <select id="listReservationIdsForTimeoutMark" resultType="java.lang.Integer">
+        SELECT DISTINCT r.id
+        FROM user_reservation r
+        INNER JOIN user_reservation_order o ON o.reservation_id = r.id AND o.delete_flag = 0
+        WHERE r.delete_flag = 0
+          AND o.order_status = 1
+          AND r.status IN (0, 1)
+          AND r.end_time IS NOT NULL AND TRIM(r.end_time) != ''
+          AND STR_TO_DATE(CONCAT(DATE(r.reservation_date), ' ', TRIM(r.end_time)), '%Y-%m-%d %H:%i') &lt; NOW()
+    </select>
+
+</mapper>

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

@@ -0,0 +1,73 @@
+<?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="businessSection" property="businessSection"/>
+        <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"/>
+        <result column="verificationCode" property="verificationCode"/>
+        <result column="paymentStatus" property="paymentStatus"/>
+        <result column="outTradeNo" property="outTradeNo"/>
+        <result column="payType" property="payType"/>
+        <result column="merchantCancelReason" property="merchantCancelReason"/>
+    </resultMap>
+
+    <!-- 预订订单列表:店铺名称模糊搜索(限10字)、按状态筛选 -->
+    <select id="selectOrderListPage" resultMap="ReservationOrderListDtoMap">
+        SELECT
+            o.id AS orderId,
+            o.order_sn AS orderSn,
+            o.store_id AS storeId,
+            s.business_section AS businessSection,
+            s.store_name AS storeName,
+            img.img_url AS storeEntranceImageUrl,
+            o.order_status AS orderStatus,
+            o.payment_status AS paymentStatus,
+            o.reservation_id AS reservationId,
+            o.deposit_amount AS depositAmount,
+            o.created_time AS createdTime,
+            o.verification_code AS verificationCode,
+            o.out_trade_no AS outTradeNo,
+            o.payment_method AS payType,
+            r.reason AS merchantCancelReason
+        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
+        LEFT JOIN user_reservation r ON o.reservation_id = r.id AND r.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>

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

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

+ 8 - 0
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -69,4 +69,12 @@ public interface AlienStoreFeign {
     @RequestMapping("payment/refunds")
     public R refunds(@RequestBody Map<String, String> params);
 
+    /**
+     * 预订未到店超时定时任务:将 end_time 已过且订单状态为待使用的预订标记为已过期/未到店超时
+     *
+     * @return R.data 为本次更新的预约数量
+     */
+    @org.springframework.web.bind.annotation.PostMapping("/reservation/job/markTimeout")
+    R<Integer> markReservationTimeoutByEndTime();
+
 }

+ 35 - 0
alien-job/src/main/java/shop/alien/job/store/ReservationTimeoutJob.java

@@ -0,0 +1,35 @@
+package shop.alien.job.store;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.job.feign.AlienStoreFeign;
+
+/**
+ * 预订未到店超时定时任务
+ * 将 user_reservation.end_time 小于当前时间且对应 user_reservation_order 订单状态为「待使用」的预订:
+ * - user_reservation.status 置为 4(未到店超时)
+ * - user_reservation_order.order_status 置为 3(已过期)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ReservationTimeoutJob {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @XxlJob("reservationTimeoutJob")
+    public void reservationTimeoutJob() {
+        log.info("【定时任务】预订未到店超时:开始执行");
+        try {
+            R<Integer> result = alienStoreFeign.markReservationTimeoutByEndTime();
+            int count = (result != null && result.getData() != null) ? result.getData() : 0;
+            log.info("【定时任务】预订未到店超时:执行完成,本次更新条数={}", count);
+        } catch (Exception e) {
+            log.error("【定时任务】预订未到店超时:执行异常", e);
+            throw e;
+        }
+    }
+}

+ 68 - 3
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StoreBusinessServiceImpl.java

@@ -135,6 +135,7 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
     /** 商户证照历史记录数据访问对象 */
     private final StoreLicenseHistoryMapper licenseHistoryMapper;
 
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
 
     @Value("${spring.web.resources.excel-path}")
     private String excelPath;
@@ -194,7 +195,14 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
         //营业时间
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+        //storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+
+        //营业时间(通过 getStoreInfoBusinessHours 方法获取,包含节假日信息)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHoursNew(id);
+        // 设置包含节假日信息的营业时间列表
+        storeMainInfoVo.setStoreBusinessInfoVos(storeBusinessInfoVos);
+
+
         //门店头像
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, id);
         StoreImg storeImg = storeImgMapper.selectOne(eq);
@@ -252,7 +260,14 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
         //营业时间
         List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id));
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoList);
+
+
+        //营业时间(通过 getStoreInfoBusinessHours 方法获取,包含节假日信息)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHoursNew(id);
+        // 设置包含节假日信息的营业时间列表
+        storeMainInfoVo.setStoreBusinessInfoVos(storeBusinessInfoVos);
+
+
         //营业执照
         storeMainInfoVo.setBusinessLicenseList(storeImgMapper.selectList(new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 14).eq(StoreImg::getStoreId, id)));
         //合同照片
@@ -273,6 +288,50 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
         return storeBusinessInfoList;
     }
 
+
+    public List<StoreBusinessInfoVo> getStoreInfoBusinessHoursNew(Integer id) {
+        // 查询营业时间(包含正常时间和特殊时间)
+        List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, id)
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+                        .orderByAsc(StoreBusinessInfo::getBusinessType) // 先按类型排序:1-正常时间,2-特殊时间
+                        .orderByAsc(StoreBusinessInfo::getBusinessDate) // 再按日期排序
+        );
+
+        // 转换为 VO 并关联节假日信息
+        // store_business_info 的 essential_id 关联 essential_holiday_comparison 的 id
+        List<StoreBusinessInfoVo> resultList = new ArrayList<>();
+        for (StoreBusinessInfo businessInfo : storeBusinessInfoList) {
+            StoreBusinessInfoVo vo = new StoreBusinessInfoVo();
+            // 复制基本信息
+            BeanUtils.copyProperties(businessInfo, vo);
+
+            // 如果有关联的节假日ID(essential_id),查询 essential_holiday_comparison 表的节假日信息
+            if (businessInfo.getEssentialId() != null && !businessInfo.getEssentialId().trim().isEmpty()) {
+                Integer essentialId = null;
+                try {
+                    essentialId = Integer.parseInt(businessInfo.getEssentialId().trim());
+                    EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(essentialId);
+                    if (holiday != null) {
+                        vo.setHolidayInfo(holiday);
+                    } else {
+                        log.warn("门店营业时间关联的节假日信息不存在,storeId=" + id + ", essentialId=" + essentialId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("门店营业时间关联的节假日ID格式错误,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId());
+                } catch (Exception e) {
+                    log.error("查询节假日信息失败,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId(), e);
+                }
+            }
+
+            resultList.add(vo);
+        }
+
+        return resultList;
+    }
+
+
     /**
      * 门店信息-修改后展示
      *
@@ -285,7 +344,13 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
         //营业时间
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+        //storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+
+        //营业时间(通过 getStoreInfoBusinessHours 方法获取,包含节假日信息)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHoursNew(id);
+        // 设置包含节假日信息的营业时间列表
+        storeMainInfoVo.setStoreBusinessInfoVos(storeBusinessInfoVos);
+
         return storeMainInfoVo;
     }
 

+ 54 - 6
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StoreManageServiceImpl.java

@@ -13,10 +13,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreInfoDto;
-import shop.alien.entity.store.vo.StoreInfoVo;
-import shop.alien.entity.store.vo.StoreMainInfoVo;
-import shop.alien.entity.store.vo.StoreMenuVo;
-import shop.alien.entity.store.vo.WebSocketVo;
+import shop.alien.entity.store.vo.*;
 import shop.alien.mapper.*;
 import shop.alien.storeplatform.feign.AlienStoreFeign;
 import shop.alien.storeplatform.service.StoreManageService;
@@ -76,6 +73,9 @@ public class StoreManageServiceImpl implements StoreManageService {
 
     private final GaoDeMapApiUtil gaoDeMapApiUtil;
 
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+
+
     /**
      * 默认店铺密码
      */
@@ -570,8 +570,14 @@ public class StoreManageServiceImpl implements StoreManageService {
                 new LambdaQueryWrapper<StoreBusinessInfo>()
                         .eq(StoreBusinessInfo::getStoreId, id)
         );
-        storeMainInfoVo.setStoreBusinessInfo(businessInfoList);
-        
+        //storeMainInfoVo.setStoreBusinessInfo(businessInfoList);
+
+        //营业时间(通过 getStoreInfoBusinessHours 方法获取,包含节假日信息)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHoursNew1(id);
+        // 设置包含节假日信息的营业时间列表
+        storeMainInfoVo.setStoreBusinessInfoVos(storeBusinessInfoVos);
+
+
         // 11. 查询门店头像(imgType=10)
         StoreImg headImg = storeImgMapper.selectOne(
                 new LambdaQueryWrapper<StoreImg>()
@@ -633,6 +639,48 @@ public class StoreManageServiceImpl implements StoreManageService {
         return storeMainInfoVo;
     }
 
+    public List<StoreBusinessInfoVo> getStoreInfoBusinessHoursNew1(Integer id) {
+        // 查询营业时间(包含正常时间和特殊时间)
+        List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, id)
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+                        .orderByAsc(StoreBusinessInfo::getBusinessType) // 先按类型排序:1-正常时间,2-特殊时间
+                        .orderByAsc(StoreBusinessInfo::getBusinessDate) // 再按日期排序
+        );
+
+        // 转换为 VO 并关联节假日信息
+        // store_business_info 的 essential_id 关联 essential_holiday_comparison 的 id
+        List<StoreBusinessInfoVo> resultList = new ArrayList<>();
+        for (StoreBusinessInfo businessInfo : storeBusinessInfoList) {
+            StoreBusinessInfoVo vo = new StoreBusinessInfoVo();
+            // 复制基本信息
+            BeanUtils.copyProperties(businessInfo, vo);
+
+            // 如果有关联的节假日ID(essential_id),查询 essential_holiday_comparison 表的节假日信息
+            if (businessInfo.getEssentialId() != null && !businessInfo.getEssentialId().trim().isEmpty()) {
+                Integer essentialId = null;
+                try {
+                    essentialId = Integer.parseInt(businessInfo.getEssentialId().trim());
+                    EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(essentialId);
+                    if (holiday != null) {
+                        vo.setHolidayInfo(holiday);
+                    } else {
+                        log.warn("门店营业时间关联的节假日信息不存在,storeId=" + id + ", essentialId=" + essentialId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("门店营业时间关联的节假日ID格式错误,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId());
+                } catch (Exception e) {
+                    log.error("查询节假日信息失败,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId(), e);
+                }
+            }
+
+            resultList.add(vo);
+        }
+
+        return resultList;
+    }
+
     @Override
     public String getTodayIncome(Integer storeId) {
         log.info("StoreManageServiceImpl.getTodayIncome - 查询店铺今日收益: storeId={}", storeId);

+ 53 - 1
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StorePlatformInfoServiceImpl.java

@@ -13,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreInfoDto;
+import shop.alien.entity.store.vo.StoreBusinessInfoVo;
 import shop.alien.entity.store.vo.StoreMainInfoVo;
 import shop.alien.entity.store.vo.StoreMenuVo;
 import shop.alien.mapper.*;
@@ -56,6 +57,8 @@ public class StorePlatformInfoServiceImpl extends ServiceImpl<StoreInfoMapper, S
 
     private final GaoDeMapUtil gaoDeMapUtil;
 
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+
     @Override
     public int saveOrUpdateStoreInfo(StoreInfoDto storeInfodto) {
         if (storeInfodto.getId() != null) {
@@ -261,7 +264,13 @@ public class StorePlatformInfoServiceImpl extends ServiceImpl<StoreInfoMapper, S
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
         //营业时间
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+        //storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+
+        //营业时间(通过 getStoreInfoBusinessHours 方法获取,包含节假日信息)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHoursNew(id);
+        // 设置包含节假日信息的营业时间列表
+        storeMainInfoVo.setStoreBusinessInfoVos(storeBusinessInfoVos);
+
         //门店头像
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, id);
         StoreImg storeImg = storeImgMapper.selectOne(eq);
@@ -289,4 +298,47 @@ public class StorePlatformInfoServiceImpl extends ServiceImpl<StoreInfoMapper, S
         return storeMainInfoVo;
     }
 
+
+    public List<StoreBusinessInfoVo> getStoreInfoBusinessHoursNew(Integer id) {
+        // 查询营业时间(包含正常时间和特殊时间)
+        List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, id)
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+                        .orderByAsc(StoreBusinessInfo::getBusinessType) // 先按类型排序:1-正常时间,2-特殊时间
+                        .orderByAsc(StoreBusinessInfo::getBusinessDate) // 再按日期排序
+        );
+
+        // 转换为 VO 并关联节假日信息
+        // store_business_info 的 essential_id 关联 essential_holiday_comparison 的 id
+        List<StoreBusinessInfoVo> resultList = new ArrayList<>();
+        for (StoreBusinessInfo businessInfo : storeBusinessInfoList) {
+            StoreBusinessInfoVo vo = new StoreBusinessInfoVo();
+            // 复制基本信息
+            BeanUtils.copyProperties(businessInfo, vo);
+
+            // 如果有关联的节假日ID(essential_id),查询 essential_holiday_comparison 表的节假日信息
+            if (businessInfo.getEssentialId() != null && !businessInfo.getEssentialId().trim().isEmpty()) {
+                Integer essentialId = null;
+                try {
+                    essentialId = Integer.parseInt(businessInfo.getEssentialId().trim());
+                    EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(essentialId);
+                    if (holiday != null) {
+                        vo.setHolidayInfo(holiday);
+                    } else {
+                        log.warn("门店营业时间关联的节假日信息不存在,storeId=" + id + ", essentialId=" + essentialId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("门店营业时间关联的节假日ID格式错误,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId());
+                } catch (Exception e) {
+                    log.error("查询节假日信息失败,storeId=" + id + ", essentialId=" + businessInfo.getEssentialId(), e);
+                }
+            }
+
+            resultList.add(vo);
+        }
+
+        return resultList;
+    }
+
 }

+ 1 - 1
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StorePlatformRenovationServiceImpl.java

@@ -96,7 +96,7 @@ public class StorePlatformRenovationServiceImpl extends ServiceImpl<StoreInfoMap
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
         //营业时间
         List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id));
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoList);
+        //storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoList);
         //营业执照
         storeMainInfoVo.setBusinessLicenseList(storeImgMapper.selectList(new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 14).eq(StoreImg::getStoreId, id)));
         //合同照片

+ 12 - 0
alien-store/pom.xml

@@ -258,6 +258,18 @@
             <version>1.0.0</version>
         </dependency>
 
+        <!-- 核销码二维码生成(ZXing) -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>3.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.3.0</version>
+        </dependency>
+
         <dependency>
             <groupId>shop.alien</groupId>
             <artifactId>alien-config</artifactId>

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

@@ -123,7 +123,7 @@ public class AiSearchController {
             CompletableFuture<Void> matchedImageFuture = CompletableFuture.runAsync(() -> fillStoreImages(matchedResult, 1));
             CompletableFuture<Void> relatedBusinessHoursFuture = CompletableFuture.runAsync(() -> fillBusinessHours(relatedResult));
             CompletableFuture<Void> matchedBusinessHoursFuture = CompletableFuture.runAsync(() -> fillBusinessHours(matchedResult));
-            
+
             // 等待所有任务完成
             CompletableFuture.allOf(relatedImageFuture, matchedImageFuture, relatedBusinessHoursFuture, matchedBusinessHoursFuture).join();
 

+ 88 - 0
alien-store/src/main/java/shop/alien/store/controller/MerchantPaymentController.java

@@ -0,0 +1,88 @@
+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.MerchantPaymentQueryService;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+
+import java.util.Map;
+
+/**
+ * 商户支付接口(预订订单 user_reservation_order,使用 StorePaymentConfig 按门店配置)
+ * 与 /payment 现有接口隔离,不影响原有支付接口。
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"商户支付(预订订单)"})
+@RestController
+@RequestMapping("/merchantPayment")
+@RequiredArgsConstructor
+public class MerchantPaymentController {
+
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+    private final MerchantPaymentQueryService merchantPaymentQueryService;
+
+    @ApiOperation("预订订单-创建预支付(支付宝,使用门店支付配置)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "orderId", value = "预订订单ID user_reservation_order.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", required = true, 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("MerchantPaymentController.prePay storeId={}, orderId={}, payType={}", storeId, orderId, payType);
+        return merchantPaymentStrategyFactory.getStrategy(payType).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", required = true, paramType = "query", dataType = "String")
+    })
+    @GetMapping("/queryStatus")
+    public R<Object> queryStatus(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("MerchantPaymentController.queryStatus storeId={}, outTradeNo={}", storeId, outTradeNo);
+        return merchantPaymentQueryService.queryPayStatusWithRetry(storeId, outTradeNo, payType);
+    }
+
+    @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 = "refundType", value = "退款类型 0:用户取消 1:商家退款 2:部分退款等 3.扫码核销成功", required = false, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "refundReason", value = "退款原因", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay", required = true, paramType = "query", dataType = "String")
+    })
+    @PostMapping("/refund")
+    public R<String> refund(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam String refundAmount,
+            @RequestParam Integer refundType,
+            @RequestParam(required = false) String refundReason,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("MerchantPaymentController.refund storeId={}, outTradeNo={}", storeId, outTradeNo);
+        return merchantPaymentStrategyFactory.getStrategy(payType).refund(storeId, outTradeNo, refundAmount, refundReason, refundType);
+    }
+}

+ 33 - 0
alien-store/src/main/java/shop/alien/store/controller/ReservationJobController.java

@@ -0,0 +1,33 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.UserReservationService;
+
+/**
+ * 预订相关定时任务回调(供 XXL-JOB 通过 Feign 调用)
+ */
+@Slf4j
+@Api(tags = {"预订定时任务"})
+@RestController
+@RequestMapping("/reservation/job")
+@RequiredArgsConstructor
+public class ReservationJobController {
+
+    private final UserReservationService userReservationService;
+
+    @ApiOperation("标记「结束时间已过且订单待使用」的预订为未到店超时/已过期")
+    @PostMapping("/markTimeout")
+    public R<Integer> markReservationTimeoutByEndTime() {
+        log.info("reservation job: markTimeout 开始");
+        int count = userReservationService.markReservationTimeoutByEndTime();
+        log.info("reservation job: markTimeout 结束,更新条数={}", count);
+        return R.data(count);
+    }
+}

+ 84 - 0
alien-store/src/main/java/shop/alien/store/controller/ReservationOrderPageController.java

@@ -0,0 +1,84 @@
+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查询)、预订订单列表(店铺名称模糊搜索)
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"预订订单页面"})
+@RestController
+@RequestMapping("/store/reservationOrder")
+@RequiredArgsConstructor
+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")
+    @GetMapping("/page")
+    public R<ReservationOrderPageVo> getPage(@RequestParam Integer orderId) {
+        log.info("ReservationOrderPageController.getPage orderId={}", orderId);
+        ReservationOrderPageVo vo = reservationOrderPageService.getPageByOrderId(orderId);
+        if (vo == null) {
+            return R.fail("订单不存在");
+        }
+        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());
+        }
+    }
+}

+ 222 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingBusinessHoursController.java

@@ -0,0 +1,222 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 预订服务营业时间管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"预订服务-营业时间管理"})
+@ApiSort(18)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/business-hours")
+@RequiredArgsConstructor
+public class StoreBookingBusinessHoursController {
+
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询预订服务营业时间列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "settingsId", value = "设置ID(关联store_booking_settings表 id)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "businessType", value = "营业类型(1:正常营业时间, 2:特殊营业时间,可选,不传则查询全部)", dataType = "Integer", paramType = "query", required = false)
+    })
+    @GetMapping("/list")
+    public R<List<StoreBookingBusinessHours>> getBusinessHoursList(
+            @RequestParam Integer settingsId,
+            @RequestParam(required = false) Integer businessType) {
+        log.info("StoreBookingBusinessHoursController.getBusinessHoursList?settingsId={}, businessType={}", settingsId, businessType);
+        
+        if (settingsId == null) {
+            return R.fail("设置ID不能为空");
+        }
+        
+        try {
+            List<StoreBookingBusinessHours> list;
+            if (businessType != null) {
+                list = storeBookingBusinessHoursService.getListBySettingsIdAndType(settingsId, businessType);
+            } else {
+                list = storeBookingBusinessHoursService.getListBySettingsId(settingsId);
+            }
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询预订服务营业时间列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("查询预订服务营业时间详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "营业时间ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreBookingBusinessHours> getBusinessHoursDetail(@RequestParam Integer id) {
+        log.info("StoreBookingBusinessHoursController.getBusinessHoursDetail?id={}", id);
+        
+        if (id == null) {
+            return R.fail("营业时间ID不能为空");
+        }
+        
+        try {
+            StoreBookingBusinessHours businessHours = storeBookingBusinessHoursService.getById(id);
+            if (businessHours == null) {
+                return R.fail("营业时间不存在");
+            }
+            return R.data(businessHours);
+        } catch (Exception e) {
+            log.error("查询预订服务营业时间详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("新增或更新预订服务营业时间")
+    @PostMapping("/save")
+    public R<String> saveBusinessHours(@RequestBody StoreBookingBusinessHoursDTO dto) {
+        log.info("StoreBookingBusinessHoursController.saveBusinessHours?dto={}", dto);
+        
+        // 参数验证
+        if (dto.getSettingsId() == null) {
+            return R.fail("设置ID不能为空");
+        }
+        if (dto.getBusinessType() == null) {
+            return R.fail("营业类型不能为空");
+        }
+        if (dto.getBookingTimeType() == null) {
+            return R.fail("预订时间类型不能为空");
+        }
+        
+        // 如果是特殊营业(节假日),验证节假日类型
+//        if (dto.getBusinessType() != null && dto.getBusinessType() == 1) {
+//            if (!StringUtils.hasText(dto.getHolidayType())) {
+//                return R.fail("特殊营业时必须填写节假日类型");
+//            }
+//        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+        if (dto.getBookingTimeType() != null && dto.getBookingTimeType() == 0) {
+            if (!StringUtils.hasText(dto.getStartTime())) {
+                return R.fail("非全天时必须填写开始时间");
+            }
+            if (!StringUtils.hasText(dto.getEndTime())) {
+                return R.fail("非全天时必须填写结束时间");
+            }
+        }
+        
+        try {
+            StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+            businessHours.setId(dto.getId());
+            businessHours.setSettingsId(dto.getSettingsId());
+            businessHours.setBusinessType(dto.getBusinessType());
+            businessHours.setHolidayType(dto.getHolidayType());
+            businessHours.setHolidayDate(dto.getHolidayDate());
+            businessHours.setBookingTimeType(dto.getBookingTimeType());
+            businessHours.setStartTime(dto.getStartTime());
+            businessHours.setEndTime(dto.getEndTime());
+            businessHours.setSort(dto.getSort());
+            
+            boolean result = storeBookingBusinessHoursService.saveOrUpdateBusinessHours(businessHours);
+            if (result) {
+                return R.success("保存成功");
+            } else {
+                return R.fail("保存失败");
+            }
+        } catch (Exception e) {
+            log.error("保存预订服务营业时间失败", e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("批量保存预订服务营业时间")
+    @PostMapping("/batch-save")
+    public R<String> batchSaveBusinessHours(@RequestBody List<StoreBookingBusinessHoursDTO> dtoList) {
+        log.info("StoreBookingBusinessHoursController.batchSaveBusinessHours?size={}", dtoList != null ? dtoList.size() : 0);
+        
+        if (dtoList == null || dtoList.isEmpty()) {
+            return R.fail("营业时间列表不能为空");
+        }
+        
+        // 验证所有DTO的settingsId是否一致
+        Integer settingsId = dtoList.get(0).getSettingsId();
+        if (settingsId == null) {
+            return R.fail("设置ID不能为空");
+        }
+        for (StoreBookingBusinessHoursDTO dto : dtoList) {
+            if (dto.getSettingsId() == null || !dto.getSettingsId().equals(settingsId)) {
+                return R.fail("所有营业时间的设置ID必须一致");
+            }
+        }
+        
+        try {
+            // 转换为实体对象列表
+            List<StoreBookingBusinessHours> businessHoursList = dtoList.stream()
+                    .map(dto -> {
+                        StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+                        businessHours.setId(dto.getId());
+                        businessHours.setSettingsId(dto.getSettingsId());
+                        businessHours.setBusinessType(dto.getBusinessType());
+                        businessHours.setHolidayType(dto.getHolidayType());
+                        businessHours.setHolidayDate(dto.getHolidayDate());
+                        businessHours.setBookingTimeType(dto.getBookingTimeType());
+                        businessHours.setStartTime(dto.getStartTime());
+                        businessHours.setEndTime(dto.getEndTime());
+                        businessHours.setSort(dto.getSort());
+                        return businessHours;
+                    })
+                    .collect(Collectors.toList());
+            
+            boolean result = storeBookingBusinessHoursService.batchSaveBusinessHours(settingsId, businessHoursList);
+            if (result) {
+                return R.success("批量保存成功");
+            } else {
+                return R.fail("批量保存失败");
+            }
+        } catch (Exception e) {
+            log.error("批量保存预订服务营业时间失败", e);
+            return R.fail("批量保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除预订服务营业时间")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "营业时间ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<String> deleteBusinessHours(@RequestParam Integer id) {
+        log.info("StoreBookingBusinessHoursController.deleteBusinessHours?id={}", id);
+        
+        if (id == null) {
+            return R.fail("营业时间ID不能为空");
+        }
+        
+        try {
+            boolean result = storeBookingBusinessHoursService.deleteBusinessHours(id);
+            if (result) {
+                return R.success("删除成功");
+            } else {
+                return R.fail("删除失败");
+            }
+        } catch (Exception e) {
+            log.error("删除预订服务营业时间失败", e);
+            return R.fail("删除失败:" + e.getMessage());
+        }
+    }
+}

+ 227 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingCategoryController.java

@@ -0,0 +1,227 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.dto.StoreBookingCategoryDTO;
+import shop.alien.entity.store.dto.StoreBookingCategorySortDTO;
+import shop.alien.store.service.StoreBookingCategoryService;
+
+import java.util.List;
+
+/**
+ * 预订服务分类管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"预订服务-分类管理"})
+@ApiSort(15)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/category")
+@RequiredArgsConstructor
+public class StoreBookingCategoryController {
+
+    private final StoreBookingCategoryService storeBookingCategoryService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询预订服务分类列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/list")
+    public R<List<StoreBookingCategory>> getCategoryList(@RequestParam Integer storeId) {
+        log.info("StoreBookingCategoryController.getCategoryList?storeId={}", storeId);
+        
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        
+        try {
+            List<StoreBookingCategory> list = storeBookingCategoryService.getCategoryList(storeId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询预订服务分类列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("查询预订服务分类详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "分类ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreBookingCategory> getCategoryDetail(@RequestParam Integer id) {
+        log.info("StoreBookingCategoryController.getCategoryDetail?id={}", id);
+        
+        if (id == null) {
+            return R.fail("分类ID不能为空");
+        }
+        
+        try {
+            StoreBookingCategory category = storeBookingCategoryService.getById(id);
+            if (category == null) {
+                return R.fail("分类不存在");
+            }
+            return R.data(category);
+        } catch (Exception e) {
+            log.error("查询预订服务分类详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("新增预订服务分类")
+    @PostMapping("/add")
+    public R<StoreBookingCategory> addCategory(@RequestBody StoreBookingCategoryDTO dto) {
+        log.info("StoreBookingCategoryController.addCategory?dto={}", dto);
+        try {
+            StoreBookingCategory category = new StoreBookingCategory();
+            BeanUtils.copyProperties(dto, category);
+            storeBookingCategoryService.addCategory(category);
+        } catch (Exception e) {
+            log.error("新增预订服务分类失败", e);
+            // 如果是名称已存在的错误,直接返回友好提示
+            return R.fail("新增失败:" + e.getMessage());
+        }
+        return R.success("新增成功");
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("编辑预订服务分类")
+    @PostMapping("/update")
+    public R<String> updateCategory(@RequestBody StoreBookingCategoryDTO dto) {
+        log.info("StoreBookingCategoryController.updateCategory?dto={}", dto);
+        
+        if (dto.getId() == null) {
+            return R.fail("分类ID不能为空");
+        }
+        
+        try {
+            StoreBookingCategory category = new StoreBookingCategory();
+            category.setId(dto.getId());
+            if (StringUtils.hasText(dto.getCategoryName())) {
+                category.setCategoryName(dto.getCategoryName());
+            }
+            if (StringUtils.hasText(dto.getFloorPlanImages())) {
+                category.setFloorPlanImages(dto.getFloorPlanImages());
+            }
+            if (dto.getIsDisplay() != null) {
+                category.setIsDisplay(dto.getIsDisplay());
+            }
+            if (dto.getMaxBookingTime() != null) {
+                category.setMaxBookingTime(dto.getMaxBookingTime());
+            }
+            if (dto.getSort() != null) {
+                category.setSort(dto.getSort());
+            }
+            
+            boolean result = storeBookingCategoryService.updateCategory(category);
+            if (result) {
+                return R.success("更新成功");
+            } else {
+                return R.fail("更新失败");
+            }
+        } catch (Exception e) {
+            log.error("更新预订服务分类失败", e);
+            // 如果是名称已存在的错误,直接返回友好提示
+            if (e.getMessage() != null && e.getMessage().contains("此名称已存在")) {
+                return R.fail("此名称已存在");
+            }
+            return R.fail("更新失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除预订服务分类")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "分类ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<String> deleteCategory(@RequestParam Integer id) {
+        log.info("StoreBookingCategoryController.deleteCategory?id={}", id);
+        
+        if (id == null) {
+            return R.fail("分类ID不能为空");
+        }
+        
+        try {
+            boolean result = storeBookingCategoryService.deleteCategory(id);
+            if (result) {
+                return R.success("删除成功");
+            } else {
+                return R.fail("删除失败");
+            }
+        } catch (Exception e) {
+            log.error("删除预订服务分类失败", e);
+            return R.fail("删除失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("更新预订服务分类排序")
+    @PostMapping("/updateSort")
+    public R<String> updateCategorySort(@RequestBody StoreBookingCategorySortDTO dto) {
+        log.info("StoreBookingCategoryController.updateCategorySort?dto={}", dto);
+        
+        if (dto.getStoreId() == null || dto.getCategoryIds() == null || dto.getCategoryIds().isEmpty()) {
+            return R.fail("门店ID和分类ID列表不能为空");
+        }
+        
+        try {
+            boolean result = storeBookingCategoryService.updateCategorySort(dto.getStoreId(), dto.getCategoryIds());
+            if (result) {
+                return R.success("更新排序成功");
+            } else {
+                return R.fail("更新排序失败");
+            }
+        } catch (Exception e) {
+            log.error("更新预订服务分类排序失败", e);
+            return R.fail("更新排序失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation("设置预订服务分类显示状态")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "分类ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "isDisplay", value = "显示状态(0:隐藏, 1:显示)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/updateDisplayStatus")
+    public R<String> updateDisplayStatus(@RequestParam Integer id, @RequestParam Integer isDisplay) {
+        log.info("StoreBookingCategoryController.updateDisplayStatus?id={}&isDisplay={}", id, isDisplay);
+        
+        if (id == null) {
+            return R.fail("分类ID不能为空");
+        }
+        
+        if (isDisplay == null) {
+            return R.fail("显示状态不能为空");
+        }
+        
+        if (isDisplay != 0 && isDisplay != 1) {
+            return R.fail("显示状态值无效,必须为0(隐藏)或1(显示)");
+        }
+        
+        try {
+            boolean result = storeBookingCategoryService.updateDisplayStatus(id, isDisplay);
+            if (result) {
+                String message = isDisplay == 1 ? "设置显示成功" : "设置隐藏成功";
+                return R.success(message);
+            } else {
+                return R.fail("更新显示状态失败");
+            }
+        } catch (Exception e) {
+            log.error("更新预订服务分类显示状态失败", e);
+            return R.fail("更新显示状态失败:" + e.getMessage());
+        }
+    }
+}

+ 161 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingSettingsController.java

@@ -0,0 +1,161 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
+import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
+import shop.alien.store.service.StoreBookingSettingsService;
+
+/**
+ * 预订服务信息设置 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"预订服务-信息设置"})
+@ApiSort(17)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/settings")
+@RequiredArgsConstructor
+public class StoreBookingSettingsController {
+
+    private final StoreBookingSettingsService storeBookingSettingsService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询预订服务信息设置(包含营业时间)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreBookingSettingsDTO> getSettings(@RequestParam Integer storeId) {
+        log.info("StoreBookingSettingsController.getSettings?storeId={}", storeId);
+        
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        
+        try {
+            StoreBookingSettingsDTO dto = storeBookingSettingsService.getSettingsWithBusinessHoursByStoreId(storeId);
+            if (dto == null) {
+                return R.fail("未找到该门店的预订服务信息设置");
+            }
+            return R.data(dto);
+        } catch (Exception e) {
+            log.error("查询预订服务信息设置失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("保存预订服务信息设置(新增或更新,包含营业时间)")
+    @PostMapping("/save")
+    public R<String> saveSettings(@RequestBody StoreBookingSettingsDTO dto) {
+        log.info("StoreBookingSettingsController.saveSettings?dto={}", dto);
+        
+        // 参数验证
+        if (dto.getStoreId() == null) {
+            return R.fail("门店ID不能为空");
+        }
+        if (dto.getRetainPositionFlag() == null) {
+            return R.fail("未按时到店是否保留位置不能为空");
+        }
+        if (dto.getRetentionDuration() == null || dto.getRetentionDuration() <= 0) {
+            return R.fail("保留时长必须大于0");
+        }
+        if (dto.getBookingDateDisplayDays() == null || dto.getBookingDateDisplayDays() <= 0) {
+            return R.fail("预订日期显示天数必须大于0");
+        }
+        if (dto.getMaxCapacityPerSlot() == null || dto.getMaxCapacityPerSlot() <= 0) {
+            return R.fail("单时段最大容纳人数必须大于0");
+        }
+        
+        // 验证预订相关字段
+        // 如果选择付费(reservation=1),必须填写预订金额
+        if (StringUtils.hasText(dto.getReservation()) && "1".equals(dto.getReservation())) {
+            if (dto.getReservationMoney() == null || dto.getReservationMoney() < 0) {
+                return R.fail("选择付费时必须填写预订金额,且金额不能小于0");
+            }
+        }
+        
+        // 验证取消预订退费时长设置
+        if (dto.getOffUnsubscribeHours() == null || dto.getOffUnsubscribeHours() < 0) {
+            return R.fail("取消预订退费时长设置必须大于等于0");
+        }
+        
+        // 验证营业时间结束前不可预订时间
+        if (dto.getBookingNotAvailableTime() == null || dto.getBookingNotAvailableTime() < 0) {
+            return R.fail("营业时间结束前不可预订时间必须大于等于0");
+        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+//        if (dto.getBookingTimeType() != null && dto.getBookingTimeType() == 0) {
+//            if (!StringUtils.hasText(dto.getBookingStartTime())) {
+//                return R.fail("非全天时必须填写开始时间");
+//            }
+//            if (!StringUtils.hasText(dto.getBookingEndTime())) {
+//                return R.fail("非全天时必须填写结束时间");
+//            }
+//        }
+        
+        // 验证正常营业时间
+        if (dto.getNormalBusinessHours() != null) {
+            StoreBookingBusinessHoursDTO normalHours = dto.getNormalBusinessHours();
+            if (normalHours.getBookingTimeType() == null) {
+                return R.fail("正常营业时间的预订时间类型不能为空");
+            }
+            if (normalHours.getBookingTimeType() == 0) {
+                if (!StringUtils.hasText(normalHours.getStartTime())) {
+                    return R.fail("正常营业时间非全天时必须填写开始时间");
+                }
+                if (!StringUtils.hasText(normalHours.getEndTime())) {
+                    return R.fail("正常营业时间非全天时必须填写结束时间");
+                }
+            }
+        }
+        
+        // 验证特殊营业时间列表
+        if (dto.getSpecialBusinessHoursList() != null && !dto.getSpecialBusinessHoursList().isEmpty()) {
+            for (StoreBookingBusinessHoursDTO specialHours : dto.getSpecialBusinessHoursList()) {
+                if (specialHours.getBookingTimeType() == null) {
+                    return R.fail("特殊营业时间的预订时间类型不能为空");
+                }
+                if (specialHours.getBookingTimeType() == 0) {
+                    if (!StringUtils.hasText(specialHours.getStartTime())) {
+                        return R.fail("特殊营业时间非全天时必须填写开始时间");
+                    }
+                    if (!StringUtils.hasText(specialHours.getEndTime())) {
+                        return R.fail("特殊营业时间非全天时必须填写结束时间");
+                    }
+                }
+            }
+        }
+        
+        // 编辑校验:如果是编辑操作,检查正常营业时间的id是否有值
+        // 注意:特殊营业时间列表中的项可以部分有id(编辑)部分没有id(新增)
+        if (dto.getId() != null) {
+            // 检查正常营业时间:编辑模式下,如果提供了正常营业时间,必须提供id
+            if (dto.getNormalBusinessHours() != null && dto.getNormalBusinessHours().getId() == null) {
+                return R.fail("编辑模式下,正常营业时间必须提供id");
+            }
+        }
+        
+        try {
+            // 使用新的方法保存设置和营业时间
+            boolean result = storeBookingSettingsService.saveSettingsWithBusinessHours(dto);
+            if (result) {
+                return R.success("保存成功");
+            } else {
+                return R.fail("保存失败");
+            }
+        } catch (Exception e) {
+            log.error("保存预订服务信息设置失败", e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+}

+ 194 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingTableController.java

@@ -0,0 +1,194 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.dto.StoreBookingTableBatchDTO;
+import shop.alien.entity.store.dto.StoreBookingTableDTO;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+import shop.alien.store.service.StoreBookingTableService;
+
+import java.util.List;
+
+/**
+ * 预订服务桌号管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"预订服务-桌号管理"})
+@ApiSort(16)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/table")
+@RequiredArgsConstructor
+public class StoreBookingTableController {
+
+    private final StoreBookingTableService storeBookingTableService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询预订服务桌号列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "categoryId", value = "分类ID(可选,不传则查询全部)", dataType = "Integer", paramType = "query", required = false)
+    })
+    @GetMapping("/list")
+    public R<List<StoreBookingTableVo>> getTableList(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer categoryId) {
+        log.info("StoreBookingTableController.getTableList?storeId={}, categoryId={}", storeId, categoryId);
+        
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+        
+        try {
+            List<StoreBookingTableVo> list = storeBookingTableService.getTableListWithCategoryName(storeId, categoryId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询预订服务桌号列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("查询预订服务桌号详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "桌号ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreBookingTable> getTableDetail(@RequestParam Integer id) {
+        log.info("StoreBookingTableController.getTableDetail?id={}", id);
+        
+        if (id == null) {
+            return R.fail("桌号ID不能为空");
+        }
+        
+        try {
+            StoreBookingTable table = storeBookingTableService.getById(id);
+            if (table == null) {
+                return R.fail("桌号不存在");
+            }
+            return R.data(table);
+        } catch (Exception e) {
+            log.error("查询预订服务桌号详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("新增预订服务桌号(支持批量添加)")
+    @PostMapping("/add")
+    public R<String> addTable(@RequestBody StoreBookingTableBatchDTO dto) {
+        log.info("StoreBookingTableController.addTable?dto={}", dto);
+        
+        // 参数验证
+        if (dto.getStoreId() == null) {
+            return R.fail("门店ID不能为空");
+        }
+        if (dto.getCategoryId() == null) {
+            return R.fail("分类ID不能为空");
+        }
+        if (dto.getTables() == null || dto.getTables().isEmpty()) {
+            return R.fail("桌号列表不能为空");
+        }
+        
+        try {
+            // 转换为 StoreBookingTable 列表
+            List<StoreBookingTable> tables = dto.getTables().stream()
+                    .map(item -> {
+                        StoreBookingTable table = new StoreBookingTable();
+                        table.setTableNumber(item.getTableNumber());
+                        table.setSeatingCapacity(item.getSeatingCapacity());
+                        return table;
+                    })
+                    .collect(java.util.stream.Collectors.toList());
+            
+            boolean result = storeBookingTableService.batchAddTables(dto.getStoreId(), dto.getCategoryId(), tables);
+            if (result) {
+                return R.success("新增成功");
+            } else {
+                return R.fail("新增失败");
+            }
+        } catch (Exception e) {
+            log.error("新增预订服务桌号失败", e);
+            // 如果是桌号已存在的错误,直接返回友好提示
+            if (e.getMessage() != null && (e.getMessage().contains("该桌号已存在不能添加") || 
+                e.getMessage().contains("以下桌号已存在不能添加"))) {
+                return R.fail(e.getMessage());
+            }
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("编辑预订服务桌号")
+    @PostMapping("/update")
+    public R<String> updateTable(@RequestBody StoreBookingTableDTO dto) {
+        log.info("StoreBookingTableController.updateTable?dto={}", dto);
+        
+        if (dto.getId() == null) {
+            return R.fail("桌号ID不能为空");
+        }
+        
+        try {
+            StoreBookingTable table = new StoreBookingTable();
+            table.setId(dto.getId());
+            if (dto.getCategoryId() != null) {
+                table.setCategoryId(dto.getCategoryId());
+            }
+            if (StringUtils.hasText(dto.getTableNumber())) {
+                table.setTableNumber(dto.getTableNumber());
+            }
+            if (dto.getSeatingCapacity() != null) {
+                table.setSeatingCapacity(dto.getSeatingCapacity());
+            }
+            
+            boolean result = storeBookingTableService.updateTable(table);
+            if (result) {
+                return R.success("更新成功");
+            } else {
+                return R.fail("更新失败");
+            }
+        } catch (Exception e) {
+            log.error("更新预订服务桌号失败", e);
+            // 如果是桌号已存在的错误,直接返回友好提示
+            if (e.getMessage() != null && e.getMessage().contains("该桌号已存在不能编辑")) {
+                return R.fail("该桌号已存在不能编辑");
+            }
+            return R.fail("更新失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除预订服务桌号")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "桌号ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<String> deleteTable(@RequestParam Integer id) {
+        log.info("StoreBookingTableController.deleteTable?id={}", id);
+        
+        if (id == null) {
+            return R.fail("桌号ID不能为空");
+        }
+        
+        try {
+            boolean result = storeBookingTableService.deleteTable(id);
+            if (result) {
+                return R.success("删除成功");
+            } else {
+                return R.fail("删除失败");
+            }
+        } catch (Exception e) {
+            log.error("删除预订服务桌号失败", e);
+            return R.fail("删除失败:" + e.getMessage());
+        }
+    }
+
+}

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

@@ -1811,7 +1811,7 @@ public class StoreInfoController {
             return R.fail("查询失败: " + e.getMessage());
         }
     }
-    
+
     /**
      * 根据经营板块返回店铺详情页可展示的标签(仅当对应接口有数据时才展示)
      * <p>

+ 235 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreReservationController.java

@@ -0,0 +1,235 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+import shop.alien.store.service.StoreReservationService;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商家端预约信息管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"商家端-预约信息管理"})
+@ApiSort(16)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/reservation")
+@RequiredArgsConstructor
+public class StoreReservationController {
+
+    private final StoreReservationService storeReservationService;
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询商家端预约信息列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "status", value = "预约状态(可选,0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "dateFrom", value = "预约日期起 yyyy-MM-dd", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "dateTo", value = "预约日期止 yyyy-MM-dd", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "orderStatus", value = "订单状态(可选,0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订;不传则默认查询:待使用、已完成、已退款三种状态)", dataType = "Integer", paramType = "query", required = false)
+    })
+    @GetMapping("/list")
+    public R<List<StoreReservationListVo>> getReservationList(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateFrom,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateTo,
+            @RequestParam(required = false) Integer orderStatus) {
+        log.info("StoreReservationController.getReservationList?storeId={}, status={}, dateFrom={}, dateTo={}, orderStatus={}",
+                storeId, status, dateFrom, dateTo, orderStatus);
+
+        if (storeId == null) {
+            return R.fail("门店ID不能为空");
+        }
+
+        try {
+            List<StoreReservationListVo> list = storeReservationService.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询商家端预约信息列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("商家端取消预约")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationId", value = "预约ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "cancelReason", value = "取消原因(限30字)", dataType = "String", paramType = "query", required = false)
+    })
+    @PostMapping("/cancel")
+    public R<String> cancelReservation(
+            @RequestParam Integer reservationId,
+            @RequestParam(required = false) String cancelReason) {
+        log.info("StoreReservationController.cancelReservation?reservationId={}&cancelReason={}", reservationId, cancelReason);
+
+        if (reservationId == null) {
+            return R.fail("预约ID不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.cancelReservationByStore(reservationId, cancelReason);
+            if (result) {
+                return R.success("取消预约成功");
+            } else {
+                return R.fail("取消预约失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端取消预约失败", e);
+            return R.fail("取消预约失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("商家端删除预订信息")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationId", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<String> deleteReservation(@RequestParam Integer reservationId) {
+        log.info("StoreReservationController.deleteReservation?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            return R.fail("预约ID不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.deleteReservationByStore(reservationId);
+            if (result) {
+                return R.success("删除预订信息成功");
+            } else {
+                return R.fail("删除预订信息失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端删除预订信息失败", e);
+            return R.fail("删除预订信息失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("商家端加时")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationId", value = "预约ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "addTimeStart", value = "加时开始时间(yyyy-MM-dd HH:mm:ss格式,如:2026-03-11 13:00:00)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "addTimeMinutes", value = "加时分钟数(必须大于0)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/addTime")
+    public R<String> addTime(
+            @RequestParam Integer reservationId,
+            @RequestParam String addTimeStart,
+            @RequestParam Integer addTimeMinutes) {
+        log.info("StoreReservationController.addTime?reservationId={}, addTimeStart={}, addTimeMinutes={}", 
+                reservationId, addTimeStart, addTimeMinutes);
+
+        if (reservationId == null) {
+            return R.fail("预约ID不能为空");
+        }
+
+        if (addTimeStart == null || addTimeStart.trim().isEmpty()) {
+            return R.fail("加时开始时间不能为空");
+        }
+
+        if (addTimeMinutes == null || addTimeMinutes <= 0) {
+            return R.fail("加时分钟数必须大于0");
+        }
+
+        try {
+            boolean result = storeReservationService.addTimeByStore(reservationId, addTimeStart, addTimeMinutes);
+            if (result) {
+                return R.success("加时成功");
+            } else {
+                return R.fail("加时失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端加时失败", e);
+            return R.fail("加时失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("商家端核销预约订单")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "verificationCode", value = "核销码/券码(user_reservation_order.verification_code)", dataType = "String", paramType = "query", required = true)
+    })
+    @PostMapping("/verify")
+    public R<String> verifyReservation(@RequestParam String verificationCode) {
+        log.info("StoreReservationController.verifyReservation?verificationCode={}", verificationCode);
+
+        if (verificationCode == null || verificationCode.trim().isEmpty()) {
+            return R.fail("核销码不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.verifyReservationByCode(verificationCode);
+            if (result) {
+                return R.success("核销成功");
+            } else {
+                return R.fail("核销失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端核销预约订单失败", e);
+            return R.fail("核销失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("商家端退款(预留)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reservationId", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/refund")
+    public R<String> refundReservation(@RequestParam Integer reservationId) {
+        log.info("StoreReservationController.refundReservation?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            return R.fail("预约ID不能为空");
+        }
+
+        try {
+            boolean result = storeReservationService.refundReservation(reservationId);
+            if (result) {
+                return R.success("退款成功");
+            } else {
+                return R.fail("退款失败");
+            }
+        } catch (Exception e) {
+            log.error("商家端退款失败", e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation("通过预订设置ID查询线上预订营业时间")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "settingsId", value = "预订设置ID(store_booking_settings表的id)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getBookingBusinessHours")
+    public R<List<StoreBookingBusinessHours>> getBookingBusinessHours(@RequestParam Integer settingsId) {
+        log.info("StoreReservationController.getBookingBusinessHours?settingsId={}", settingsId);
+
+        if (settingsId == null) {
+            return R.fail("预订设置ID不能为空");
+        }
+
+        try {
+            List<StoreBookingBusinessHours> list = storeBookingBusinessHoursService.getListBySettingsId(settingsId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询线上预订营业时间失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,251 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.store.service.UserReservationService;
+import 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;
+
+/**
+ * 用户预约 前端控制器
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"用户预约"})
+@CrossOrigin
+@RestController
+@RequestMapping("/user/reservation")
+@RequiredArgsConstructor
+public class UserReservationController {
+
+    private final UserReservationService userReservationService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
+
+    @ApiOperation("新增预约")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/add")
+    public R<Integer> add(@RequestBody UserReservationDTO dto) {
+        log.info("UserReservationController.add?userId={}, storeId={}", dto.getUserId(), dto.getStoreId());
+        try {
+            Integer id = userReservationService.add(dto);
+            return R.data(id);
+        } catch (Exception e) {
+            log.error("新增预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改预约")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/update")
+    public R<String> update(@RequestBody UserReservationDTO dto) {
+        log.info("UserReservationController.update?id={}", dto.getId());
+        if (dto.getId() == null) {
+            return R.fail("预约ID不能为空");
+        }
+        try {
+            boolean ok = userReservationService.updateReservation(dto);
+            return ok ? R.success("修改成功") : R.fail("修改失败");
+        } catch (Exception e) {
+            log.error("修改预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除预约")
+    @ApiOperationSupport(order = 3)
+    @ApiImplicitParam(name = "id", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    @PostMapping("/delete")
+    public R<String> delete(@RequestParam Integer id) {
+        log.info("UserReservationController.delete?id={}", id);
+        if (id == null) {
+            return R.fail("预约ID不能为空");
+        }
+        try {
+            boolean ok = userReservationService.removeReservation(id);
+            return ok ? R.success("删除成功") : R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("删除预约失败", e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("预约详情")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParam(name = "id", value = "预约ID", dataType = "Integer", paramType = "query", required = true)
+    @GetMapping("/detail")
+    public R<UserReservationVo> detail(@RequestParam Integer id) {
+        log.info("UserReservationController.detail?id={}", id);
+        if (id == null) {
+            return R.fail("预约ID不能为空");
+        }
+        UserReservationVo vo = userReservationService.getDetail(id);
+        if (vo == null) {
+            return R.fail("预约不存在");
+        }
+        return R.data(vo);
+    }
+
+    @ApiOperation("分页查询预约列表")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "预约状态", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "dateFrom", value = "预约日期起 yyyy-MM-dd", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "dateTo", value = "预约日期止 yyyy-MM-dd", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数", dataType = "Integer", paramType = "query", defaultValue = "10")
+    })
+    @GetMapping("/page")
+    public R<IPage<UserReservationVo>> page(
+            @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateFrom,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateTo,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        log.info("UserReservationController.page?userId={}, storeId={}, status={}, pageNum={}, pageSize={}",
+                userId, storeId, status, pageNum, pageSize);
+        IPage<UserReservationVo> page = userReservationService.pageList(userId, storeId, status, dateFrom, dateTo, pageNum, pageSize);
+        return R.data(page);
+    }
+
+    @ApiOperation("预约列表(不分页)")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "预约状态", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/list")
+    public R<List<UserReservationVo>> list(
+            @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer status) {
+        log.info("UserReservationController.list?userId={}, storeId={}, status={}", userId, storeId, status);
+        List<UserReservationVo> list = userReservationService.list(userId, storeId, status);
+        return R.data(list);
+    }
+
+    @ApiOperation("预订信息")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getBookingsByStoreId")
+    public R<Map<String, Object>> getBookingsByStoreId(@RequestParam Integer storeId) {
+        log.info("UserReservationController.getBookingsByStoreId?storeId={}", storeId);
+        Map<String, Object> list = userReservationService.getBookingsByStoreId(storeId);
+        return R.data(list);
+    }
+
+    /**
+     * 获取定桌情况。不传预定日期时:从今天起找首个存在未约满选座的日期并返回该日数据;
+     * 传入预定日期时:直接查询该日期的定桌情况(该日预约列表 + 每个选座约满标识 full)。
+     */
+    @ApiOperation("获取首个未约满日期的预约数据 / 按预定日期查询指定日期的定桌情况")
+    @ApiOperationSupport(order = 8)
+    @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,
+            @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, 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查询详情(门店+预订+定桌,门店信息含 distance、distance2、subwayName、storeLocation)")
+    @ApiOperationSupport(order = 11)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "jingdu", value = "经度(可选,与 weidu 同时传入时返回用户到门店距离)", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "weidu", value = "纬度(可选)", dataType = "String", paramType = "query")
+    })
+    @GetMapping("/orderDetail")
+    public R<ReservationOrderDetailVo> orderDetail(@RequestParam Integer orderId,
+                                                    @RequestParam(required = false) String jingdu,
+                                                    @RequestParam(required = false) String weidu) {
+        log.info("UserReservationController.orderDetail?orderId={},jingdu={},weidu={}", orderId, jingdu, weidu);
+        if (orderId == null) {
+            return R.fail("订单ID不能为空");
+        }
+        ReservationOrderDetailVo vo = userReservationService.getOrderDetailByOrderId(orderId, jingdu, weidu);
+        if (vo == null) {
+            return R.fail("订单不存在");
+        }
+        return R.data(vo);
+    }
+}

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

@@ -0,0 +1,90 @@
+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.MerchantPaymentQueryService;
+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;
+    private final MerchantPaymentQueryService merchantPaymentQueryService;
+
+    @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);
+        return merchantPaymentQueryService.queryPayStatusWithRetry(storeId, outTradeNo, payType);
+    }
+
+    @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 = "refundType", value = "退款类型 0:用户取消 1:商家退款 2:部分退款等 3.扫码核销成功", required = false, paramType = "query", dataType = "int"),
+            @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 Integer refundType,
+            @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, refundType);
+    }
+}

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

@@ -0,0 +1,62 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.MerchantPaymentOrder;
+
+import java.util.List;
+
+/**
+ * 商户支付单表 服务类
+ *
+ * @author system
+ */
+public interface MerchantPaymentOrderService extends IService<MerchantPaymentOrder> {
+
+    /**
+     * 根据商户订单号查询支付单
+     *
+     * @param outTradeNo 商户订单号
+     * @return 支付单,不存在返回 null
+     */
+    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 支付单号
+     */
+    String generatePaymentNo();
+
+    /**
+     * 按订单ID将支付单逻辑删除(delete_flag=1)
+     *
+     * @param orderId 业务订单ID user_reservation_order.id
+     * @return 更新的条数
+     */
+    int logicDeleteByOrderId(Integer orderId);
+
+    /**
+     * 按订单ID与支付类型将支付单逻辑删除(未命中缓存重新预支付时,仅删除同 pay_type 的支付单)
+     *
+     * @param orderId 业务订单ID
+     * @param payType 支付类型,如 alipay、wechat
+     * @return 更新的条数
+     */
+    int logicDeleteByOrderIdAndPayType(Integer orderId, String payType);
+}

+ 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);
+}

+ 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);
+//            }
+//        }
+    }
+}

+ 15 - 0
alien-store/src/main/java/shop/alien/store/service/RefundRecordAsyncService.java

@@ -0,0 +1,15 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.RefundRecord;
+
+/**
+ * 退款相关异步落库:更新支付单 + 保存退款记录,避免阻塞退款接口响应
+ */
+public interface RefundRecordAsyncService {
+
+    /**
+     * 异步执行:先更新支付单状态,再保存退款记录(顺序保证)
+     */
+    void completeRefundAsync(MerchantPaymentOrder paymentOrder, RefundRecord record);
+}

+ 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);
+}

+ 19 - 0
alien-store/src/main/java/shop/alien/store/service/ReservationOrderPageService.java

@@ -0,0 +1,19 @@
+package shop.alien.store.service;
+
+import shop.alien.store.vo.ReservationOrderPageVo;
+
+/**
+ * 预订订单页面展示服务(按订单ID查询)
+ *
+ * @author system
+ */
+public interface ReservationOrderPageService {
+
+    /**
+     * 按订单ID查询预订页面展示数据
+     *
+     * @param orderId 预订订单ID(user_reservation_order.id)
+     * @return 页面 VO,订单不存在返回 null
+     */
+    ReservationOrderPageVo getPageByOrderId(Integer orderId);
+}

+ 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);
+}

+ 58 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingBusinessHoursService.java

@@ -0,0 +1,58 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+
+import java.util.List;
+
+/**
+ * 预订服务营业时间表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingBusinessHoursService extends IService<StoreBookingBusinessHours> {
+
+
+    /**
+     * 根据设置ID查询营业时间列表
+     *
+     * @param settingsId 设置ID
+     * @return 营业时间列表
+     */
+    List<StoreBookingBusinessHours> getListBySettingsId(Integer settingsId);
+
+    /**
+     * 根据设置ID和营业类型查询营业时间列表
+     *
+     * @param settingsId 设置ID
+     * @param businessType 营业类型(1:正常营业时间, 2:特殊营业时间)
+     * @return 营业时间列表
+     */
+    List<StoreBookingBusinessHours> getListBySettingsIdAndType(Integer settingsId, Integer businessType);
+
+    /**
+     * 新增或更新营业时间
+     *
+     * @param businessHours 营业时间对象
+     * @return boolean
+     */
+    boolean saveOrUpdateBusinessHours(StoreBookingBusinessHours businessHours);
+
+    /**
+     * 批量保存营业时间
+     *
+     * @param settingsId 设置ID
+     * @param businessHoursList 营业时间列表
+     * @return boolean
+     */
+    boolean batchSaveBusinessHours(Integer settingsId, List<StoreBookingBusinessHours> businessHoursList);
+
+    /**
+     * 删除营业时间(逻辑删除)
+     *
+     * @param id 营业时间ID
+     * @return boolean
+     */
+    boolean deleteBusinessHours(Integer id);
+}

+ 65 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingCategoryService.java

@@ -0,0 +1,65 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreBookingCategory;
+
+import java.util.List;
+
+/**
+ * 预订服务分类表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingCategoryService extends IService<StoreBookingCategory> {
+
+    /**
+     * 查询预订服务分类列表(按排序字段排序)
+     *
+     * @param storeId 门店ID
+     * @return List<StoreBookingCategory>
+     */
+    List<StoreBookingCategory> getCategoryList(Integer storeId);
+
+    /**
+     * 新增预订服务分类
+     *
+     * @param category 分类对象
+     * @return boolean
+     */
+    boolean addCategory(StoreBookingCategory category);
+
+    /**
+     * 更新预订服务分类
+     *
+     * @param category 分类对象
+     * @return boolean
+     */
+    boolean updateCategory(StoreBookingCategory category);
+
+    /**
+     * 删除预订服务分类
+     *
+     * @param id 分类ID
+     * @return boolean
+     */
+    boolean deleteCategory(Integer id);
+
+    /**
+     * 更新预订服务分类排序
+     *
+     * @param storeId     门店ID
+     * @param categoryIds 分类ID列表(按顺序排列)
+     * @return boolean
+     */
+    boolean updateCategorySort(Integer storeId, List<Integer> categoryIds);
+
+    /**
+     * 更新预订服务分类显示状态
+     *
+     * @param id        分类ID
+     * @param isDisplay 显示状态(0:隐藏, 1:显示)
+     * @return boolean
+     */
+    boolean updateDisplayStatus(Integer id, Integer isDisplay);
+}

+ 46 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingSettingsService.java

@@ -0,0 +1,46 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
+
+/**
+ * 预订服务信息设置表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingSettingsService extends IService<StoreBookingSettings> {
+
+    /**
+     * 根据门店ID查询预订服务信息设置
+     *
+     * @param storeId 门店ID
+     * @return StoreBookingSettings
+     */
+    StoreBookingSettings getByStoreId(Integer storeId);
+
+    /**
+     * 新增或更新预订服务信息设置
+     *
+     * @param settings 设置对象
+     * @return boolean
+     */
+    boolean saveOrUpdateSettings(StoreBookingSettings settings);
+
+    /**
+     * 保存预订服务信息设置(包含营业时间)
+     *
+     * @param dto 设置DTO(包含营业时间信息)
+     * @return boolean
+     */
+    boolean saveSettingsWithBusinessHours(StoreBookingSettingsDTO dto);
+
+    /**
+     * 根据门店ID查询预订服务信息设置(包含营业时间)
+     *
+     * @param storeId 门店ID
+     * @return StoreBookingSettingsDTO
+     */
+    StoreBookingSettingsDTO getSettingsWithBusinessHoursByStoreId(Integer storeId);
+}

+ 68 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingTableService.java

@@ -0,0 +1,68 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+
+import java.util.List;
+
+/**
+ * 预订服务桌号表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingTableService extends IService<StoreBookingTable> {
+
+    /**
+     * 查询预订服务桌号列表
+     *
+     * @param storeId   门店ID
+     * @param categoryId 分类ID(可选,null表示查询全部)
+     * @return List<StoreBookingTable>
+     */
+    List<StoreBookingTable> getTableList(Integer storeId, Integer categoryId);
+
+    /**
+     * 查询预订服务桌号列表(包含分类名称)
+     *
+     * @param storeId   门店ID
+     * @param categoryId 分类ID(可选,null表示查询全部)
+     * @return List<StoreBookingTableVo>
+     */
+    List<StoreBookingTableVo> getTableListWithCategoryName(Integer storeId, Integer categoryId);
+
+    /**
+     * 新增预订服务桌号
+     *
+     * @param table 桌号对象
+     * @return boolean
+     */
+    boolean addTable(StoreBookingTable table);
+
+    /**
+     * 批量新增预订服务桌号
+     *
+     * @param storeId   门店ID
+     * @param categoryId 分类ID
+     * @param tables    桌号列表(每个包含桌号和座位数)
+     * @return boolean
+     */
+    boolean batchAddTables(Integer storeId, Integer categoryId, List<StoreBookingTable> tables);
+
+    /**
+     * 更新预订服务桌号
+     *
+     * @param table 桌号对象
+     * @return boolean
+     */
+    boolean updateTable(StoreBookingTable table);
+
+    /**
+     * 删除预订服务桌号
+     *
+     * @param id 桌号ID
+     * @return boolean
+     */
+    boolean deleteTable(Integer id);
+}

+ 11 - 0
alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java

@@ -449,6 +449,17 @@ public interface StoreInfoService extends IService<StoreInfo> {
     StoreInfoVo getClientStoreDetail(String storeId, String userId, String jingdu, String weidu);
 
     /**
+     * 仅填充门店基础信息及距离相关字段(distance、distance2、subwayName、storeLocation),供预订订单详情等使用。
+     * 逻辑与 getClientStoreDetail 中对应部分一致。
+     *
+     * @param storeId 门店ID
+     * @param jingdu  经度(可选,与 weidu 同时传入时计算 distance)
+     * @param weidu   纬度(可选)
+     * @return StoreInfoVo 含基础门店信息及 distance/distance2/subwayName/storeLocation,无则 null
+     */
+    StoreInfoVo getStoreInfoVoWithDistanceFields(Integer storeId, String jingdu, String weidu);
+
+    /**
      * 获取店铺营业状态
      * 判断店铺当前是否在营业时间内,返回营业状态和营业时间信息
      *

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

@@ -0,0 +1,21 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StorePaymentConfig;
+
+import java.util.List;
+
+/**
+ * 店铺支付证书落盘服务:按店铺ID将 store_payment_config 中的证书写入本地目录(路径按 storeId + storeTel 区分),并回写路径到配置表。
+ *
+ * @author system
+ */
+public interface StorePaymentConfigCertService {
+
+    /**
+     * 按店铺ID刷新证书到本地目录:先删除该店铺证书目录下原有文件,再根据当前配置重新写入并更新路径。
+     *
+     * @param storeId 店铺ID
+     * @return 本次写入的证书完整路径列表;若配置不存在或无证书内容则返回空列表
+     */
+    List<String> refreshCertByStoreId(Integer storeId);
+}

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

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

+ 43 - 0
alien-store/src/main/java/shop/alien/store/service/UserReservationOrderService.java

@@ -0,0 +1,43 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.UserReservationOrder;
+
+/**
+ * 用户预订订单表 服务类
+ *
+ * @author system
+ */
+public interface UserReservationOrderService extends IService<UserReservationOrder> {
+
+    /**
+     * 根据订单编号查询
+     *
+     * @param orderSn 订单编号
+     * @return 订单,不存在返回 null
+     */
+    UserReservationOrder getByOrderSn(String orderSn);
+
+    /**
+     * 根据支付单商户订单号查询(通过 merchant_payment_order 关联)
+     *
+     * @param outTradeNo 商户订单号
+     * @return 预订订单,不存在返回 null
+     */
+    UserReservationOrder getByOutTradeNo(String outTradeNo);
+
+    /**
+     * 生成订单编号,格式:YS + yyyyMMdd + 6位序列
+     *
+     * @return 订单编号
+     */
+    String generateOrderSn();
+
+    /**
+     * 按预约ID物理删除订单(用于 delete 接口与 add 新建数据对应)
+     *
+     * @param reservationId 预约ID
+     * @return 删除行数
+     */
+    int physicalDeleteByReservationId(Integer reservationId);
+}

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

@@ -0,0 +1,193 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.store.vo.ReservationOrderDetailVo;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 用户预约 服务类
+ *
+ * @author system
+ */
+public interface UserReservationService extends IService<UserReservation> {
+
+    /**
+     * 新增预约(含关联桌号)
+     *
+     * @param dto 预约DTO
+     * @return 预约ID
+     */
+    Integer add(UserReservationDTO dto);
+
+    /**
+     * 修改预约(含关联桌号)
+     *
+     * @param dto 预约DTO
+     * @return 是否成功
+     */
+    boolean updateReservation(UserReservationDTO dto);
+
+    /**
+     * 删除预约(假删除:将 user_reservation_order、user_reservation_table、user_reservation 的 delete_flag 置为 1,与 add 新建表数据对应)
+     *
+     * @param id 预约ID
+     * @return 是否成功
+     */
+    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
+     * @return 预约VO,不存在返回 null
+     */
+    UserReservationVo getDetail(Integer id);
+
+    /**
+     * 分页查询预约列表
+     *
+     * @param userId   用户ID(可选)
+     * @param storeId  门店ID(可选)
+     * @param status   预约状态(可选)
+     * @param dateFrom 预约日期起(可选)
+     * @param dateTo   预约日期止(可选)
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页结果
+     */
+    IPage<UserReservationVo> pageList(Integer userId, Integer storeId, Integer status,
+                                      Date dateFrom, Date dateTo,
+                                      Integer pageNum, Integer pageSize);
+
+    /**
+     * 列表查询预约(不分页)
+     *
+     * @param userId  用户ID(可选)
+     * @param storeId 门店ID(可选)
+     * @param status  预约状态(可选)
+     * @return 预约VO列表
+     */
+    List<UserReservationVo> list(Integer userId, Integer storeId, Integer status);
+
+    Map<String, Object> getBookingsByStoreId(Integer storeId);
+
+    /**
+     * 从今天起查找第一个存在未约满选座的日期,并返回该日的预约数据及每个选座的约满标识。
+     * 不按商户设定计算,仅根据 UserReservation、UserReservationTable 数据:以日期为集合,每个选座(桌)当日若被任意有效预约占用则为已约满,否则未约满;同一标识字段 full(true=已约满,false=未约满)。
+     *
+     * @param storeId 门店ID
+     * @return Map:date、reservations、tableStatusList(每项含 tableId/tableNumber/seatingCapacity/full)
+     */
+    Map<String, Object> findFirstAvailableDayReservations(Integer storeId);
+
+    /**
+     * 查询商家端预约信息列表
+     *
+     * @param storeId    门店ID(必填)
+     * @param status     预约状态(可选,0:待确认 1:已确认 2:已到店 3:已取消 4:未到店超时)
+     * @param dateFrom   预约日期起(可选)
+     * @param dateTo     预约日期止(可选)
+     * @param orderStatus 订单状态(可选,0:待支付 1:待使用 2:已完成 3:已过期 4:已取消 5:已关闭 6:退款中 7:已退款 8:商家预订)
+     * @return 预约信息列表
+     */
+    java.util.List<shop.alien.entity.store.vo.StoreReservationListVo> getStoreReservationList(
+            Integer storeId, Integer status, java.util.Date dateFrom, java.util.Date dateTo, Integer orderStatus);
+
+    /**
+     * 商家端取消预约
+     *
+     * @param reservationId 预约ID
+     * @return 是否成功
+     */
+    boolean cancelReservationByStore(Integer reservationId);
+
+    /**
+     * 商家端删除预订信息
+     * 只有订单状态为已取消(4)、已退款(7)、已完成(2)时才能删除
+     *
+     * @param reservationId 预约ID
+     * @return 是否成功
+     */
+    boolean deleteReservationByStore(Integer reservationId);
+
+    /**
+     * 商家端加时
+     * 更新预订信息的结束时间,新的结束时间 = 加时开始时间 + 加时分钟数
+     *
+     * @param reservationId 预约ID
+     * @param addTimeStart 加时开始时间(HH:mm格式)
+     * @param addTimeMinutes 加时分钟数
+     * @return 是否成功
+     */
+    boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes);
+
+    /**
+     * 按预定日期查询指定日期的定桌情况(该日预约列表 + 每个选座的约满标识 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查询详情:门店信息(含 distance、distance2、subwayName、storeLocation)、预订信息、定桌信息。
+     *
+     * @param orderId 预订订单ID user_reservation_order.id
+     * @param jingdu  经度(可选,与 weidu 同时传入时返回用户到门店距离 distance)
+     * @param weidu   纬度(可选)
+     * @return 详情 VO,订单不存在返回 null
+     */
+    ReservationOrderDetailVo getOrderDetailByOrderId(Integer orderId, String jingdu, String weidu);
+
+    /**
+     * 定时任务:将「预约结束时间已过」且「订单状态为待使用」的预订标记为已过期/未到店超时。
+     * 更新:user_reservation.status = 4(未到店超时),user_reservation_order.order_status = 3(已过期)。
+     *
+     * @return 本次更新的预约数量(即更新的 order 数量)
+     */
+    int markReservationTimeoutByEndTime();
+}

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

@@ -0,0 +1,100 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.MerchantPaymentOrder;
+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;
+
+/**
+ * 商户支付单表 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class MerchantPaymentOrderServiceImpl extends ServiceImpl<MerchantPaymentOrderMapper, MerchantPaymentOrder> implements MerchantPaymentOrderService {
+
+    @Override
+    public MerchantPaymentOrder getByOutTradeNo(String outTradeNo) {
+        if (outTradeNo == null || outTradeNo.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<MerchantPaymentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOutTradeNo, outTradeNo);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public String generatePaymentNo() {
+        String dateStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+        int random = ThreadLocalRandom.current().nextInt(10000);
+        return "PAY" + dateStr + String.format("%04d", random);
+    }
+
+    @Override
+    public int logicDeleteByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return 0;
+        }
+        LambdaUpdateWrapper<MerchantPaymentOrder> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOrderId, orderId);
+        wrapper.set(MerchantPaymentOrder::getDeleteFlag, 1);
+        return baseMapper.update(null, wrapper);
+    }
+
+    @Override
+    public int logicDeleteByOrderIdAndPayType(Integer orderId, String payType) {
+        if (orderId == null || payType == null || payType.trim().isEmpty()) {
+            return 0;
+        }
+        LambdaUpdateWrapper<MerchantPaymentOrder> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOrderId, orderId);
+        wrapper.eq(MerchantPaymentOrder::getPayType, payType.trim());
+        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);
+    }
+}

+ 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");
+        }
+    }
+}

+ 40 - 0
alien-store/src/main/java/shop/alien/store/service/impl/RefundRecordAsyncServiceImpl.java

@@ -0,0 +1,40 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.RefundRecord;
+import shop.alien.store.service.MerchantPaymentOrderService;
+import shop.alien.store.service.RefundRecordAsyncService;
+import shop.alien.store.service.RefundRecordService;
+
+/**
+ * 退款相关异步落库:更新支付单 + 保存退款记录
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RefundRecordAsyncServiceImpl implements RefundRecordAsyncService {
+
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+    private final RefundRecordService refundRecordService;
+
+    @Async("taskExecutor")
+    @Override
+    public void completeRefundAsync(MerchantPaymentOrder paymentOrder, RefundRecord record) {
+        String outTradeNo = paymentOrder != null ? paymentOrder.getOutTradeNo() : (record != null ? record.getOutTradeNo() : null);
+        try {
+            if (paymentOrder != null) {
+                merchantPaymentOrderService.updateById(paymentOrder);
+            }
+            if (record != null) {
+                refundRecordService.save(record);
+            }
+            log.debug("退款异步落库成功 outTradeNo={}", outTradeNo);
+        } catch (Exception e) {
+            log.error("退款异步落库失败 outTradeNo={}", outTradeNo, e);
+        }
+    }
+}

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

@@ -0,0 +1,224 @@
+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.setBusinessSection(dto.getBusinessSection());
+            vo.setStoreName(dto.getStoreName());
+            vo.setStoreEntranceImageUrl(dto.getStoreEntranceImageUrl());
+            vo.setOrderStatus(dto.getOrderStatus());
+            vo.setPaymentStatus(dto.getPaymentStatus());
+            UserReservation resForStatus = dto.getReservationId() != null ? reservationMap.get(dto.getReservationId()) : null;
+            vo.setStatusText(buildStatusText(dto.getOrderStatus(), resForStatus));
+            vo.setDepositAmount(dto.getDepositAmount());
+            vo.setVerificationCode(dto.getVerificationCode());
+            vo.setOutTradeNo(dto.getOutTradeNo());
+            vo.setPayType(dto.getPayType());
+            vo.setMerchantCancelReason(dto.getMerchantCancelReason());
+            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;
+    }
+}

+ 332 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ReservationOrderPageServiceImpl.java

@@ -0,0 +1,332 @@
+package shop.alien.store.service.impl;
+
+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.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;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 预订订单页面展示服务实现
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReservationOrderPageServiceImpl implements ReservationOrderPageService {
+
+    private static final int PAYMENT_DEADLINE_MINUTES = 15;
+    private static final int ORDER_STATUS_UNPAID = 0;
+    private static final int ORDER_STATUS_TO_USE = 1;
+    private static final int CANCELLATION_FREE = 0;
+    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) {
+        if (orderId == null) {
+            return null;
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return null;
+        }
+        return buildPageVo(order.getStoreId(), order);
+    }
+
+    private ReservationOrderPageVo buildPageVo(Integer storeId, UserReservationOrder order) {
+        ReservationOrderPageVo vo = new ReservationOrderPageVo();
+        vo.setOrderId(order.getId());
+        vo.setOrderSn(order.getOrderSn());
+        vo.setOrderStatus(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);
+
+        // 支付倒计时
+        vo.setPaymentDeadline(order.getPaymentDeadline());
+        int secondsLeft = computePaymentSecondsLeft(order);
+        vo.setPaymentSecondsLeft(secondsLeft);
+        vo.setCanContinuePay(Boolean.valueOf(ORDER_STATUS_UNPAID == order.getOrderStatus() && secondsLeft > 0));
+
+        // 页面标题与后缀(全状态);已取消时在加载 reservation 后根据 reason 区分为用户取消/商家取消
+        Integer orderStatus = order.getOrderStatus();
+        vo.setPageTitle(buildPageTitle(orderStatus, null));
+        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) {
+            StoreInfo store = storeInfoService.getById(storeId);
+            if (store != null) {
+            vo.setStoreId(storeId);
+            vo.setStoreName(store.getStoreName());
+            vo.setStoreAddress(store.getStoreAddress());
+            vo.setStoreTel(store.getStoreTel());
+            vo.setStorePosition(store.getStorePosition());
+            }
+        }
+
+        // 预约信息
+        if (order.getReservationId() != null) {
+            UserReservation reservation = userReservationService.getById(order.getReservationId());
+            if (reservation != null) {
+                vo.setReservationDate(reservation.getReservationDate());
+                vo.setReservationDateText(formatDateWithToday(reservation.getReservationDate()));
+                vo.setGuestCount(reservation.getGuestCount());
+                vo.setDiningTimeSlotText(buildDiningTimeSlot(reservation.getStartTime(), reservation.getEndTime()));
+                vo.setDiningTimeText(buildDiningTimeSlot(reservation.getStartTime(), reservation.getEndTime()));
+
+                List<Integer> tableIds = listTableIdsByReservationId(reservation.getId());
+                List<StoreBookingTable> tables = new ArrayList<>();
+                String categoryName = null;
+                for (Integer tableId : tableIds) {
+                    StoreBookingTable t = storeBookingTableService.getById(tableId);
+                    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(","));
+                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));
+                }
+            }
+        }
+
+        // 使用凭证
+        vo.setVerificationCode(order.getVerificationCode());
+        vo.setVerificationUrl(order.getVerificationUrl());
+
+        // 预订说明(待支付与待使用页面共用)
+        Integer graceMinutes = order.getLateArrivalGraceMinutes() != null ? order.getLateArrivalGraceMinutes() : 0;
+        vo.setLateArrivalGraceMinutes(graceMinutes);
+        if (graceMinutes != null && graceMinutes > 0) {
+            vo.setSeatRetentionText("未按时到店座位保留" + graceMinutes + "分钟");
+        } else {
+            vo.setSeatRetentionText("未按时到店座位将不会保留");
+        }
+        vo.setDepositAmount(order.getDepositAmount());
+        vo.setDepositText(order.getDepositAmount() != null && order.getDepositAmount().compareTo(BigDecimal.ZERO) > 0
+                ? "需支付¥" + order.getDepositAmount().stripTrailingZeros().toPlainString() + "订金"
+                : null);
+        vo.setDepositRefundRule(order.getDepositRefundRule());
+        vo.setCancellationPolicyType(order.getCancellationPolicyType());
+        vo.setFreeCancellationDeadline(order.getFreeCancellationDeadline());
+        vo.setFreeCancellationDeadlineText(formatFreeCancellationDeadline(order.getFreeCancellationDeadline()));
+        vo.setCancellationPolicyText(buildCancellationPolicyText(order));
+
+        // 订单信息
+        vo.setOrderCreatedTime(order.getCreatedTime());
+        vo.setOrderCreatedTimeText(order.getCreatedTime() != null ? formatDateWithToday(order.getCreatedTime()) : null);
+        vo.setContactName(null);
+        vo.setContactPhone(null);
+        vo.setContactText(null);
+
+        return vo;
+    }
+
+    private List<Integer> listTableIdsByReservationId(Integer reservationId) {
+        return userReservationTableMapper.selectList(
+                new LambdaQueryWrapper<UserReservationTable>()
+                        .eq(UserReservationTable::getReservationId, reservationId)
+                        .orderByAsc(UserReservationTable::getSort)
+                        .orderByAsc(UserReservationTable::getId)
+        ).stream().map(UserReservationTable::getTableId).collect(Collectors.toList());
+    }
+
+    private int computePaymentSecondsLeft(UserReservationOrder order) {
+        if (order.getOrderStatus() == null || order.getOrderStatus() != ORDER_STATUS_UNPAID || order.getPaymentDeadline() == null) {
+            return 0;
+        }
+        long now = System.currentTimeMillis();
+        long deadline = order.getPaymentDeadline().getTime();
+        long diff = (deadline - now) / 1000;
+        return diff > 0 ? (int) diff : 0;
+    }
+
+    /**
+     * 页面主标题。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 "预订成功";
+            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 "免费预订";
+        if (cancellationPolicyType == CANCELLATION_CONDITIONAL) return "分情况是否免责";
+        return "不可免责取消";
+    }
+
+    private String buildSuccessPageTitleSuffix(Integer cancellationPolicyType) {
+        if (cancellationPolicyType == null) return "不可免责";
+        if (cancellationPolicyType == CANCELLATION_FREE) return "免费预订";
+        if (cancellationPolicyType == CANCELLATION_CONDITIONAL) return "分情况是否免责";
+        return "不可免责";
+    }
+
+    private 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;
+    }
+
+    private String buildGuestAndCategoryText(String categoryName, Integer guestCount, List<StoreBookingTable> tables) {
+        int capacity = tables.stream().mapToInt(t -> t.getSeatingCapacity() != null ? t.getSeatingCapacity() : 0).max().orElse(guestCount != null ? guestCount : 0);
+        if (capacity <= 0 && guestCount != null) capacity = guestCount;
+        String part = categoryName != null ? categoryName : "预订";
+        return part + (capacity > 0 ? capacity + "人" : "");
+    }
+
+    private String buildLocationTableText(String categoryName, String tableNumbersText) {
+        if (categoryName != null && tableNumbersText != null && !tableNumbersText.isEmpty()) {
+            return categoryName + "|" + tableNumbersText;
+        }
+        if (tableNumbersText != null && !tableNumbersText.isEmpty()) return tableNumbersText;
+        return categoryName;
+    }
+
+    private String formatDateWithToday(Date date) {
+        if (date == null) return null;
+        SimpleDateFormat f = new SimpleDateFormat("MM月dd日", Locale.CHINA);
+        String s = f.format(date);
+        Calendar cal = Calendar.getInstance();
+        Calendar d = Calendar.getInstance();
+        d.setTime(date);
+        if (cal.get(Calendar.YEAR) == d.get(Calendar.YEAR) && cal.get(Calendar.DAY_OF_YEAR) == d.get(Calendar.DAY_OF_YEAR)) {
+            s += " 今天";
+        }
+        return s;
+    }
+
+    private String formatFreeCancellationDeadline(Date deadline) {
+        if (deadline == null) return null;
+        SimpleDateFormat f = new SimpleDateFormat("MM月dd日 HH:mm", Locale.CHINA);
+        return f.format(deadline) + "前可免责取消";
+    }
+
+    private String buildCancellationPolicyText(UserReservationOrder order) {
+        Integer type = order.getCancellationPolicyType();
+        if (type == null) return "取消将收取订金";
+        if (type == CANCELLATION_FREE) return "免费取消";
+        if (type == CANCELLATION_NOT_FREE) return "不可免责取消,取消将收取订金";
+        if (type == CANCELLATION_CONDITIONAL) {
+            String line = formatFreeCancellationDeadline(order.getFreeCancellationDeadline());
+            return line != null ? line + ";在上述时间前取消订金可原路返回,之后取消将收取订金" : "分情况是否免责,取消将收取订金";
+        }
+        return "取消将收取订金";
+    }
+
+    /** 根据订单状态推导支付状态 0:未支付 1:已支付 2:已退款 */
+    private int derivePaymentStatus(Integer orderStatus) {
+        if (orderStatus == null) return 0;
+        if (orderStatus == 0) return 0;
+        if (orderStatus == 6 || orderStatus == 7) return 2;
+        return 1;
+    }
+}

+ 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());
+        }
+    }
+}

+ 289 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingBusinessHoursServiceImpl.java

@@ -0,0 +1,289 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.mapper.StoreBookingBusinessHoursMapper;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 预订服务营业时间表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreBookingBusinessHoursServiceImpl extends ServiceImpl<StoreBookingBusinessHoursMapper, StoreBookingBusinessHours> implements StoreBookingBusinessHoursService {
+
+    // 时间格式正则:HH:mm
+    private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3]):[0-5][0-9]$");
+
+
+    @Override
+    public List<StoreBookingBusinessHours> getListBySettingsId(Integer settingsId) {
+        log.info("StoreBookingBusinessHoursServiceImpl.getListBySettingsId?settingsId={}", settingsId);
+        
+        if (settingsId == null) {
+            log.warn("查询营业时间列表失败:设置ID不能为空");
+            return Collections.emptyList();
+        }
+        
+        LambdaQueryWrapper<StoreBookingBusinessHours> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getSort);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getId);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public List<StoreBookingBusinessHours> getListBySettingsIdAndType(Integer settingsId, Integer businessType) {
+        log.info("StoreBookingBusinessHoursServiceImpl.getListBySettingsIdAndType?settingsId={}, businessType={}", settingsId, businessType);
+        
+        if (settingsId == null) {
+            log.warn("查询营业时间列表失败:设置ID不能为空");
+            return Collections.emptyList();
+        }
+        
+        LambdaQueryWrapper<StoreBookingBusinessHours> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+        if (businessType != null) {
+            wrapper.eq(StoreBookingBusinessHours::getBusinessType, businessType);
+        }
+        wrapper.orderByAsc(StoreBookingBusinessHours::getSort);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getId);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public boolean saveOrUpdateBusinessHours(StoreBookingBusinessHours businessHours) {
+        log.info("StoreBookingBusinessHoursServiceImpl.saveOrUpdateBusinessHours?businessHours={}", businessHours);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (businessHours.getSettingsId() == null) {
+            log.warn("保存营业时间失败:设置ID不能为空");
+            throw new RuntimeException("设置ID不能为空");
+        }
+        if (businessHours.getBusinessType() == null) {
+            log.warn("保存营业时间失败:营业类型不能为空");
+            throw new RuntimeException("营业类型不能为空");
+        }
+        if (businessHours.getBookingTimeType() == null) {
+            log.warn("保存营业时间失败:预订时间类型不能为空");
+            throw new RuntimeException("预订时间类型不能为空");
+        }
+        
+        // 如果是特殊营业(节假日),验证节假日类型
+//        if (businessHours.getBusinessType() != null && businessHours.getBusinessType() == 2) {
+//            if (!StringUtils.hasText(businessHours.getHolidayType())) {
+//                log.warn("保存营业时间失败:特殊营业时必须填写节假日类型");
+//                throw new RuntimeException("特殊营业时必须填写节假日类型");
+//            }
+//        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+        if (businessHours.getBookingTimeType() != null && businessHours.getBookingTimeType() == 0) {
+            if (!StringUtils.hasText(businessHours.getStartTime())) {
+                log.warn("保存营业时间失败:非全天时必须填写开始时间");
+                throw new RuntimeException("非全天时必须填写开始时间");
+            }
+            if (!StringUtils.hasText(businessHours.getEndTime())) {
+                log.warn("保存营业时间失败:非全天时必须填写结束时间");
+                throw new RuntimeException("非全天时必须填写结束时间");
+            }
+            // 验证时间格式
+            if (!TIME_PATTERN.matcher(businessHours.getStartTime()).matches()) {
+                log.warn("保存营业时间失败:开始时间格式不正确,应为HH:mm格式");
+                throw new RuntimeException("开始时间格式不正确,应为HH:mm格式");
+            }
+            if (!TIME_PATTERN.matcher(businessHours.getEndTime()).matches()) {
+                log.warn("保存营业时间失败:结束时间格式不正确,应为HH:mm格式");
+                throw new RuntimeException("结束时间格式不正确,应为HH:mm格式");
+            }
+            // 验证开始时间必须小于结束时间(支持跨天,如22:00到次日02:00)
+            // 这里只做基本格式验证,跨天逻辑由业务层处理
+        } else {
+            // 如果是全天,清空开始时间和结束时间
+            businessHours.setStartTime(null);
+            businessHours.setEndTime(null);
+        }
+        
+        // 设置排序,如果为空则默认为0
+        if (businessHours.getSort() == null) {
+            businessHours.setSort(0);
+        }
+        
+        if (businessHours.getId() != null) {
+            // 更新
+            LambdaUpdateWrapper<StoreBookingBusinessHours> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreBookingBusinessHours::getId, businessHours.getId());
+            
+            updateWrapper.set(StoreBookingBusinessHours::getBusinessType, businessHours.getBusinessType());
+            updateWrapper.set(StoreBookingBusinessHours::getHolidayType, businessHours.getHolidayType());
+            updateWrapper.set(StoreBookingBusinessHours::getHolidayDate, businessHours.getHolidayDate());
+            updateWrapper.set(StoreBookingBusinessHours::getBookingTimeType, businessHours.getBookingTimeType());
+            updateWrapper.set(StoreBookingBusinessHours::getStartTime, businessHours.getStartTime());
+            updateWrapper.set(StoreBookingBusinessHours::getEndTime, businessHours.getEndTime());
+            updateWrapper.set(StoreBookingBusinessHours::getSort, businessHours.getSort());
+            
+            if (userId != null) {
+                updateWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+            }
+            
+            return this.update(updateWrapper);
+        } else {
+            // 新增
+            businessHours.setCreatedUserId(userId);
+            return this.save(businessHours);
+        }
+    }
+
+    @Override
+    public boolean batchSaveBusinessHours(Integer settingsId, List<StoreBookingBusinessHours> businessHoursList) {
+        log.info("StoreBookingBusinessHoursServiceImpl.batchSaveBusinessHours?settingsId={}, size={}", settingsId, businessHoursList != null ? businessHoursList.size() : 0);
+        
+//        if (settingsId == null) {
+//            log.warn("批量保存营业时间失败:设置ID不能为空");
+//            throw new RuntimeException("设置ID不能为空");
+//        }
+//        if (businessHoursList == null || businessHoursList.isEmpty()) {
+//            log.warn("批量保存营业时间失败:营业时间列表不能为空");
+//            throw new RuntimeException("营业时间列表不能为空");
+//        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 处理空列表情况
+        if (businessHoursList == null || businessHoursList.isEmpty()) {
+            // 如果列表为空,删除该设置下的所有营业时间(逻辑删除)
+            LambdaUpdateWrapper<StoreBookingBusinessHours> deleteWrapper = new LambdaUpdateWrapper<>();
+            deleteWrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+            deleteWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+            if (userId != null) {
+                deleteWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+            }
+            this.update(deleteWrapper);
+            return true;
+        }
+        
+        // 1. 收集所有传入的id(有id的项,用于编辑)
+        List<Integer> incomingIds = businessHoursList.stream()
+                .map(StoreBookingBusinessHours::getId)
+                .filter(id -> id != null)
+                .collect(Collectors.toList());
+        
+        // 2. 查询现有的营业时间
+        List<StoreBookingBusinessHours> existingHours = getListBySettingsId(settingsId);
+        List<Integer> existingIds = existingHours.stream()
+                .map(StoreBookingBusinessHours::getId)
+                .filter(id -> id != null)
+                .collect(Collectors.toList());
+        
+        // 3. 删除不在传入列表中的记录(逻辑删除)
+        // 如果传入列表中有id,则删除那些不在传入列表中的记录
+        if (!incomingIds.isEmpty()) {
+            List<Integer> idsToDelete = existingIds.stream()
+                    .filter(id -> !incomingIds.contains(id))
+                    .collect(Collectors.toList());
+            
+            if (!idsToDelete.isEmpty()) {
+                LambdaUpdateWrapper<StoreBookingBusinessHours> deleteWrapper = new LambdaUpdateWrapper<>();
+                deleteWrapper.in(StoreBookingBusinessHours::getId, idsToDelete);
+                deleteWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+                if (userId != null) {
+                    deleteWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+                }
+                this.update(deleteWrapper);
+            }
+        } else {
+            // 如果传入列表中没有id,说明全部是新增,先删除该设置下的所有营业时间(逻辑删除)
+            LambdaUpdateWrapper<StoreBookingBusinessHours> deleteWrapper = new LambdaUpdateWrapper<>();
+            deleteWrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+            deleteWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+            if (userId != null) {
+                deleteWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+            }
+            this.update(deleteWrapper);
+        }
+        
+        // 4. 遍历传入的列表,有id的更新,没有id的新增
+        for (StoreBookingBusinessHours businessHours : businessHoursList) {
+            businessHours.setSettingsId(settingsId);
+            if (businessHours.getId() != null) {
+                // 有id,走更新逻辑
+                businessHours.setCreatedUserId(null); // 更新时不需要设置创建人
+            } else {
+                // 没有id,走新增逻辑
+                businessHours.setId(null); // 确保是新增
+                businessHours.setCreatedUserId(userId);
+            }
+            // 验证并保存
+            saveOrUpdateBusinessHours(businessHours);
+        }
+        
+        return true;
+    }
+
+    @Override
+    public boolean deleteBusinessHours(Integer id) {
+        log.info("StoreBookingBusinessHoursServiceImpl.deleteBusinessHours?id={}", id);
+        
+        if (id == null) {
+            log.warn("删除营业时间失败:营业时间ID不能为空");
+            throw new RuntimeException("营业时间ID不能为空");
+        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 逻辑删除
+        LambdaUpdateWrapper<StoreBookingBusinessHours> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreBookingBusinessHours::getId, id);
+        updateWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+        if (userId != null) {
+            updateWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+        }
+        
+        return this.update(updateWrapper);
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 310 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingCategoryServiceImpl.java

@@ -0,0 +1,310 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.mapper.StoreBookingCategoryMapper;
+import shop.alien.store.service.StoreBookingCategoryService;
+import shop.alien.store.service.StoreBookingTableService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.List;
+
+/**
+ * 预订服务分类表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreBookingCategoryServiceImpl extends ServiceImpl<StoreBookingCategoryMapper, StoreBookingCategory> implements StoreBookingCategoryService {
+
+    private final StoreBookingTableService storeBookingTableService;
+
+    @Override
+    public List<StoreBookingCategory> getCategoryList(Integer storeId) {
+        log.info("StoreBookingCategoryServiceImpl.getCategoryList?storeId={}", storeId);
+        
+        LambdaQueryWrapper<StoreBookingCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingCategory::getStoreId, storeId)
+                .orderByAsc(StoreBookingCategory::getSort) // 按排序字段升序
+                .orderByDesc(StoreBookingCategory::getCreatedTime); // 如果排序字段相同,按创建时间倒序
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public boolean addCategory(StoreBookingCategory category) {
+        log.info("StoreBookingCategoryServiceImpl.addCategory?category={}", category);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (category.getStoreId() == null) {
+            log.warn("新增预订服务分类失败:门店ID不能为空");
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (!StringUtils.hasText(category.getCategoryName())) {
+            log.warn("新增预订服务分类失败:分类名称不能为空");
+            throw new RuntimeException("分类名称不能为空");
+        }
+        if (!StringUtils.hasText(category.getFloorPlanImages())) {
+            log.warn("新增预订服务分类失败:平面图不能为空");
+            throw new RuntimeException("平面图不能为空");
+        }
+        if (category.getMaxBookingTime() == null || category.getMaxBookingTime() <= 0) {
+            log.warn("新增预订服务分类失败:最长预订时间必须大于0");
+            throw new RuntimeException("最长预订时间必须大于0");
+        }
+        
+        // 验证图片数量(最多9张)
+        String[] images = category.getFloorPlanImages().split(",");
+        if (images.length > 9) {
+            log.warn("新增预订服务分类失败:平面图最多9张");
+            throw new RuntimeException("平面图最多9张");
+        }
+        
+        // 先通过门店ID查询当前门店下的所有分类,检查分类名称是否重复
+        // 处理分类名称:去除前后空格
+        String categoryName = category.getCategoryName().trim();
+        category.setCategoryName(categoryName);
+        
+        // 查询当前门店下是否已存在相同的分类名称(只检查未删除的记录)
+        LambdaQueryWrapper<StoreBookingCategory> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreBookingCategory::getStoreId, category.getStoreId())
+                .eq(StoreBookingCategory::getCategoryName, categoryName);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        StoreBookingCategory existingCategory = this.getOne(checkWrapper);
+        if (existingCategory != null) {
+            log.warn("新增预订服务分类失败:当前门店下分类名称已存在,storeId={}, categoryName={}", 
+                    category.getStoreId(), categoryName);
+            throw new RuntimeException("此名称已存在");
+        }
+        
+        // 查询当前最大的排序值
+        LambdaQueryWrapper<StoreBookingCategory> maxSortWrapper = new LambdaQueryWrapper<>();
+        maxSortWrapper.eq(StoreBookingCategory::getStoreId, category.getStoreId())
+                .orderByDesc(StoreBookingCategory::getSort)
+                .last("LIMIT 1");
+        StoreBookingCategory maxSortCategory = this.getOne(maxSortWrapper);
+        int maxSort = (maxSortCategory != null && maxSortCategory.getSort() != null) ? maxSortCategory.getSort() : 0;
+        
+        // 设置默认值
+        if (category.getIsDisplay() == null) {
+            category.setIsDisplay(1); // 默认显示
+        }
+        if (category.getSort() == null) {
+            category.setSort(maxSort + 1); // 设置排序值
+        }
+        category.setCreatedUserId(userId);
+        
+        return this.save(category);
+    }
+
+    @Override
+    public boolean updateCategory(StoreBookingCategory category) {
+        log.info("StoreBookingCategoryServiceImpl.updateCategory?category={}", category);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (category.getId() == null) {
+            log.warn("更新预订服务分类失败:分类ID不能为空");
+            throw new RuntimeException("分类ID不能为空");
+        }
+        
+        StoreBookingCategory existingCategory = this.getById(category.getId());
+        if (existingCategory == null) {
+            log.warn("更新预订服务分类失败:分类不存在,id={}", category.getId());
+            throw new RuntimeException("分类不存在");
+        }
+        
+        // 如果修改了分类名称,先通过门店ID查询当前门店下的所有分类,检查新分类名称是否重复
+        if (StringUtils.hasText(category.getCategoryName())) {
+            // 处理分类名称:去除前后空格
+            String categoryName = category.getCategoryName().trim();
+            category.setCategoryName(categoryName);
+            
+            // 如果分类名称有变化,检查当前门店下是否已存在相同的分类名称
+            if (!categoryName.equals(existingCategory.getCategoryName())) {
+                // 查询当前门店下是否已存在相同的分类名称(排除当前记录,只检查未删除的记录)
+                LambdaQueryWrapper<StoreBookingCategory> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(StoreBookingCategory::getStoreId, existingCategory.getStoreId())
+                        .eq(StoreBookingCategory::getCategoryName, categoryName)
+                        .ne(StoreBookingCategory::getId, category.getId());
+                // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+                StoreBookingCategory duplicateCategory = this.getOne(wrapper);
+                if (duplicateCategory != null) {
+                    log.warn("更新预订服务分类失败:当前门店下分类名称已存在,storeId={}, categoryName={}, id={}", 
+                            existingCategory.getStoreId(), categoryName, category.getId());
+                    throw new RuntimeException("此名称已存在不能编辑");
+                }
+            }
+        }
+        
+        // 验证图片数量(最多9张)
+        if (StringUtils.hasText(category.getFloorPlanImages())) {
+            String[] images = category.getFloorPlanImages().split(",");
+            if (images.length > 9) {
+                log.warn("更新预订服务分类失败:平面图最多9张");
+                throw new RuntimeException("平面图最多9张");
+            }
+        }
+        
+        // 验证最长预订时间
+        if (category.getMaxBookingTime() != null && category.getMaxBookingTime() <= 0) {
+            log.warn("更新预订服务分类失败:最长预订时间必须大于0");
+            throw new RuntimeException("最长预订时间必须大于0");
+        }
+        
+        // 更新字段
+        LambdaUpdateWrapper<StoreBookingCategory> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreBookingCategory::getId, category.getId());
+        
+        if (StringUtils.hasText(category.getCategoryName())) {
+            updateWrapper.set(StoreBookingCategory::getCategoryName, category.getCategoryName());
+        }
+        if (StringUtils.hasText(category.getFloorPlanImages())) {
+            updateWrapper.set(StoreBookingCategory::getFloorPlanImages, category.getFloorPlanImages());
+        }
+        if (category.getIsDisplay() != null) {
+            updateWrapper.set(StoreBookingCategory::getIsDisplay, category.getIsDisplay());
+        }
+        if (category.getMaxBookingTime() != null) {
+            updateWrapper.set(StoreBookingCategory::getMaxBookingTime, category.getMaxBookingTime());
+        }
+        if (category.getSort() != null) {
+            updateWrapper.set(StoreBookingCategory::getSort, category.getSort());
+        }
+        
+        if (userId != null) {
+            updateWrapper.set(StoreBookingCategory::getUpdatedUserId, userId);
+        }
+        
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean deleteCategory(Integer id) {
+        log.info("StoreBookingCategoryServiceImpl.deleteCategory?id={}", id);
+        
+        StoreBookingCategory category = this.getById(id);
+        if (category == null) {
+            log.warn("删除预订服务分类失败:分类不存在,id={}", id);
+            throw new RuntimeException("分类不存在");
+        }
+        
+        // 校验:检查当前分类下是否有桌号,如果有则不允许删除
+        LambdaQueryWrapper<StoreBookingTable> tableWrapper = new LambdaQueryWrapper<>();
+        tableWrapper.eq(StoreBookingTable::getCategoryId, id);
+        long tableCount = storeBookingTableService.count(tableWrapper);
+        if (tableCount > 0) {
+            log.warn("删除预订服务分类失败:当前分类下存在桌号,需要先删除桌号才能删除该分类,categoryId={}, tableCount={}", id, tableCount);
+            throw new RuntimeException("当前分类下存在桌号,需要先删除桌号才能删除该分类");
+        }
+        
+        // 逻辑删除
+        return this.removeById(id);
+    }
+
+    @Override
+    public boolean updateCategorySort(Integer storeId, List<Integer> categoryIds) {
+        log.info("StoreBookingCategoryServiceImpl.updateCategorySort?storeId={}&categoryIds={}", storeId, categoryIds);
+        
+        if (storeId == null || categoryIds == null || categoryIds.isEmpty()) {
+            log.warn("更新预订服务分类排序失败:参数不完整");
+            throw new RuntimeException("参数不完整");
+        }
+        
+        // 验证所有分类ID是否属于该门店
+        LambdaQueryWrapper<StoreBookingCategory> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreBookingCategory::getStoreId, storeId)
+                .in(StoreBookingCategory::getId, categoryIds);
+        long count = this.count(checkWrapper);
+        if (count != categoryIds.size()) {
+            log.warn("更新预订服务分类排序失败:部分分类ID不属于该门店");
+            throw new RuntimeException("部分分类ID不属于该门店");
+        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 批量更新排序值
+        for (int i = 0; i < categoryIds.size(); i++) {
+            LambdaUpdateWrapper<StoreBookingCategory> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreBookingCategory::getId, categoryIds.get(i))
+                    .set(StoreBookingCategory::getSort, i + 1);
+            if (userId != null) {
+                updateWrapper.set(StoreBookingCategory::getUpdatedUserId, userId);
+            }
+            this.update(updateWrapper);
+        }
+        
+        return true;
+    }
+
+    @Override
+    public boolean updateDisplayStatus(Integer id, Integer isDisplay) {
+        log.info("StoreBookingCategoryServiceImpl.updateDisplayStatus?id={}&isDisplay={}", id, isDisplay);
+        
+        if (id == null) {
+            log.warn("更新预订服务分类显示状态失败:分类ID不能为空");
+            throw new RuntimeException("分类ID不能为空");
+        }
+        
+        if (isDisplay == null || (isDisplay != 0 && isDisplay != 1)) {
+            log.warn("更新预订服务分类显示状态失败:显示状态值无效,isDisplay={}", isDisplay);
+            throw new RuntimeException("显示状态值无效,必须为0(隐藏)或1(显示)");
+        }
+        
+        StoreBookingCategory category = this.getById(id);
+        if (category == null) {
+            log.warn("更新预订服务分类显示状态失败:分类不存在,id={}", id);
+            throw new RuntimeException("分类不存在");
+        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 更新显示状态
+        LambdaUpdateWrapper<StoreBookingCategory> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreBookingCategory::getId, id)
+                .set(StoreBookingCategory::getIsDisplay, isDisplay);
+        
+        if (userId != null) {
+            updateWrapper.set(StoreBookingCategory::getUpdatedUserId, userId);
+        }
+        
+        return this.update(updateWrapper);
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 379 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java

@@ -0,0 +1,379 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.EssentialHolidayComparison;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
+import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
+import shop.alien.mapper.EssentialHolidayComparisonMapper;
+import shop.alien.mapper.StoreBookingSettingsMapper;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+import shop.alien.store.service.StoreBookingSettingsService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.regex.Pattern;
+
+/**
+ * 预订服务信息设置表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSettingsMapper, StoreBookingSettings> implements StoreBookingSettingsService {
+
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+
+    // 时间格式正则:HH:mm
+    private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3]):[0-5][0-9]$");
+
+    @Override
+    public StoreBookingSettings getByStoreId(Integer storeId) {
+        log.info("StoreBookingSettingsServiceImpl.getByStoreId?storeId={}", storeId);
+        
+        if (storeId == null) {
+            log.warn("查询预订服务信息设置失败:门店ID不能为空");
+            return null;
+        }
+        
+        LambdaQueryWrapper<StoreBookingSettings> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingSettings::getStoreId, storeId);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public boolean saveOrUpdateSettings(StoreBookingSettings settings) {
+        log.info("StoreBookingSettingsServiceImpl.saveOrUpdateSettings?settings={}", settings);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (settings.getStoreId() == null) {
+            log.warn("保存预订服务信息设置失败:门店ID不能为空");
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (settings.getRetainPositionFlag() == null) {
+            log.warn("保存预订服务信息设置失败:未按时到店是否保留位置不能为空");
+            throw new RuntimeException("未按时到店是否保留位置不能为空");
+        }
+        if (settings.getRetentionDuration() == null || settings.getRetentionDuration() <= 0) {
+            log.warn("保存预订服务信息设置失败:保留时长必须大于0");
+            throw new RuntimeException("保留时长必须大于0");
+        }
+        if (settings.getBookingDateDisplayDays() == null || settings.getBookingDateDisplayDays() <= 0) {
+            log.warn("保存预订服务信息设置失败:预订日期显示天数必须大于0");
+            throw new RuntimeException("预订日期显示天数必须大于0");
+        }
+//        if (settings.getBookingTimeType() == null) {
+//            log.warn("保存预订服务信息设置失败:预订时间类型不能为空");
+//            throw new RuntimeException("预订时间类型不能为空");
+//        }
+        if (settings.getMaxCapacityPerSlot() == null || settings.getMaxCapacityPerSlot() <= 0) {
+            log.warn("保存预订服务信息设置失败:单时段最大容纳人数必须大于0");
+            throw new RuntimeException("单时段最大容纳人数必须大于0");
+        }
+        
+        // 验证预订相关字段
+        // 如果选择付费(reservation=1),必须填写预订金额
+        if (StringUtils.hasText(settings.getReservation()) && "1".equals(settings.getReservation())) {
+            if (settings.getReservationMoney() == null || settings.getReservationMoney() < 0) {
+                log.warn("保存预订服务信息设置失败:选择付费时必须填写预订金额,且金额不能小于0");
+                throw new RuntimeException("选择付费时必须填写预订金额,且金额不能小于0");
+            }
+        } else if (StringUtils.hasText(settings.getReservation()) && "0".equals(settings.getReservation())) {
+            // 如果选择免费,清空预订金额
+            settings.setReservationMoney(null);
+        }
+        
+        // 验证取消预订退费时长设置
+        if (settings.getOffUnsubscribeHours() == null || settings.getOffUnsubscribeHours() < 0) {
+            log.warn("保存预订服务信息设置失败:取消预订退费时长设置必须大于等于0");
+            throw new RuntimeException("取消预订退费时长设置必须大于等于0");
+        }
+        
+        // 验证营业时间结束前不可预订时间
+        if (settings.getBookingNotAvailableTime() == null || settings.getBookingNotAvailableTime() < 0) {
+            log.warn("保存预订服务信息设置失败:营业时间结束前不可预订时间必须大于等于0");
+            throw new RuntimeException("营业时间结束前不可预订时间必须大于等于0");
+        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+//        if (settings.getBookingTimeType() != null && settings.getBookingTimeType() == 0) {
+//            if (!StringUtils.hasText(settings.getBookingStartTime())) {
+//                log.warn("保存预订服务信息设置失败:非全天时必须填写开始时间");
+//                throw new RuntimeException("非全天时必须填写开始时间");
+//            }
+//            if (!StringUtils.hasText(settings.getBookingEndTime())) {
+//                log.warn("保存预订服务信息设置失败:非全天时必须填写结束时间");
+//                throw new RuntimeException("非全天时必须填写结束时间");
+//            }
+//            // 验证时间格式
+//            if (!TIME_PATTERN.matcher(settings.getBookingStartTime()).matches()) {
+//                log.warn("保存预订服务信息设置失败:开始时间格式不正确,应为HH:mm格式");
+//                throw new RuntimeException("开始时间格式不正确,应为HH:mm格式");
+//            }
+//            if (!TIME_PATTERN.matcher(settings.getBookingEndTime()).matches()) {
+//                log.warn("保存预订服务信息设置失败:结束时间格式不正确,应为HH:mm格式");
+//                throw new RuntimeException("结束时间格式不正确,应为HH:mm格式");
+//            }
+//            // 验证开始时间必须小于结束时间
+//            if (settings.getBookingStartTime().compareTo(settings.getBookingEndTime()) >= 0) {
+//                log.warn("保存预订服务信息设置失败:开始时间必须小于结束时间");
+//                throw new RuntimeException("开始时间必须小于结束时间");
+//            }
+//        } else {
+//            // 如果是全天,清空开始时间和结束时间
+//            settings.setBookingStartTime(null);
+//            settings.setBookingEndTime(null);
+//        }
+        
+        // 查询是否已存在该门店的设置
+        StoreBookingSettings existingSettings = getByStoreId(settings.getStoreId());
+        
+        if (existingSettings != null) {
+            // 更新
+            settings.setId(existingSettings.getId());
+            LambdaUpdateWrapper<StoreBookingSettings> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreBookingSettings::getId, existingSettings.getId());
+            
+            updateWrapper.set(StoreBookingSettings::getRetainPositionFlag, settings.getRetainPositionFlag());
+            updateWrapper.set(StoreBookingSettings::getRetentionDuration, settings.getRetentionDuration());
+            updateWrapper.set(StoreBookingSettings::getBookingDateDisplayDays, settings.getBookingDateDisplayDays());
+            updateWrapper.set(StoreBookingSettings::getBookingTimeType, settings.getBookingTimeType());
+            updateWrapper.set(StoreBookingSettings::getBookingStartTime, settings.getBookingStartTime());
+            updateWrapper.set(StoreBookingSettings::getBookingEndTime, settings.getBookingEndTime());
+            updateWrapper.set(StoreBookingSettings::getMaxCapacityPerSlot, settings.getMaxCapacityPerSlot());
+            updateWrapper.set(StoreBookingSettings::getReservation, settings.getReservation());
+            updateWrapper.set(StoreBookingSettings::getReservationMoney, settings.getReservationMoney());
+            updateWrapper.set(StoreBookingSettings::getOffUnsubscribeHours, settings.getOffUnsubscribeHours());
+            updateWrapper.set(StoreBookingSettings::getBookingNotAvailableTime, settings.getBookingNotAvailableTime());
+            updateWrapper.set(StoreBookingSettings::getImgUrl, settings.getImgUrl());
+            
+            if (userId != null) {
+                updateWrapper.set(StoreBookingSettings::getUpdatedUserId, userId);
+            }
+            
+            return this.update(updateWrapper);
+        } else {
+            // 新增
+            settings.setCreatedUserId(userId);
+            return this.save(settings);
+        }
+    }
+
+    @Override
+    public boolean saveSettingsWithBusinessHours(StoreBookingSettingsDTO dto) {
+        log.info("StoreBookingSettingsServiceImpl.saveSettingsWithBusinessHours?dto={}", dto);
+        
+        // 1. 先保存或更新设置信息
+        StoreBookingSettings settings = new StoreBookingSettings();
+        settings.setId(dto.getId());
+        settings.setStoreId(dto.getStoreId());
+        settings.setRetainPositionFlag(dto.getRetainPositionFlag());
+        settings.setRetentionDuration(dto.getRetentionDuration());
+        settings.setBookingDateDisplayDays(dto.getBookingDateDisplayDays());
+        settings.setBookingTimeType(dto.getBookingTimeType());
+        settings.setBookingStartTime(dto.getBookingStartTime());
+        settings.setBookingEndTime(dto.getBookingEndTime());
+        settings.setMaxCapacityPerSlot(dto.getMaxCapacityPerSlot());
+        settings.setReservation(dto.getReservation());
+        settings.setReservationMoney(dto.getReservationMoney());
+        settings.setOffUnsubscribeHours(dto.getOffUnsubscribeHours());
+        settings.setBookingNotAvailableTime(dto.getBookingNotAvailableTime());
+        settings.setImgUrl(dto.getImgUrl());
+        
+        boolean settingsResult = saveOrUpdateSettings(settings);
+        if (!settingsResult) {
+            log.error("保存预订服务信息设置失败");
+            throw new RuntimeException("保存预订服务信息设置失败");
+        }
+        
+        // 2. 获取保存后的设置ID(如果是新增,需要获取新生成的ID)
+        Integer settingsId = settings.getId();
+        if (settingsId == null) {
+            // 如果是新增,重新查询获取ID
+            StoreBookingSettings savedSettings = getByStoreId(dto.getStoreId());
+            if (savedSettings == null) {
+                log.error("保存设置后无法获取设置ID");
+                throw new RuntimeException("保存设置后无法获取设置ID");
+            }
+            settingsId = savedSettings.getId();
+        }
+        
+        // 3. 准备营业时间列表
+        List<StoreBookingBusinessHours> businessHoursList = new ArrayList<>();
+        
+        // 3.1 处理正常营业时间(business_type=1)
+        if (dto.getNormalBusinessHours() != null) {
+            StoreBookingBusinessHours normalHours = convertToBusinessHours(dto.getNormalBusinessHours(), settingsId, 1);
+            businessHoursList.add(normalHours);
+        }
+        
+        // 3.2 处理特殊营业时间列表(business_type=2)
+        if (dto.getSpecialBusinessHoursList() != null && !dto.getSpecialBusinessHoursList().isEmpty()) {
+            int sort = 1;
+            for (StoreBookingBusinessHoursDTO specialDto : dto.getSpecialBusinessHoursList()) {
+                StoreBookingBusinessHours specialHours = convertToBusinessHours(specialDto, settingsId, 2);
+                specialHours.setSort(sort++);
+                businessHoursList.add(specialHours);
+            }
+        }
+        
+        // 4. 批量保存营业时间(如果列表不为空)
+        if (!businessHoursList.isEmpty()) {
+            boolean businessHoursResult = storeBookingBusinessHoursService.batchSaveBusinessHours(settingsId, businessHoursList);
+            if (!businessHoursResult) {
+                log.error("保存营业时间失败");
+                throw new RuntimeException("保存营业时间失败");
+            }
+        }
+        
+        return true;
+    }
+
+    @Override
+    public StoreBookingSettingsDTO getSettingsWithBusinessHoursByStoreId(Integer storeId) {
+        log.info("StoreBookingSettingsServiceImpl.getSettingsWithBusinessHoursByStoreId?storeId={}", storeId);
+        
+        if (storeId == null) {
+            log.warn("查询预订服务信息设置失败:门店ID不能为空");
+            return null;
+        }
+        
+        // 1. 查询设置信息
+        StoreBookingSettings settings = getByStoreId(storeId);
+        if (settings == null) {
+            return null;
+        }
+        
+        // 2. 转换为DTO
+        StoreBookingSettingsDTO dto = new StoreBookingSettingsDTO();
+        BeanUtils.copyProperties(settings, dto);
+        
+        // 3. 查询营业时间列表
+        List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
+        
+        // 4. 分离正常营业时间和特殊营业时间
+        StoreBookingBusinessHours normalHours = businessHoursList.stream()
+                .filter(h -> h.getBusinessType() != null && h.getBusinessType() == 1)
+                .findFirst()
+                .orElse(null);
+        
+        List<StoreBookingBusinessHours> specialHoursList = businessHoursList.stream()
+                .filter(h -> h.getBusinessType() != null && h.getBusinessType() == 2)
+                .collect(Collectors.toList());
+        
+        // 5. 转换为DTO
+        if (normalHours != null) {
+            dto.setNormalBusinessHours(convertToBusinessHoursDTO(normalHours));
+        }
+        
+        if (!specialHoursList.isEmpty()) {
+            List<StoreBookingBusinessHoursDTO> specialHoursDTOList = specialHoursList.stream()
+                    .map(businessHours -> {
+                        StoreBookingBusinessHoursDTO hoursDTO = convertToBusinessHoursDTO(businessHours);
+                        // 如果有关联的节假日ID,查询节假日信息
+                        if (businessHours.getEssentialId() != null) {
+                            try {
+                                EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(businessHours.getEssentialId());
+                                if (holiday != null) {
+                                    hoursDTO.setHolidayInfo(holiday);
+                                }
+                            } catch (Exception e) {
+                                log.warn("查询节假日信息失败,essentialId={}", businessHours.getEssentialId(), e);
+                            }
+                        }
+                        return hoursDTO;
+                    })
+                    .collect(Collectors.toList());
+            dto.setSpecialBusinessHoursList(specialHoursDTOList);
+        }
+        
+        return dto;
+    }
+
+    /**
+     * 将DTO转换为营业时间实体
+     *
+     * @param dto DTO对象
+     * @param settingsId 设置ID
+     * @param businessType 营业类型(1:正常营业时间, 2:特殊营业时间)
+     * @return StoreBookingBusinessHours
+     */
+    private StoreBookingBusinessHours convertToBusinessHours(StoreBookingBusinessHoursDTO dto, Integer settingsId, Integer businessType) {
+        StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+        businessHours.setId(dto.getId());
+        businessHours.setSettingsId(settingsId);
+        businessHours.setBusinessType(businessType);
+        businessHours.setHolidayType(dto.getHolidayType());
+        businessHours.setHolidayDate(dto.getHolidayDate());
+        businessHours.setBookingTimeType(dto.getBookingTimeType());
+        businessHours.setStartTime(dto.getStartTime());
+        businessHours.setEndTime(dto.getEndTime());
+        businessHours.setSort(dto.getSort() != null ? dto.getSort() : 0);
+        businessHours.setEssentialId(dto.getEssentialId());
+        businessHours.setReservation(dto.getReservation());
+        businessHours.setReservationMoney(dto.getReservationMoney());
+        return businessHours;
+    }
+
+    /**
+     * 将营业时间实体转换为DTO
+     *
+     * @param businessHours 营业时间实体
+     * @return StoreBookingBusinessHoursDTO
+     */
+    private StoreBookingBusinessHoursDTO convertToBusinessHoursDTO(StoreBookingBusinessHours businessHours) {
+        StoreBookingBusinessHoursDTO dto = new StoreBookingBusinessHoursDTO();
+        dto.setId(businessHours.getId());
+        dto.setSettingsId(businessHours.getSettingsId());
+        dto.setBusinessType(businessHours.getBusinessType());
+        dto.setHolidayType(businessHours.getHolidayType());
+        dto.setHolidayDate(businessHours.getHolidayDate());
+        dto.setBookingTimeType(businessHours.getBookingTimeType());
+        dto.setStartTime(businessHours.getStartTime());
+        dto.setEndTime(businessHours.getEndTime());
+        dto.setSort(businessHours.getSort());
+        dto.setEssentialId(businessHours.getEssentialId());
+        dto.setReservation(businessHours.getReservation());
+        dto.setReservationMoney(businessHours.getReservationMoney());
+        return dto;
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 390 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingTableServiceImpl.java

@@ -0,0 +1,390 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import org.springframework.beans.BeanUtils;
+import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.vo.StoreBookingTableVo;
+import shop.alien.mapper.StoreBookingTableMapper;
+import shop.alien.store.service.StoreBookingCategoryService;
+import shop.alien.store.service.StoreBookingTableService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 预订服务桌号表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableMapper, StoreBookingTable> implements StoreBookingTableService {
+
+    private final StoreBookingCategoryService storeBookingCategoryService;
+
+    public StoreBookingTableServiceImpl(@Lazy StoreBookingCategoryService storeBookingCategoryService) {
+        this.storeBookingCategoryService = storeBookingCategoryService;
+    }
+
+    @Override
+    public List<StoreBookingTable> getTableList(Integer storeId, Integer categoryId) {
+        log.info("StoreBookingTableServiceImpl.getTableList?storeId={}, categoryId={}", storeId, categoryId);
+        
+        LambdaQueryWrapper<StoreBookingTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingTable::getStoreId, storeId);
+        
+        // 如果指定了分类ID,则按分类筛选;否则查询全部
+        if (categoryId != null) {
+            wrapper.eq(StoreBookingTable::getCategoryId, categoryId);
+        }
+        
+        // 查询所有数据,然后在Java中进行排序
+        List<StoreBookingTable> list = this.list(wrapper);
+        
+        // 自定义排序:根据类别、字母(A-Z)、数字(由小到大)顺序排列
+        list.sort(Comparator
+                .comparing(StoreBookingTable::getCategoryId, Comparator.nullsLast(Integer::compareTo)) // 先按类别排序
+                .thenComparing(table -> parseTableNumberForSort(table.getTableNumber()))); // 再按桌号排序
+        
+        return list;
+    }
+
+    @Override
+    public List<StoreBookingTableVo> getTableListWithCategoryName(Integer storeId, Integer categoryId) {
+        log.info("StoreBookingTableServiceImpl.getTableListWithCategoryName?storeId={}, categoryId={}", storeId, categoryId);
+        
+        // 先查询桌号列表
+        List<StoreBookingTable> tableList = getTableList(storeId, categoryId);
+        
+        // 查询所有分类信息,构建categoryId -> categoryName的映射
+        List<StoreBookingCategory> categoryList = storeBookingCategoryService.list(
+                new LambdaQueryWrapper<StoreBookingCategory>()
+                        .eq(StoreBookingCategory::getStoreId, storeId)
+        );
+        Map<Integer, String> categoryMap = categoryList.stream()
+                .collect(Collectors.toMap(
+                        StoreBookingCategory::getId,
+                        StoreBookingCategory::getCategoryName,
+                        (v1, v2) -> v1 // 如果有重复key,保留第一个
+                ));
+        
+        // 转换为VO并设置分类名称
+        return tableList.stream()
+                .filter(table -> table != null)
+                .map(table -> {
+                    StoreBookingTableVo vo = new StoreBookingTableVo();
+                    BeanUtils.copyProperties(table, vo);
+                    // 设置分类名称
+                    if (table.getCategoryId() != null) {
+                        vo.setCategoryName(categoryMap.get(table.getCategoryId()));
+                    }
+                    return vo;
+                })
+                .collect(Collectors.toList());
+    }
+    
+    /**
+     * 解析桌号用于排序
+     * 规则:字母(A-Z)优先,然后数字(由小到大)
+     * 排序逻辑:
+     * 1. 纯数字桌号(如:1, 2, 10)按数字从小到大排序
+     * 2. 字母桌号(如:A1, B2)按字母A-Z排序,然后按数字从小到大排序
+     * 3. 字母桌号优先于纯数字桌号
+     * 
+     * @param tableNumber 桌号
+     * @return 排序键
+     */
+    private String parseTableNumberForSort(String tableNumber) {
+        if (tableNumber == null || tableNumber.isEmpty()) {
+            return "ZZZZZZZZZZ9999999999"; // 空值排在最后
+        }
+        
+        // 去除空格并转为大写,便于比较
+        String normalized = tableNumber.trim().toUpperCase();
+        
+        // 分离字母和数字部分
+        StringBuilder letters = new StringBuilder();
+        StringBuilder numbers = new StringBuilder();
+        
+        for (char c : normalized.toCharArray()) {
+            if (Character.isLetter(c)) {
+                letters.append(c);
+            } else if (Character.isDigit(c)) {
+                numbers.append(c);
+            }
+        }
+        
+        String letterPart = letters.toString();
+        String numberPart = numbers.toString();
+        
+        // 数字部分处理:转换为整数再格式化,确保数字排序正确(如:1 < 2 < 10)
+        if (numberPart.isEmpty()) {
+            numberPart = "0000000000";
+        } else {
+            try {
+                int num = Integer.parseInt(numberPart);
+                numberPart = String.format("%010d", num); // 补零到10位,确保数字排序正确
+            } catch (NumberFormatException e) {
+                // 如果数字太大超出int范围,保持原样并补零
+                numberPart = String.format("%-10s", numberPart).replace(' ', '0');
+            }
+        }
+        
+        // 字母部分处理
+        if (letterPart.isEmpty()) {
+            // 纯数字桌号:使用特殊前缀确保排在字母桌号后面
+            // 但根据用户需求,可能需要纯数字排在前面,这里先按字母优先的逻辑
+            letterPart = "ZZZZZZZZZZ"; // 纯数字排在字母后面
+        } else {
+            // 字母桌号:补空格到固定长度,确保字母排序正确
+            letterPart = String.format("%-10s", letterPart);
+        }
+        
+        return letterPart + numberPart;
+    }
+
+    @Override
+    public boolean addTable(StoreBookingTable table) {
+        log.info("StoreBookingTableServiceImpl.addTable?table={}", table);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (table.getStoreId() == null) {
+            log.warn("新增预订服务桌号失败:门店ID不能为空");
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (table.getCategoryId() == null) {
+            log.warn("新增预订服务桌号失败:分类ID不能为空");
+            throw new RuntimeException("分类ID不能为空");
+        }
+        if (!StringUtils.hasText(table.getTableNumber())) {
+            log.warn("新增预订服务桌号失败:桌号不能为空");
+            throw new RuntimeException("桌号不能为空");
+        }
+        if (table.getSeatingCapacity() == null || table.getSeatingCapacity() <= 0) {
+            log.warn("新增预订服务桌号失败:座位数必须大于0");
+            throw new RuntimeException("座位数必须大于0");
+        }
+        
+        // 处理桌号:去除前后空格
+        String tableNumber = table.getTableNumber().trim();
+        table.setTableNumber(tableNumber);
+        
+        // 检查同一分类下是否已存在相同的桌号(不同分类可以同名)
+        LambdaQueryWrapper<StoreBookingTable> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreBookingTable::getStoreId, table.getStoreId())
+                .eq(StoreBookingTable::getCategoryId, table.getCategoryId())
+                .eq(StoreBookingTable::getTableNumber, tableNumber);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        StoreBookingTable existingTable = this.getOne(checkWrapper);
+        if (existingTable != null) {
+            log.warn("新增预订服务桌号失败:同一分类下桌号已存在,storeId={}, categoryId={}, tableNumber={}", 
+                    table.getStoreId(), table.getCategoryId(), tableNumber);
+            throw new RuntimeException("该分类下桌号已存在不能添加");
+        }
+        
+        table.setCreatedUserId(userId);
+        
+        return this.save(table);
+    }
+
+    @Override
+    public boolean batchAddTables(Integer storeId, Integer categoryId, List<StoreBookingTable> tables) {
+        log.info("StoreBookingTableServiceImpl.batchAddTables?storeId={}, categoryId={}, tables.size={}", 
+                storeId, categoryId, tables != null ? tables.size() : 0);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (storeId == null) {
+            log.warn("批量新增预订服务桌号失败:门店ID不能为空");
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (categoryId == null) {
+            log.warn("批量新增预订服务桌号失败:分类ID不能为空");
+            throw new RuntimeException("分类ID不能为空");
+        }
+        if (tables == null || tables.isEmpty()) {
+            log.warn("批量新增预订服务桌号失败:桌号列表不能为空");
+            throw new RuntimeException("桌号列表不能为空");
+        }
+        
+        // 验证并处理每个桌号
+        for (StoreBookingTable table : tables) {
+            if (!StringUtils.hasText(table.getTableNumber())) {
+                log.warn("批量新增预订服务桌号失败:桌号不能为空");
+                throw new RuntimeException("桌号不能为空");
+            }
+            if (table.getSeatingCapacity() == null || table.getSeatingCapacity() <= 0) {
+                log.warn("批量新增预订服务桌号失败:座位数必须大于0");
+                throw new RuntimeException("座位数必须大于0");
+            }
+            
+            // 处理桌号:去除前后空格
+            String tableNumber = table.getTableNumber().trim();
+            table.setTableNumber(tableNumber);
+            table.setStoreId(storeId);
+            table.setCategoryId(categoryId);
+            table.setCreatedUserId(userId);
+        }
+        
+        // 检查是否有重复的桌号(在本次批量添加的列表中)
+        long distinctCount = tables.stream()
+                .map(StoreBookingTable::getTableNumber)
+                .distinct()
+                .count();
+        if (distinctCount != tables.size()) {
+            log.warn("批量新增预订服务桌号失败:本次批量添加的桌号列表中存在重复");
+            throw new RuntimeException("本次批量添加的桌号列表中存在重复");
+        }
+        
+        // 检查同一分类下是否已存在相同的桌号(不同分类可以同名)
+        List<String> tableNumbers = tables.stream()
+                .map(StoreBookingTable::getTableNumber)
+                .collect(Collectors.toList());
+        
+        LambdaQueryWrapper<StoreBookingTable> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(StoreBookingTable::getStoreId, storeId)
+                .eq(StoreBookingTable::getCategoryId, categoryId)
+                .in(StoreBookingTable::getTableNumber, tableNumbers);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        List<StoreBookingTable> existingTables = this.list(checkWrapper);
+        
+        if (!existingTables.isEmpty()) {
+            List<String> existingNumbers = existingTables.stream()
+                    .map(StoreBookingTable::getTableNumber)
+                    .distinct()
+                    .collect(Collectors.toList());
+            log.warn("批量新增预订服务桌号失败:同一分类下桌号已存在,storeId={}, categoryId={}, existingNumbers={}", 
+                    storeId, categoryId, existingNumbers);
+            if (existingNumbers.size() == 1) {
+                throw new RuntimeException("该分类下桌号已存在不能添加");
+            } else {
+                throw new RuntimeException("该分类下以下桌号已存在不能添加:" + String.join("、", existingNumbers));
+            }
+        }
+        
+        // 批量保存
+        return this.saveBatch(tables);
+    }
+
+    @Override
+    public boolean updateTable(StoreBookingTable table) {
+        log.info("StoreBookingTableServiceImpl.updateTable?table={}", table);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (table.getId() == null) {
+            log.warn("更新预订服务桌号失败:桌号ID不能为空");
+            throw new RuntimeException("桌号ID不能为空");
+        }
+        
+        StoreBookingTable existingTable = this.getById(table.getId());
+        if (existingTable == null) {
+            log.warn("更新预订服务桌号失败:桌号不存在,id={}", table.getId());
+            throw new RuntimeException("桌号不存在");
+        }
+        
+        // 如果修改了桌号,检查新桌号是否已存在
+        if (StringUtils.hasText(table.getTableNumber())) {
+            // 处理桌号:去除前后空格
+            String tableNumber = table.getTableNumber().trim();
+            table.setTableNumber(tableNumber);
+            
+            // 如果桌号有变化,检查同一分类下是否已存在相同的桌号(不同分类可以同名,排除当前记录)
+            if (!tableNumber.equals(existingTable.getTableNumber())) {
+                // 如果分类ID也变化了,使用新的分类ID;否则使用原有的分类ID
+                Integer checkCategoryId = table.getCategoryId() != null ? table.getCategoryId() : existingTable.getCategoryId();
+                
+                LambdaQueryWrapper<StoreBookingTable> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(StoreBookingTable::getStoreId, existingTable.getStoreId())
+                        .eq(StoreBookingTable::getCategoryId, checkCategoryId)
+                        .eq(StoreBookingTable::getTableNumber, tableNumber)
+                        .ne(StoreBookingTable::getId, table.getId());
+                // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+                StoreBookingTable duplicateTable = this.getOne(wrapper);
+                if (duplicateTable != null) {
+                    log.warn("更新预订服务桌号失败:同一分类下桌号已存在,storeId={}, categoryId={}, tableNumber={}, id={}", 
+                            existingTable.getStoreId(), checkCategoryId, tableNumber, table.getId());
+                    throw new RuntimeException("该分类下桌号已存在不能编辑");
+                }
+            }
+        }
+        
+        // 验证座位数
+        if (table.getSeatingCapacity() != null && table.getSeatingCapacity() <= 0) {
+            log.warn("更新预订服务桌号失败:座位数必须大于0");
+            throw new RuntimeException("座位数必须大于0");
+        }
+        
+        // 更新字段
+        LambdaUpdateWrapper<StoreBookingTable> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreBookingTable::getId, table.getId());
+        
+        if (StringUtils.hasText(table.getTableNumber())) {
+            updateWrapper.set(StoreBookingTable::getTableNumber, table.getTableNumber());
+        }
+        if (table.getCategoryId() != null) {
+            updateWrapper.set(StoreBookingTable::getCategoryId, table.getCategoryId());
+        }
+        if (table.getSeatingCapacity() != null) {
+            updateWrapper.set(StoreBookingTable::getSeatingCapacity, table.getSeatingCapacity());
+        }
+        
+        if (userId != null) {
+            updateWrapper.set(StoreBookingTable::getUpdatedUserId, userId);
+        }
+        
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean deleteTable(Integer id) {
+        log.info("StoreBookingTableServiceImpl.deleteTable?id={}", id);
+        
+        StoreBookingTable table = this.getById(id);
+        if (table == null) {
+            log.warn("删除预订服务桌号失败:桌号不存在,id={}", id);
+            throw new RuntimeException("桌号不存在");
+        }
+        
+        // 逻辑删除
+        return this.removeById(id);
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 75 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -256,8 +256,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
-        //营业时间
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+        //营业时间(包含节假日信息,返回结构要和 storeBookingBusinessHours 一样)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHours(id);
+        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoVos);
         //门店头像
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, id);
         StoreImg storeImg = storeImgMapper.selectOne(eq);
@@ -422,9 +423,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         // }
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
-        //营业时间
-        List<StoreBusinessInfo> storeBusinessInfoList = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id));
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoList);
+        //营业时间(包含节假日信息,返回结构要和 storeBookingBusinessHours 一样)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHours(id);
+        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoVos);
         //营业执照
         storeMainInfoVo.setBusinessLicenseList(storeImgMapper.selectList(new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 14).eq(StoreImg::getStoreId, id).eq(StoreImg::getDeleteFlag, 0)));
         //其他资质证明图片(img_type=35)
@@ -451,30 +452,35 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                         .orderByAsc(StoreBusinessInfo::getBusinessType) // 先按类型排序:1-正常时间,2-特殊时间
                         .orderByAsc(StoreBusinessInfo::getBusinessDate) // 再按日期排序
         );
-        
+
         // 转换为 VO 并关联节假日信息
+        // store_business_info 的 essential_id 关联 essential_holiday_comparison 的 id
         List<StoreBusinessInfoVo> resultList = new ArrayList<>();
         for (StoreBusinessInfo businessInfo : storeBusinessInfoList) {
             StoreBusinessInfoVo vo = new StoreBusinessInfoVo();
             // 复制基本信息
             BeanUtils.copyProperties(businessInfo, vo);
-            
-            // 如果有关联的节假日ID,查询节假日信息
+
+            // 如果有关联的节假日ID(essential_id),查询 essential_holiday_comparison 表的节假日信息
             if (businessInfo.getEssentialId() != null && !businessInfo.getEssentialId().trim().isEmpty()) {
                 try {
                     Integer essentialId = Integer.parseInt(businessInfo.getEssentialId().trim());
                     EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(essentialId);
                     if (holiday != null) {
                         vo.setHolidayInfo(holiday);
+                    } else {
+                        log.warn("门店营业时间关联的节假日信息不存在,storeId={}, essentialId={}", id, essentialId);
                     }
                 } catch (NumberFormatException e) {
                     log.warn("门店营业时间关联的节假日ID格式错误,storeId={}, essentialId={}", id, businessInfo.getEssentialId());
+                } catch (Exception e) {
+                    log.error("查询节假日信息失败,storeId={}, essentialId={}", id, businessInfo.getEssentialId(), e);
                 }
             }
-            
+
             resultList.add(vo);
         }
-        
+
         return resultList;
     }
 
@@ -489,8 +495,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         StoreMainInfoVo storeMainInfoVo = storeInfoMapper.getStoreInfo(id);
         //门店标签
         storeMainInfoVo.setStoreLabel(storeLabelMapper.selectOne(new LambdaQueryWrapper<StoreLabel>().eq(StoreLabel::getStoreId, id)));
-        //营业时间
-        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, id)));
+        //营业时间(包含节假日信息,返回结构要和 storeBookingBusinessHours 一样)
+        List<StoreBusinessInfoVo> storeBusinessInfoVos = this.getStoreInfoBusinessHours(id);
+        storeMainInfoVo.setStoreBusinessInfo(storeBusinessInfoVos);
         return storeMainInfoVo;
     }
 
@@ -6153,6 +6160,62 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     }
 
     @Override
+    public StoreInfoVo getStoreInfoVoWithDistanceFields(Integer storeId, String jingdu, String weidu) {
+        if (storeId == null) {
+            return null;
+        }
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
+        StoreInfoVo result = new StoreInfoVo();
+        BeanUtils.copyProperties(storeInfo, result);
+        String storePosition = result.getStorePosition();
+        if (StringUtils.isNotEmpty(storePosition) && storePosition.contains(",")) {
+            String[] pos = storePosition.split(",");
+            result.setStorePositionLongitude(pos[0]);
+            result.setStorePositionLatitude(pos[1]);
+            result.setLongitude(pos[0]);
+            result.setLatitude(pos[1]);
+        }
+        // 用户经纬度存在时设置与用户距离(与 getClientStoreDetail 一致)
+        if ((jingdu != null && !jingdu.isEmpty()) && (weidu != null && !weidu.isEmpty())) {
+            Double distance = storeInfoMapper.getStoreDistance(jingdu + "," + weidu, result.getId());
+            result.setDistance(distance != null ? distance : 0);
+        }
+        // 店铺到最近地铁站距离及地铁名(与 getClientStoreDetail 一致)
+        if (StringUtils.isNotEmpty(storePosition) && storePosition.contains(",")) {
+            String[] pos = storePosition.split(",");
+            JSONObject nearbySubway = gaoDeMapUtil.getNearbySubway(pos[0], pos[1]);
+            String subWayName = nearbySubway != null ? nearbySubway.getString("name") : null;
+            result.setSubwayName(subWayName);
+            String subWayJing = null;
+            String subWayWei = null;
+            if (nearbySubway != null && nearbySubway.getString("location") != null) {
+                String[] loc = nearbySubway.getString("location").split(",");
+                if (loc.length >= 2) {
+                    subWayJing = loc[0];
+                    subWayWei = loc[1];
+                }
+            }
+            if (subWayJing != null && !subWayJing.isEmpty() && subWayWei != null && !subWayWei.isEmpty()) {
+                double storeJing = Double.parseDouble(pos[0]);
+                double storeWei = Double.parseDouble(pos[1]);
+                double storeDistance2 = DistanceUtil.haversineCalculateDistance(Double.parseDouble(subWayJing), Double.parseDouble(subWayWei), storeJing, storeWei);
+                result.setDistance2(storeDistance2);
+            } else {
+                result.setDistance2(0);
+            }
+        } else {
+            result.setSubwayName(null);
+            result.setDistance2(0);
+        }
+        // 门店位置/地址(与 getClientStoreDetail 及 LifeUserStoreService 一致)
+        result.setStoreLocation(result.getStoreAddress());
+        return result;
+    }
+
+    @Override
     public StoreBusinessStatusVo getStoreBusinessStatus(String storeId) {
         log.info("StoreInfoServiceImpl.getStoreBusinessStatus?storeId={}", storeId);
         StoreBusinessStatusVo result = new StoreBusinessStatusVo();

+ 168 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigCertServiceImpl.java

@@ -0,0 +1,168 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.store.service.StoreInfoService;
+import shop.alien.store.service.StorePaymentConfigCertService;
+import shop.alien.store.service.StorePaymentConfigService;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 店铺支付证书落盘服务实现:删除店铺证书目录下原有文件后重新写入,并将完整证书路径回写配置表。
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StorePaymentConfigCertServiceImpl implements StorePaymentConfigCertService {
+
+    private static final Pattern NON_ALNUM = Pattern.compile("[^0-9a-zA-Z]");
+
+    @Value("${payment.cert.base-path:${user.dir}/payment-certs}")
+    private String certBasePath;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final StoreInfoService storeInfoService;
+
+    @Override
+    public List<String> refreshCertByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return new ArrayList<>();
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            log.warn("店铺支付配置不存在 storeId={}", storeId);
+            return new ArrayList<>();
+        }
+        try {
+            return writeCertFilesForConfigWithClean(config);
+        } catch (Exception e) {
+            log.error("店铺证书落盘失败 storeId={}", storeId, e);
+            throw new RuntimeException("证书落盘失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 先清空该店铺证书目录(删除目录下所有文件及目录本身再重建),再写入证书并回写路径。
+     */
+    private List<String> writeCertFilesForConfigWithClean(StorePaymentConfig config) throws Exception {
+        Integer storeId = config.getStoreId();
+        if (storeId == null) {
+            return new ArrayList<>();
+        }
+        String storeTel = null;
+        try {
+            StoreInfo store = storeInfoService.getById(storeId);
+            if (store != null && StringUtils.hasText(store.getStoreTel())) {
+                storeTel = store.getStoreTel();
+            }
+        } catch (Exception e) {
+            log.debug("获取店铺手机号失败 storeId={}", storeId, e);
+        }
+        String dirSegment = sanitizeDirSegment(storeTel);
+        String storeDirName = storeId + "_" + dirSegment;
+        Path baseDir = Paths.get(certBasePath).resolve(storeDirName);
+
+        // 删除店铺证书目录下原有内容:若目录存在则删除后重建
+        if (Files.exists(baseDir)) {
+            deleteDirectoryRecursively(baseDir);
+        }
+        Files.createDirectories(baseDir);
+
+        List<String> writtenPaths = new ArrayList<>();
+
+        // 支付宝:应用公钥、支付宝公钥、根证书
+        writeIfPresent(baseDir, config.getAppPublicCert(),
+                defaultName(config.getAppPublicCertName(), "app_public_cert.crt"),
+                (path, name) -> {
+                    config.setAppPublicCertPath(path);
+                    config.setAppPublicCertName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getAlipayPublicCert(),
+                defaultName(config.getAlipayPublicCertName(), "alipay_public_cert.crt"),
+                (path, name) -> {
+                    config.setAlipayPublicCertPath(path);
+                    config.setAlipayPublicCertName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getAlipayRootCert(),
+                defaultName(config.getAlipayRootCertName(), "alipay_root_cert.crt"),
+                (path, name) -> {
+                    config.setAlipayRootCertPath(path);
+                    config.setAlipayRootCertName(name);
+                }, writtenPaths);
+
+        // 微信:商户私钥、微信公钥
+        writeIfPresent(baseDir, config.getWechatPrivateKeyFile(),
+                defaultName(config.getWechatPrivateKeyName(), "wechat_private_key.pem"),
+                (path, name) -> {
+                    config.setWechatPrivateKeyPath(path);
+                    config.setWechatPrivateKeyName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getWechatPayPublicKeyFile(),
+                defaultName(config.getWechatPayPublicKeyFileName(), "wechat_public_key.pem"),
+                (path, name) -> {
+                    config.setWechatPayPublicKeyFilePath(path);
+                    config.setWechatPayPublicKeyFileName(name);
+                }, writtenPaths);
+
+        storePaymentConfigService.updateById(config);
+        log.info("店铺支付证书已刷新落盘 storeId={}, dir={}, 写入文件数={}", storeId, baseDir.toAbsolutePath(), writtenPaths.size());
+        return writtenPaths;
+    }
+
+    private static void deleteDirectoryRecursively(Path path) throws Exception {
+        if (!Files.exists(path)) {
+            return;
+        }
+        if (Files.isDirectory(path)) {
+            try (Stream<Path> stream = Files.list(path)) {
+                for (Path entry : stream.collect(Collectors.toList())) {
+                    deleteDirectoryRecursively(entry);
+                }
+            }
+        }
+        Files.delete(path);
+    }
+
+    private static String sanitizeDirSegment(String storeTel) {
+        if (!StringUtils.hasText(storeTel)) {
+            return "no_tel";
+        }
+        return NON_ALNUM.matcher(storeTel.trim()).replaceAll("_");
+    }
+
+    private static String defaultName(String name, String defaultName) {
+        return StringUtils.hasText(name) ? name.trim() : defaultName;
+    }
+
+    private interface PathUpdater {
+        void set(String certFilePath, String fileName);
+    }
+
+    private void writeIfPresent(Path baseDir, byte[] content, String fileName, PathUpdater updater, List<String> writtenPaths) throws Exception {
+        if (content == null || content.length == 0) {
+            return;
+        }
+        Path file = baseDir.resolve(fileName);
+        Files.write(file, content);
+        String certFilePath = file.toAbsolutePath().toString();
+        writtenPaths.add(certFilePath);
+        if (updater != null) {
+            updater.set(certFilePath, fileName);
+        }
+    }
+}

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

@@ -0,0 +1,1279 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreBookingTable;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationTable;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.EssentialHolidayComparison;
+import shop.alien.entity.store.vo.StoreBusinessInfoVo;
+import shop.alien.mapper.StoreReservationMapper;
+import shop.alien.mapper.UserReservationTableMapper;
+import shop.alien.mapper.EssentialHolidayComparisonMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.listener.RedisKeyExpirationHandler;
+import shop.alien.store.service.StoreBookingTableService;
+import shop.alien.store.service.StoreInfoService;
+import shop.alien.store.service.StoreReservationService;
+import shop.alien.store.service.UserReservationOrderService;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+import shop.alien.store.service.StoreBookingSettingsService;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+import shop.alien.store.util.ali.AliSms;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import shop.alien.entity.store.LifeNotice;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.mapper.LifeUserMapper;
+import org.springframework.util.StringUtils;
+import javax.annotation.PostConstruct;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 商家端预约管理 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMapper, UserReservation> implements StoreReservationService {
+
+    private final UserReservationOrderService userReservationOrderService;
+    private final BaseRedisService baseRedisService;
+    private final RedisKeyExpirationHandler expirationHandler;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final AliSms aliSms;
+    private final StoreInfoService storeInfoService;
+    private final StoreBookingTableService storeBookingTableService;
+    private final UserReservationTableMapper userReservationTableMapper;
+    private final LifeNoticeMapper lifeNoticeMapper;
+    private final LifeUserMapper lifeUserMapper;
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+    private final StoreBookingSettingsService storeBookingSettingsService;
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+
+    /** 预约状态:已取消 */
+    private static final int STATUS_CANCELLED = 3;
+    /** Redis key前缀:预订核销 */
+    private static final String RESERVATION_VERIFY_PREFIX = "reservation:verify:";
+
+    /**
+     * 初始化时注册预订核销过期处理器
+     */
+    @PostConstruct
+    public void initReservationExpirationHandler() {
+        // 注册预订核销过期处理器
+        expirationHandler.registerHandler(RESERVATION_VERIFY_PREFIX, this::handleExpiredReservationKey);
+        log.info("预订核销过期处理器注册完成,前缀: {}", RESERVATION_VERIFY_PREFIX);
+    }
+
+    @Override
+    public List<StoreReservationListVo> getStoreReservationList(Integer storeId, Integer status, Date dateFrom, Date dateTo, Integer orderStatus) {
+        log.info("StoreReservationServiceImpl.getStoreReservationList?storeId={}, status={}, dateFrom={}, dateTo={}, orderStatus={}",
+                storeId, status, dateFrom, dateTo, orderStatus);
+
+        if (storeId == null) {
+            throw new RuntimeException("门店ID不能为空");
+        }
+
+        return baseMapper.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
+    }
+
+    @Override
+    public boolean cancelReservationByStore(Integer reservationId, String cancelReason) {
+        log.info("StoreReservationServiceImpl.cancelReservationByStore?reservationId={}&cancelReason={}", reservationId, cancelReason);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 检查预约状态,已取消的不能再次取消
+        if (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
+            throw new RuntimeException("预约已取消,不能重复取消");
+        }
+
+        // 校验取消原因(如果提供,限30字)
+        if (cancelReason != null && cancelReason.length() > 30) {
+            throw new RuntimeException("取消原因不能超过30字");
+        }
+        // 如果没有提供取消原因,使用默认值
+        if (cancelReason == null || cancelReason.trim().isEmpty()) {
+            cancelReason = "商家取消";
+        }
+
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            // 如果没有订单,直接更新预约状态为3(已取消)
+            reservation.setStatus(STATUS_CANCELLED);
+            reservation.setReason(cancelReason); // 保存取消原因
+            boolean updateResult = this.updateById(reservation);
+            if (!updateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+            log.info("商家端取消预约成功(无订单),reservationId={}", reservationId);
+            
+            // 发送短信通知
+            sendCancelReservationSms(reservation, cancelReason);
+            return true;
+        }
+
+        // 判断订单费用类型:0-免费, 1-收费
+        Integer orderCostType = order.getOrderCostType();
+        if (orderCostType == null) {
+            orderCostType = 0;
+        }
+
+        if (orderCostType == 0) {
+            // 免费订单:更新订单状态为4(已取消),更新预约状态为3(已取消)
+            order.setOrderStatus(4); // 4:已取消
+            boolean orderUpdateResult = userReservationOrderService.updateById(order);
+            if (!orderUpdateResult) {
+                throw new RuntimeException("更新订单状态失败");
+            }
+
+            reservation.setStatus(STATUS_CANCELLED);
+            reservation.setReason(cancelReason); // 保存取消原因
+            boolean reservationUpdateResult = this.updateById(reservation);
+            if (!reservationUpdateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+
+            log.info("商家端取消预约成功(免费订单),reservationId={}, orderId={}", reservationId, order.getId());
+            
+            // 发送短信通知
+            sendCancelReservationSms(reservation, cancelReason);
+            return true;
+        } else if (orderCostType == 1) {
+            // 付费订单:调用退款接口,然后更新状态
+            try {
+                // 调用退款接口(和核销中的退款逻辑一样)
+                MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
+                strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "商家取消预约退款", 1);
+                
+                // 退款成功后,更新订单状态为已退款(7)
+//                order.setOrderStatus(7); // 7:已退款
+//                order.setPaymentStatus(2); // 2:已退款
+//                order.setRefundAmount(order.getDepositAmount());
+//                order.setRefundTime(new Date());
+//                order.setRefundReason("商家取消预约退款");
+//                order.setRefundType(1); // 1:商家退款
+//                boolean orderUpdateResult = userReservationOrderService.updateById(order);
+//                if (!orderUpdateResult) {
+//                    throw new RuntimeException("更新订单状态失败");
+//                }
+
+                // 更新预约状态为已取消(3)
+                reservation.setStatus(STATUS_CANCELLED);
+                reservation.setReason(cancelReason); // 保存取消原因
+                boolean reservationUpdateResult = this.updateById(reservation);
+                if (!reservationUpdateResult) {
+                    throw new RuntimeException("更新预约状态失败");
+                }
+
+                log.info("商家端取消预约成功(付费订单),reservationId={}, orderId={}", reservationId, order.getId());
+                
+                // 发送短信通知
+                sendCancelReservationSms(reservation, cancelReason);
+                return true;
+            } catch (Exception e) {
+                log.error("付费订单取消退款失败,reservationId={}, orderId={}, error={}", 
+                        reservationId, order.getId(), e.getMessage(), e);
+                throw new RuntimeException("付费订单取消退款失败:" + e.getMessage());
+            }
+        } else {
+            throw new RuntimeException("订单费用类型异常,orderCostType=" + orderCostType);
+        }
+    }
+
+    /**
+     * 发送商家取消预约短信通知
+     */
+    private void sendCancelReservationSms(UserReservation reservation, String cancelReason) {
+        try {
+            // 获取用户手机号
+            String phone = reservation.getReservationUserPhone();
+            if (phone == null || phone.trim().isEmpty()) {
+                log.warn("预约人电话为空,无法发送短信通知,reservationId={}", reservation.getId());
+                return;
+            }
+
+            // 查询店铺信息
+            StoreInfo storeInfo = storeInfoService.getById(reservation.getStoreId());
+            String storeName = storeInfo != null ? storeInfo.getStoreName() : "未知店铺";
+            if (storeName == null || storeName.trim().isEmpty()) {
+                storeName = "未知店铺";
+            }
+
+            // 查询桌号信息
+            LambdaQueryWrapper<UserReservationTable> tableWrapper = new LambdaQueryWrapper<>();
+            tableWrapper.eq(UserReservationTable::getReservationId, reservation.getId())
+                    .eq(UserReservationTable::getDeleteFlag, 0)
+                    .orderByAsc(UserReservationTable::getSort);
+            List<UserReservationTable> reservationTables = userReservationTableMapper.selectList(tableWrapper);
+            
+            String tableNumber = "未知桌号";
+            if (reservationTables != null && !reservationTables.isEmpty()) {
+                // 获取所有桌号,用逗号分隔
+                List<String> tableNumbers = reservationTables.stream()
+                        .map(rt -> {
+                            StoreBookingTable table = storeBookingTableService.getById(rt.getTableId());
+                            return table != null && table.getTableNumber() != null ? table.getTableNumber() : null;
+                        })
+                        .filter(tn -> tn != null && !tn.trim().isEmpty())
+                        .collect(Collectors.toList());
+                
+                if (!tableNumbers.isEmpty()) {
+                    tableNumber = String.join(",", tableNumbers);
+                }
+            }
+
+            // 格式化预约时间:日期 + 时间(格式:2026-01-01 14:00)
+            String dateTime = formatReservationDateTime(reservation);
+            
+            // 构建通知内容(和短信内容一样)
+            // 短信模板:您在${dateTime},预订了${storeName},的${number}桌位,已被商家取消,取消原因:${info},请您重新预订
+            String noticeMessage = String.format("您在%s,预订了%s,的%s桌位,已被商家取消,取消原因:%s,请您重新预订", 
+                    dateTime, storeName, tableNumber, cancelReason);
+            
+            // 调用发送短信接口
+            Integer smsResult = aliSms.sendCancelReservationSms(phone, dateTime, storeName, tableNumber, cancelReason);
+            if (smsResult != null && smsResult == 1) {
+                log.info("商家取消预约短信发送成功,reservationId={}, phone={}", reservation.getId(), phone);
+            } else {
+                log.warn("商家取消预约短信发送失败,reservationId={}, phone={}", reservation.getId(), phone);
+            }
+            
+            // 发送通知(和短信内容一样)
+            sendCancelReservationNotice(reservation, noticeMessage);
+        } catch (Exception e) {
+            // 短信和通知发送失败不影响取消预约流程,只记录日志
+            log.error("发送商家取消预约短信和通知异常,reservationId={}", reservation.getId(), e);
+        }
+    }
+
+    /**
+     * 发送商家取消预约通知
+     */
+    private void sendCancelReservationNotice(UserReservation reservation, String noticeMessage) {
+        try {
+            // 通过 userId 查询 life_user 表获取手机号
+            Integer userId = reservation.getUserId();
+            if (userId == null) {
+                log.warn("预约用户ID为空,无法发送通知,reservationId={}", reservation.getId());
+                return;
+            }
+            
+            // 查询用户信息
+            LifeUser lifeUser = lifeUserMapper.selectById(userId);
+            if (lifeUser == null) {
+                log.warn("未找到用户信息,无法发送通知,reservationId={}, userId={}", reservation.getId(), userId);
+                return;
+            }
+            
+            // 获取用户手机号
+            String userPhone = lifeUser.getUserPhone();
+            if (userPhone == null || userPhone.trim().isEmpty()) {
+                log.warn("用户手机号为空,无法发送通知,reservationId={}, userId={}", reservation.getId(), userId);
+                return;
+            }
+            
+            // 构建receiverId:用户端使用 "user_" + 手机号
+            String receiverId = "user_" + userPhone.trim();
+            
+            // 构建通知内容JSON
+            JSONObject contextJson = new JSONObject();
+            contextJson.put("message", noticeMessage);
+            contextJson.put("reservationId", reservation.getId());
+            contextJson.put("reservationNo", reservation.getReservationNo());
+            
+            // 创建通知记录
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setReceiverId(receiverId);
+            lifeNotice.setBusinessId(reservation.getId());
+            lifeNotice.setTitle("订单取消提醒");
+            lifeNotice.setContext(contextJson.toJSONString());
+            lifeNotice.setNoticeType(2); // 2-订单提醒
+            lifeNotice.setIsRead(0);
+            
+            // 保存通知到数据库
+            lifeNoticeMapper.insert(lifeNotice);
+            
+            log.info("商家取消预约通知发送成功,reservationId={}, userId={}, receiverId={}", 
+                    reservation.getId(), userId, receiverId);
+        } catch (Exception e) {
+            // 通知发送失败不影响流程,只记录日志
+            log.error("发送商家取消预约通知异常,reservationId={}, userId={}", 
+                    reservation.getId(), reservation.getUserId(), e);
+        }
+    }
+
+    /**
+     * 发送订金退款短信和通知
+     *
+     * @param reservation 预约信息
+     * @param order 订单信息
+     */
+    private void sendDepositRefundSmsAndNotice(UserReservation reservation, UserReservationOrder order) {
+        try {
+            // 获取用户手机号
+            String userPhone = reservation.getReservationUserPhone();
+            if (!StringUtils.hasText(userPhone)) {
+                log.warn("核销后发送订金退款短信和通知,用户手机号为空,无法发送,reservationId={}", reservation.getId());
+                return;
+            }
+
+            // 短信和通知内容
+            String message = "您预付的订金已返还至您的支付账号,请注意查收.";
+
+            // 发送短信
+            Integer smsResult = aliSms.sendDepositRefundSms(userPhone);
+            if (smsResult != null && smsResult == 1) {
+                log.info("核销后订金退款短信发送成功,reservationId={}, phone={}", reservation.getId(), userPhone);
+            } else {
+                log.warn("核销后订金退款短信发送失败,reservationId={}, phone={}", reservation.getId(), userPhone);
+            }
+
+            // 发送通知
+            sendDepositRefundNotice(reservation, message);
+        } catch (Exception e) {
+            log.error("发送订金退款短信和通知异常,reservationId={}", reservation.getId(), e);
+            throw e;
+        }
+    }
+
+    /**
+     * 发送订金退款通知
+     *
+     * @param reservation 预约信息
+     * @param noticeMessage 通知内容
+     */
+    private void sendDepositRefundNotice(UserReservation reservation, String noticeMessage) {
+        try {
+            // 通过 userId 查询 life_user 表获取手机号
+            Integer userId = reservation.getUserId();
+            if (userId == null) {
+                log.warn("预约用户ID为空,无法发送通知,reservationId={}", reservation.getId());
+                return;
+            }
+            
+            // 查询用户信息
+            LifeUser lifeUser = lifeUserMapper.selectById(userId);
+            if (lifeUser == null) {
+                log.warn("未找到用户信息,无法发送通知,reservationId={}, userId={}", reservation.getId(), userId);
+                return;
+            }
+            
+            // 获取用户手机号
+            String userPhone = lifeUser.getUserPhone();
+            if (userPhone == null || userPhone.trim().isEmpty()) {
+                log.warn("用户手机号为空,无法发送通知,reservationId={}, userId={}", reservation.getId(), userId);
+                return;
+            }
+            
+            // 构建receiverId:用户端使用 "user_" + 手机号
+            String receiverId = "user_" + userPhone.trim();
+            
+            // 构建通知内容JSON
+            JSONObject contextJson = new JSONObject();
+            contextJson.put("message", noticeMessage);
+            contextJson.put("reservationId", reservation.getId());
+            contextJson.put("reservationNo", reservation.getReservationNo());
+            
+            // 创建通知记录
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setReceiverId(receiverId);
+            lifeNotice.setBusinessId(reservation.getId());
+            lifeNotice.setTitle("退款成功通知");
+            lifeNotice.setContext(contextJson.toJSONString());
+            lifeNotice.setNoticeType(2); // 2-订单提醒
+            lifeNotice.setIsRead(0);
+            
+            // 保存通知到数据库
+            lifeNoticeMapper.insert(lifeNotice);
+            
+            log.info("核销后订金退款通知发送成功,reservationId={}, userId={}, receiverId={}", 
+                    reservation.getId(), userId, receiverId);
+        } catch (Exception e) {
+            log.error("发送订金退款通知异常,reservationId={}, userId={}", 
+                    reservation.getId(), reservation.getUserId(), e);
+            throw e;
+        }
+    }
+
+    /**
+     * 格式化预约时间:日期 + 时间(格式:2026-01-01 14:00)
+     */
+    private String formatReservationDateTime(UserReservation reservation) {
+        try {
+            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+            
+            String dateStr = "";
+            if (reservation.getReservationDate() != null) {
+                dateStr = dateFormat.format(reservation.getReservationDate());
+            }
+            
+            String timeStr = "";
+            if (reservation.getStartTime() != null && !reservation.getStartTime().trim().isEmpty()) {
+                timeStr = reservation.getStartTime();
+            }
+            
+            if (dateStr.isEmpty() && timeStr.isEmpty()) {
+                return "未知时间";
+            } else if (dateStr.isEmpty()) {
+                return timeStr;
+            } else if (timeStr.isEmpty()) {
+                return dateStr;
+            } else {
+                return dateStr + " " + timeStr;
+            }
+        } catch (Exception e) {
+            log.error("格式化预约时间失败,reservationId={}", reservation.getId(), e);
+            return "未知时间";
+        }
+    }
+
+    @Override
+    public boolean deleteReservationByStore(Integer reservationId) {
+        log.info("StoreReservationServiceImpl.deleteReservationByStore?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            // 如果没有订单,直接删除预约记录
+            boolean deleteResult = this.removeById(reservationId);
+            if (!deleteResult) {
+                throw new RuntimeException("删除预约记录失败");
+            }
+            log.info("商家端删除预订信息成功(无订单),reservationId={}", reservationId);
+            return true;
+        }
+
+        // 判断订单状态:只有已取消(4)、已退款(7)、已完成(2)状态才能删除
+        Integer orderStatus = order.getOrderStatus();
+        if (orderStatus == null) {
+            throw new RuntimeException("订单状态异常,无法删除");
+        }
+
+        // 定义可删除的订单状态:2:已完成, 4:已取消, 7:已退款
+        boolean canDelete = orderStatus == 2 || orderStatus == 4 || orderStatus == 7;
+
+        if (!canDelete) {
+            String statusText = getOrderStatusText(orderStatus);
+            throw new RuntimeException("订单状态为" + statusText + ",不允许删除。只有已取消、已退款、已完成状态的订单可以删除");
+        }
+
+        // 删除订单记录(逻辑删除)
+        boolean orderDeleteResult = userReservationOrderService.removeById(order.getId());
+        if (!orderDeleteResult) {
+            throw new RuntimeException("删除订单记录失败");
+        }
+
+        // 删除预约记录(逻辑删除)
+        boolean reservationDeleteResult = this.removeById(reservationId);
+        if (!reservationDeleteResult) {
+            throw new RuntimeException("删除预约记录失败");
+        }
+
+        log.info("商家端删除预订信息成功,reservationId={}, orderId={}, orderStatus={}",
+                reservationId, order.getId(), orderStatus);
+        return true;
+    }
+
+    @Override
+    public boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes) {
+        log.info("StoreReservationServiceImpl.addTimeByStore?reservationId={}, addTimeStart={}, addTimeMinutes={}",
+                reservationId, addTimeStart, addTimeMinutes);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        if (addTimeStart == null || addTimeStart.trim().isEmpty()) {
+            throw new RuntimeException("加时开始时间不能为空");
+        }
+
+        if (addTimeMinutes == null || addTimeMinutes <= 0) {
+            throw new RuntimeException("加时分钟数必须大于0");
+        }
+
+        // 验证并标准化时间格式(支持 yyyy-MM-dd HH:mm:ss 和 yyyy-MM-dd HH:mm)
+        String normalizedAddTimeStart = normalizeDateTime(addTimeStart);
+        if (normalizedAddTimeStart == null) {
+            throw new RuntimeException("加时开始时间格式错误,应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd HH:mm格式,如:2026-03-11 13:00:00 或 2026-03-11 13:00");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 保存原结束时间用于日志
+        String oldEndTime = reservation.getEndTime();
+
+        // 标准化预约的结束时间
+        String normalizedEndTime = normalizeDateTime(reservation.getEndTime());
+        if (normalizedEndTime == null) {
+            throw new RuntimeException("预约结束时间格式错误,无法进行加时操作");
+        }
+
+        // 计算新的结束时间
+        // 无论加时开始时间是否超过预订结束时间,都从预订结束时间开始累加
+        // 如果加时开始时间 > 预订结束时间,中间的空挡时间也要累加进去
+        // 例如:预订结束时间 12:00,加时开始时间 13:00,加时30分钟
+        // 新结束时间 = 加时开始时间 + 加时分钟数 = 13:00 + 30分钟 = 13:30
+        // (这已经包含了空挡时间,因为是从加时开始时间开始计算的)
+        String newEndTime;
+        int timeComparison = compareTime(normalizedAddTimeStart, normalizedEndTime);
+        if (timeComparison <= 0) {
+            // 加时开始时间 <= 预订结束时间:新结束时间 = 预订结束时间 + 加时分钟数
+            newEndTime = calculateNewEndTime(normalizedEndTime, addTimeMinutes);
+        } else {
+            // 加时开始时间 > 预订结束时间:新结束时间 = 加时开始时间 + 加时分钟数
+            // (这已经包含了中间的空挡时间,因为是从加时开始时间开始累加的)
+            newEndTime = calculateNewEndTime(normalizedAddTimeStart, addTimeMinutes);
+        }
+
+        // 校验:不能超过下一个已确认预约的开始时间
+        validateAddTimeNotExceedNextReservation(reservation, normalizedAddTimeStart, newEndTime);
+
+        // 校验:不能超过营业时间的结束时间
+        validateAddTimeNotExceedBusinessHours(reservation, newEndTime);
+
+        // 更新预约结束时间
+        reservation.setEndTime(newEndTime);
+        boolean updateResult = this.updateById(reservation);
+        if (!updateResult) {
+            throw new RuntimeException("更新预约结束时间失败");
+        }
+
+        log.info("商家端加时成功,reservationId={}, 原结束时间={}, 加时开始时间={}, 加时分钟数={}, 新结束时间={}",
+                reservationId, oldEndTime, addTimeStart, addTimeMinutes, newEndTime);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean verifyReservationByCode(String verificationCode) {
+        log.info("StoreReservationServiceImpl.verifyReservationByCode?verificationCode={}", verificationCode);
+
+        if (verificationCode == null || verificationCode.trim().isEmpty()) {
+            throw new RuntimeException("核销码不能为空");
+        }
+
+        // 根据核销码查询订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getVerificationCode, verificationCode)
+                .eq(UserReservationOrder::getDeleteFlag, 0)
+                .last("LIMIT 1");
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            throw new RuntimeException("核销码不存在或订单已被删除");
+        }
+
+        // 根据订单关联的预约ID查询预约信息
+        UserReservation reservation = this.getById(order.getReservationId());
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在或已被删除");
+        }
+
+        // 校验预约状态(必须是已确认状态才能核销)
+        if (reservation.getStatus() == null || reservation.getStatus() != 1) {
+            throw new RuntimeException("预约状态不正确,只有已确认的预约才能核销");
+        }
+
+        // 校验是否过期:当前时间是否超过预约结束时间
+        if (isReservationExpired(reservation)) {
+            throw new RuntimeException("预约已过期,无法核销");
+        }
+
+        if(order.getOrderCostType() == 1){
+            //调用退款
+            MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
+            strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款", 3);
+
+        }
+
+        // 更新预约状态为已到店(status = 2)
+        reservation.setStatus(2); // 已到店
+        reservation.setActualArrivalTime(new Date());
+        boolean updateReservation = this.updateById(reservation);
+        if (!updateReservation) {
+            throw new RuntimeException("更新预约状态失败");
+        }
+
+        // 更新订单状态为已完成(order_status = 2)
+        order.setOrderStatus(2); // 已完成
+        boolean updateOrder = userReservationOrderService.updateById(order);
+        if (!updateOrder) {
+            throw new RuntimeException("更新订单状态失败");
+        }
+
+        // 核销成功后,发送订金退款短信和通知
+        try {
+            sendDepositRefundSmsAndNotice(reservation, order);
+        } catch (Exception e) {
+            log.error("核销后发送订金退款短信和通知失败,但不影响核销流程,verificationCode={}, orderId={}, error={}",
+                    verificationCode, order.getId(), e.getMessage(), e);
+            // 短信和通知发送失败不影响核销流程,只记录日志
+        }
+
+
+        log.info("核销预约订单成功,verificationCode={}, reservationId={}, orderId={}",
+                verificationCode, reservation.getId(), order.getId());
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean refundReservation(Integer reservationId) {
+        log.info("StoreReservationServiceImpl.refundReservation?reservationId={}", reservationId);
+
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        // 查询关联的订单
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId)
+                .eq(UserReservationOrder::getDeleteFlag, 0)
+                .last("LIMIT 1");
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+
+        if (order == null) {
+            throw new RuntimeException("未找到关联的订单信息");
+        }
+
+        // 检查订单状态,只有已完成的订单才能退款
+        if (order.getOrderStatus() == null || order.getOrderStatus() != 1) {
+            log.warn("订单状态不正确,无法退款,订单ID: {}, 订单状态: {}", order.getId(), order.getOrderStatus());
+            throw new RuntimeException("订单状态不正确,只有已完成的订单才能退款");
+        }
+
+        // 检查订单费用类型,只有付费订单才需要退款
+        if (order.getOrderCostType() == null || order.getOrderCostType() == 0) {
+            log.info("免费订单无需退款,订单ID: {}, 预约ID: {}", order.getId(), reservationId);
+            return true;
+        }
+
+        // TODO: 调用支付退款接口
+        // 1. 调用第三方支付退款接口(微信支付、支付宝等)
+        // 2. 根据退款结果更新订单状态
+
+        // 更新订单状态为已退款(order_status = 7)
+        order.setOrderStatus(7); // 已退款
+        order.setRefundTime(new Date());
+        boolean updateOrder = userReservationOrderService.updateById(order);
+        if (!updateOrder) {
+            throw new RuntimeException("更新订单状态失败");
+        }
+
+        // 更新预约状态为已取消(status = 3)
+        reservation.setStatus(3); // 已取消
+        boolean updateReservation = this.updateById(reservation);
+        if (!updateReservation) {
+            throw new RuntimeException("更新预约状态失败");
+        }
+
+        log.info("预订退款处理成功,预约ID: {}, 订单ID: {}", reservationId, order.getId());
+        return true;
+    }
+
+    /**
+     * 校验加时不能超过下一个已确认预约的开始时间
+     */
+    private void validateAddTimeNotExceedNextReservation(UserReservation reservation, String addTimeStart, String newEndTime) {
+        // 确定查询基准时间:取当前结束时间和加时开始时间的较大值
+        String queryBaseTime = reservation.getEndTime();
+        if (compareTime(addTimeStart, reservation.getEndTime()) > 0) {
+            queryBaseTime = addTimeStart;
+        }
+
+        // 查询同一门店、同一日期、状态为已确认(status=1)的预约
+        LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservation::getStoreId, reservation.getStoreId())
+                .apply("DATE(user_reservation.reservation_date) = DATE({0})", reservation.getReservationDate())
+                .eq(UserReservation::getStatus, 1) // 已确认
+                .ge(UserReservation::getStartTime, queryBaseTime)
+                .ne(UserReservation::getId, reservation.getId())
+                .eq(UserReservation::getDeleteFlag, 0)
+                .orderByAsc(UserReservation::getStartTime)
+                .last("LIMIT 1");
+
+        UserReservation nextReservation = this.getOne(wrapper);
+
+        if (nextReservation != null) {
+            if (compareTime(newEndTime, nextReservation.getStartTime()) > 0) {
+                throw new RuntimeException(
+                        String.format("新的结束时间 %s 超过了下一个已确认预约的开始时间 %s(预约号:%s)",
+                                newEndTime, nextReservation.getStartTime(), nextReservation.getReservationNo()));
+            }
+        }
+    }
+
+    /**
+     * 标准化时间格式:将 yyyy-MM-dd HH:mm 或 yyyy-MM-dd HH:mm:ss 统一转换为 yyyy-MM-dd HH:mm:ss
+     * @param dateTime 时间字符串
+     * @return 标准化后的时间字符串(yyyy-MM-dd HH:mm:ss格式),如果格式不正确返回null
+     */
+    private String normalizeDateTime(String dateTime) {
+        if (dateTime == null || dateTime.trim().isEmpty()) {
+            return null;
+        }
+        
+        String trimmed = dateTime.trim();
+        
+        // 尝试解析 yyyy-MM-dd HH:mm:ss 格式
+        SimpleDateFormat formatWithSeconds = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        formatWithSeconds.setLenient(false);
+        try {
+            Date date = formatWithSeconds.parse(trimmed);
+            return formatWithSeconds.format(date);
+        } catch (ParseException e) {
+            // 继续尝试其他格式
+        }
+        
+        // 尝试解析 yyyy-MM-dd HH:mm 格式
+        SimpleDateFormat formatWithoutSeconds = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+        formatWithoutSeconds.setLenient(false);
+        try {
+            Date date = formatWithoutSeconds.parse(trimmed);
+            // 转换为 yyyy-MM-dd HH:mm:ss 格式(秒数补0)
+            return formatWithSeconds.format(date);
+        } catch (ParseException e) {
+            // 格式不正确
+            return null;
+        }
+    }
+
+    /**
+     * 比较两个时间字符串(支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm 格式)
+     */
+    private int compareTime(String time1, String time2) {
+        try {
+            // 先标准化时间格式
+            String normalizedTime1 = normalizeDateTime(time1);
+            String normalizedTime2 = normalizeDateTime(time2);
+            
+            if (normalizedTime1 == null || normalizedTime2 == null) {
+                throw new RuntimeException("时间格式不正确,无法比较");
+            }
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sdf.setLenient(false);
+            Date date1 = sdf.parse(normalizedTime1);
+            Date date2 = sdf.parse(normalizedTime2);
+            return date1.compareTo(date2);
+        } catch (ParseException e) {
+            log.error("比较时间失败,time1={}, time2={}", time1, time2, e);
+            throw new RuntimeException("时间比较失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 计算新的结束时间:加时开始时间 + 加时分钟数
+     * 返回格式:yyyy-MM-dd HH:mm(不含秒)
+     */
+    private String calculateNewEndTime(String addTimeStart, Integer addTimeMinutes) {
+        try {
+            // 解析输入时间(支持 yyyy-MM-dd HH:mm:ss 格式)
+            SimpleDateFormat parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            parseFormat.setLenient(false);
+            Date startDate = parseFormat.parse(addTimeStart.trim());
+
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(startDate);
+            calendar.add(Calendar.MINUTE, addTimeMinutes);
+
+            // 返回格式:yyyy-MM-dd HH:mm(不含秒)
+            SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+            return outputFormat.format(calendar.getTime());
+        } catch (ParseException e) {
+            log.error("计算新的结束时间失败,addTimeStart={}, addTimeMinutes={}", addTimeStart, addTimeMinutes, e);
+            throw new RuntimeException("时间计算失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 计算两个时间之间的分钟差(time2 - time1)
+     * @param time1 开始时间(yyyy-MM-dd HH:mm:ss格式)
+     * @param time2 结束时间(yyyy-MM-dd HH:mm:ss格式)
+     * @return 分钟差,如果time2 < time1则返回负数
+     */
+    private long calculateTimeGapInMinutes(String time1, String time2) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sdf.setLenient(false);
+            Date date1 = sdf.parse(time1.trim());
+            Date date2 = sdf.parse(time2.trim());
+            
+            long diffInMillis = date2.getTime() - date1.getTime();
+            return diffInMillis / (1000 * 60); // 转换为分钟
+        } catch (ParseException e) {
+            log.error("计算时间差失败,time1={}, time2={}", time1, time2, e);
+            throw new RuntimeException("计算时间差失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 校验加时不能超过营业时间的结束时间
+     * 
+     * @param reservation 预约信息
+     * @param newEndTime 新的结束时间(yyyy-MM-dd HH:mm:ss格式)
+     */
+    private void validateAddTimeNotExceedBusinessHours(UserReservation reservation, String newEndTime) {
+        try {
+            if (reservation == null || reservation.getStoreId() == null || reservation.getReservationDate() == null) {
+                log.warn("预约信息不完整,跳过营业时间校验");
+                return;
+            }
+
+            // 查询门店的预订设置
+            StoreBookingSettings settings = storeBookingSettingsService.getByStoreId(reservation.getStoreId());
+            if (settings == null || settings.getId() == null) {
+                log.warn("门店未配置预订设置,跳过营业时间校验,storeId={}", reservation.getStoreId());
+                return;
+            }
+
+            // 查询营业时间列表
+            List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
+            if (businessHoursList == null || businessHoursList.isEmpty()) {
+                log.warn("门店未配置营业时间,跳过营业时间校验,storeId={}", reservation.getStoreId());
+                return;
+            }
+
+            // 解析预约日期
+            Date reservationDate = reservation.getReservationDate();
+            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+            String reservationDateStr = dateFormat.format(reservationDate);
+
+            // 查找匹配的营业时间
+            StoreBookingBusinessHours matchedBusinessHours = null;
+
+            // 先查找特殊营业时间(business_type=2,兼容旧值1)
+            // 特殊营业时间的特征:有 holidayDate 或 essentialId 或 holidayType 不为空
+            for (StoreBookingBusinessHours businessHours : businessHoursList) {
+                Integer businessType = businessHours.getBusinessType();
+                // 判断是否为特殊营业时间:新值2,或旧值1且有关联的节假日信息
+                boolean isSpecialBusinessHours = false;
+                if (businessType != null) {
+                    if (businessType == 2) {
+                        // 新值:2 表示特殊营业时间
+                        isSpecialBusinessHours = true;
+                    } else if (businessType == 1) {
+                        // 旧值:1 可能是特殊营业时间,需要检查是否有节假日信息
+                        if (businessHours.getEssentialId() != null || 
+                            businessHours.getHolidayDate() != null || 
+                            (businessHours.getHolidayType() != null && !businessHours.getHolidayType().trim().isEmpty())) {
+                            isSpecialBusinessHours = true;
+                        }
+                    }
+                }
+                
+                if (isSpecialBusinessHours) {
+                    // 特殊营业时间:需要匹配 essential_holiday_comparison 表中的日期范围
+                    if (businessHours.getEssentialId() != null) {
+                        EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(businessHours.getEssentialId());
+                        if (holiday != null && holiday.getStartTime() != null && holiday.getEndTime() != null) {
+                            // 判断预约日期是否在节假日的日期范围内
+                            Date startTime = holiday.getStartTime();
+                            Date endTime = holiday.getEndTime();
+                            if (!reservationDate.before(startTime) && !reservationDate.after(endTime)) {
+                                matchedBusinessHours = businessHours;
+                                break;
+                            }
+                        }
+                    } else if (businessHours.getHolidayDate() != null) {
+                        // 如果没有 essentialId,使用 holidayDate 匹配
+                        if (reservationDateStr.equals(businessHours.getHolidayDate())) {
+                            matchedBusinessHours = businessHours;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // 如果没有匹配到特殊营业时间,使用正常营业时间(business_type=1,兼容旧值0)
+            if (matchedBusinessHours == null) {
+                matchedBusinessHours = businessHoursList.stream()
+                        .filter(h -> {
+                            Integer businessType = h.getBusinessType();
+                            // 兼容旧值:0 和新值:1 都表示正常营业时间
+                            return businessType != null && (businessType == 1 || businessType == 0);
+                        })
+                        .findFirst()
+                        .orElse(null);
+            }
+
+            // 如果 store_booking_business_hours 中没有找到匹配的营业时间,则从 store_business_info 查询
+            if (matchedBusinessHours == null) {
+                log.info("store_booking_business_hours 中未找到匹配的营业时间,尝试从 store_business_info 查询,storeId={}", reservation.getStoreId());
+                matchedBusinessHours = findBusinessHoursFromStoreBusinessInfo(reservation, reservationDate, reservationDateStr);
+            }
+
+            if (matchedBusinessHours == null) {
+                log.warn("未找到匹配的营业时间,跳过营业时间校验");
+                return;
+            }
+
+            // 如果是全天营业(booking_time_type=1),不限制结束时间
+            if (matchedBusinessHours.getBookingTimeType() != null && matchedBusinessHours.getBookingTimeType() == 1) {
+                log.info("全天营业,不限制结束时间");
+                return;
+            }
+
+            // 获取营业时间的结束时间
+            String businessEndTime = matchedBusinessHours.getEndTime();
+            if (businessEndTime == null || businessEndTime.trim().isEmpty()) {
+                log.warn("营业时间结束时间为空,跳过校验");
+                return;
+            }
+
+            // 标准化新的结束时间,提取日期部分
+            String normalizedNewEndTime = normalizeDateTime(newEndTime);
+            if (normalizedNewEndTime == null) {
+                log.warn("新的结束时间格式错误,跳过校验,newEndTime={}", newEndTime);
+                return;
+            }
+
+            // 从新的结束时间中提取日期部分(用于构建营业时间的完整日期时间)
+            // normalizedNewEndTime 格式:yyyy-MM-dd HH:mm:ss
+            String newEndTimeDateStr = normalizedNewEndTime.substring(0, 10); // 提取 yyyy-MM-dd
+
+            // 将营业时间的结束时间转换为完整的日期时间格式
+            // businessEndTime 是 HH:mm 格式,需要结合新结束时间的日期
+            String businessEndDateTimeStr = newEndTimeDateStr + " " + businessEndTime.trim() + ":00";
+            String normalizedBusinessEndTime = normalizeDateTime(businessEndDateTimeStr);
+            if (normalizedBusinessEndTime == null) {
+                log.warn("营业时间结束时间格式错误,跳过校验,businessEndTime={}", businessEndTime);
+                return;
+            }
+
+            // 比较新的结束时间和营业时间的结束时间
+            int comparison = compareTime(normalizedNewEndTime, normalizedBusinessEndTime);
+            if (comparison > 0) {
+                // 新的结束时间超过了营业时间的结束时间
+                throw new RuntimeException("加时后结束时间不能超过营业时间,营业结束时间为:" + businessEndTime);
+            }
+
+            log.info("加时校验通过,新的结束时间={},营业结束时间={}", normalizedNewEndTime, normalizedBusinessEndTime);
+        } catch (Exception e) {
+            if (e instanceof RuntimeException) {
+                throw e;
+            }
+            log.error("校验加时是否超过营业时间失败", e);
+            // 校验失败时,抛出异常以阻止加时操作
+            throw new RuntimeException("校验营业时间失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 从 store_business_info 表中查找匹配的营业时间
+     * 
+     * @param reservation 预约信息
+     * @param reservationDate 预约日期
+     * @param reservationDateStr 预约日期字符串(yyyy-MM-dd格式)
+     * @return 匹配的营业时间对象(包装为 StoreBookingBusinessHours 格式),未找到返回 null
+     */
+    private StoreBookingBusinessHours findBusinessHoursFromStoreBusinessInfo(
+            UserReservation reservation, Date reservationDate, String reservationDateStr) {
+        try {
+            // 查询门店的营业时间列表
+            List<StoreBusinessInfoVo> storeBusinessInfoList = storeInfoService.getStoreInfoBusinessHours(reservation.getStoreId());
+            if (storeBusinessInfoList == null || storeBusinessInfoList.isEmpty()) {
+                log.warn("门店未配置 store_business_info 营业时间,storeId={}", reservation.getStoreId());
+                return null;
+            }
+
+            StoreBusinessInfoVo matchedBusinessInfo = null;
+
+            // 先查找特殊营业时间(business_type=2)
+            for (StoreBusinessInfoVo businessInfo : storeBusinessInfoList) {
+                if (businessInfo.getBusinessType() != null && businessInfo.getBusinessType() == 2) {
+                    // 特殊营业时间:需要匹配 essential_holiday_comparison 表中的日期范围
+                    if (businessInfo.getEssentialId() != null && !businessInfo.getEssentialId().trim().isEmpty()) {
+                        try {
+                            Integer essentialId = Integer.parseInt(businessInfo.getEssentialId().trim());
+                            EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(essentialId);
+                            if (holiday != null && holiday.getStartTime() != null && holiday.getEndTime() != null) {
+                                // 判断预约日期是否在节假日的日期范围内
+                                Date startTime = holiday.getStartTime();
+                                Date endTime = holiday.getEndTime();
+                                if (!reservationDate.before(startTime) && !reservationDate.after(endTime)) {
+                                    matchedBusinessInfo = businessInfo;
+                                    break;
+                                }
+                            }
+                        } catch (NumberFormatException e) {
+                            log.warn("门店营业时间关联的节假日ID格式错误,storeId={}, essentialId={}", 
+                                    reservation.getStoreId(), businessInfo.getEssentialId());
+                        }
+                    } else if (businessInfo.getBusinessDate() != null) {
+                        // 如果没有 essentialId,使用 businessDate 匹配
+                        if (reservationDateStr.equals(businessInfo.getBusinessDate())) {
+                            matchedBusinessInfo = businessInfo;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // 如果没有匹配到特殊营业时间,使用正常营业时间(business_type=1)
+            if (matchedBusinessInfo == null) {
+                matchedBusinessInfo = storeBusinessInfoList.stream()
+                        .filter(h -> h.getBusinessType() != null && h.getBusinessType() == 1)
+                        .findFirst()
+                        .orElse(null);
+            }
+
+            if (matchedBusinessInfo == null) {
+                log.warn("store_business_info 中未找到匹配的营业时间,storeId={}", reservation.getStoreId());
+                return null;
+            }
+
+            // 将 StoreBusinessInfoVo 转换为 StoreBookingBusinessHours 格式(仅用于返回 endTime)
+            StoreBookingBusinessHours result = new StoreBookingBusinessHours();
+            result.setEndTime(matchedBusinessInfo.getEndTime());
+            result.setBookingTimeType(null); // store_business_info 没有 booking_time_type,设为 null 表示非全天
+            log.info("从 store_business_info 找到匹配的营业时间,endTime={}", matchedBusinessInfo.getEndTime());
+            return result;
+        } catch (Exception e) {
+            log.error("从 store_business_info 查询营业时间失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 校验预约是否过期
+     * 判断当前时间是否超过预约日期 + 结束时间
+     */
+    private boolean isReservationExpired(UserReservation reservation) {
+        try {
+            Date now = new Date();
+            Date reservationDate = reservation.getReservationDate();
+            String endTime = reservation.getEndTime();
+
+            // 预约日期为空,视为过期
+            if (reservationDate == null) {
+                log.warn("预约日期为空,视为过期,reservationId={}", reservation.getId());
+                return true;
+            }
+
+            // 结束时间为空,视为过期
+            if (endTime == null || endTime.trim().isEmpty()) {
+                log.warn("结束时间为空,视为过期,reservationId={}", reservation.getId());
+                return true;
+            }
+
+            // 解析结束时间
+            // endTime 可能是两种格式:
+            // 1. 只有时间部分(如:"09:00")
+            // 2. 完整的日期时间(如:"2026-03-11 09:00")
+            String endTimeTrimmed = endTime.trim();
+            Date endDateTime = null;
+            
+            // 尝试多种日期时间格式
+            SimpleDateFormat[] dateTimeFormats = {
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm"),      // 完整日期时间格式
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),   // 完整日期时间格式(带秒)
+                    new SimpleDateFormat("HH:mm")                  // 只有时间格式
+            };
+            
+            // 先尝试解析为完整日期时间格式
+            boolean parsed = false;
+            for (int i = 0; i < 2; i++) {
+                try {
+                    endDateTime = dateTimeFormats[i].parse(endTimeTrimmed);
+                    parsed = true;
+                    break;
+                } catch (ParseException e) {
+                    // 继续尝试下一个格式
+                }
+            }
+            
+            // 如果完整日期时间格式解析失败,尝试组合预约日期和时间
+            if (!parsed) {
+                try {
+                    // 先验证是否为时间格式(HH:mm)
+                    dateTimeFormats[2].parse(endTimeTrimmed);
+                    // 组合预约日期和时间
+                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+                    String dateStr = dateFormat.format(reservationDate);
+                    String dateTimeStr = dateStr + " " + endTimeTrimmed;
+                    endDateTime = dateTimeFormats[0].parse(dateTimeStr);
+                    parsed = true;
+                } catch (ParseException e) {
+                    log.error("无法解析结束时间格式,reservationDate={}, endTime={}", 
+                            reservationDate, endTime, e);
+                    return true;
+                }
+            }
+            
+            if (endDateTime == null) {
+                log.error("无法解析结束时间格式,reservationDate={}, endTime={}", 
+                        reservationDate, endTime);
+                return true;
+            }
+
+            // 判断当前时间是否超过结束时间
+            boolean expired = now.after(endDateTime);
+            if (expired) {
+                log.info("预约已过期,reservationId={}, reservationDate={}, endTime={}, endDateTime={}, now={}", 
+                        reservation.getId(), reservationDate, endTime, endDateTime, now);
+            }
+            return expired;
+        } catch (Exception e) {
+            log.error("校验预约是否过期失败,reservationId={}", reservation.getId(), e);
+            return true;
+        }
+    }
+
+    /**
+     * 计算Redis过期时间(秒):从当前时间到预订结束时间 + 3小时的秒数
+     */
+    private long calculateExpireSecondsWithBuffer(UserReservation reservation) {
+        try {
+            Date now = new Date();
+            String endTime = reservation.getEndTime();
+
+            if (endTime == null || endTime.trim().isEmpty()) {
+                return 0;
+            }
+
+            SimpleDateFormat[] dateTimeFormats = {
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
+                    new SimpleDateFormat("yyyy/MM/dd HH:mm")
+            };
+
+            Date endDateTime = null;
+            for (SimpleDateFormat format : dateTimeFormats) {
+                try {
+                    endDateTime = format.parse(endTime);
+                    break;
+                } catch (ParseException e) {
+                    // 继续尝试下一个格式
+                }
+            }
+
+            if (endDateTime == null) {
+                log.error("无法解析结束时间格式,endTime={}", endTime);
+                return 0;
+            }
+
+            // 在结束时间基础上加3小时
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(endDateTime);
+            calendar.add(Calendar.HOUR_OF_DAY, 3);
+            Date expireDateTime = calendar.getTime();
+
+            long diff = expireDateTime.getTime() - now.getTime();
+            return diff > 0 ? diff / 1000 : 0;
+        } catch (Exception e) {
+            log.error("计算Redis过期时间失败", e);
+            return 0;
+        }
+    }
+
+    /**
+     * 处理过期的预订核销key
+     */
+    private void handleExpiredReservationKey(String expiredKey) {
+        try {
+            String reservationNo = expiredKey.replace(RESERVATION_VERIFY_PREFIX, "");
+
+            log.info("检测到预订核销key过期,预约号: {}, key: {}", reservationNo, expiredKey);
+
+            LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(UserReservation::getReservationNo, reservationNo)
+                    .eq(UserReservation::getDeleteFlag, 0)
+                    .last("LIMIT 1");
+            UserReservation reservation = this.getOne(wrapper);
+
+            if (reservation == null) {
+                log.warn("预订核销key过期,但未找到对应的预约信息,预约号: {}", reservationNo);
+                return;
+            }
+
+            log.info("开始处理预订退款,预约号: {}, 预约ID: {}", reservationNo, reservation.getId());
+            refundReservation(reservation.getId());
+
+        } catch (Exception e) {
+            log.error("处理过期预订核销key失败,key: {}", expiredKey, e);
+        }
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private String getOrderStatusText(Integer orderStatus) {
+        if (orderStatus == null) {
+            return "未知";
+        }
+        switch (orderStatus) {
+            case 0: return "待支付";
+            case 1: return "待使用";
+            case 2: return "已完成";
+            case 3: return "已过期";
+            case 4: return "已取消";
+            case 5: return "已关闭";
+            case 6: return "退款中";
+            case 7: return "已退款";
+            case 8: return "商家预订";
+            default: return "未知";
+        }
+    }
+}

+ 68 - 0
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationOrderServiceImpl.java

@@ -0,0 +1,68 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.mapper.UserReservationOrderMapper;
+import shop.alien.store.service.MerchantPaymentOrderService;
+import shop.alien.store.service.UserReservationOrderService;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 用户预订订单表 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class UserReservationOrderServiceImpl extends ServiceImpl<UserReservationOrderMapper, UserReservationOrder> implements UserReservationOrderService {
+
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+
+    @Override
+    public UserReservationOrder getByOrderSn(String orderSn) {
+        if (orderSn == null || orderSn.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<UserReservationOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservationOrder::getOrderSn, orderSn);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public UserReservationOrder getByOutTradeNo(String outTradeNo) {
+        if (outTradeNo == null || outTradeNo.trim().isEmpty()) {
+            return null;
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return null;
+        }
+        return this.getById(paymentOrder.getOrderId());
+    }
+
+    @Override
+    public String generateOrderSn() {
+        String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
+        int random = ThreadLocalRandom.current().nextInt(1000000);
+        return "YS" + dateStr + String.format("%06d", random);
+    }
+
+    @Override
+    public int physicalDeleteByReservationId(Integer reservationId) {
+        if (reservationId == null) {
+            return 0;
+        }
+        return baseMapper.physicalDeleteByReservationId(reservationId);
+    }
+}

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

@@ -0,0 +1,1193 @@
+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.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.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.*;
+import shop.alien.entity.store.dto.UserReservationDTO;
+import shop.alien.entity.store.vo.StoreBookingBusinessHoursVo;
+import shop.alien.entity.store.vo.StoreMainInfoVo;
+import shop.alien.entity.store.vo.StoreReservationListVo;
+import shop.alien.entity.store.vo.UserReservationVo;
+import shop.alien.mapper.EssentialHolidayComparisonMapper;
+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.*;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+/**
+ * 用户预约 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class UserReservationServiceImpl extends ServiceImpl<UserReservationMapper, UserReservation> implements UserReservationService {
+
+    private final UserReservationTableMapper userReservationTableMapper;
+
+    private final StoreBookingSettingsService storeBookingSettingsService;
+
+    private final StoreBookingTableService storeBookingTableService;
+
+    private final StoreBookingCategoryService storeBookingCategoryService;
+
+    private final StoreInfoService storeInfoService;
+
+    private final UserReservationOrderService userReservationOrderService;
+
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
+
+    private final UserReservationOrderMapper userReservationOrderMapper;
+
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+
+    private ReservationOrderPageService reservationOrderPageService;
+
+    @Autowired
+    public void setReservationOrderPageService(@Lazy ReservationOrderPageService reservationOrderPageService) {
+        this.reservationOrderPageService = reservationOrderPageService;
+    }
+
+    /** 预约状态:待确认 */
+    private static final int STATUS_PENDING = 0;
+    /** 预约状态:已确认(待使用) */
+    private static final int STATUS_CONFIRMED = 1;
+    /** 预约状态:已取消(不参与约满统计与展示) */
+    private static final int STATUS_CANCELLED = 3;
+    /** 预约状态:未到店超时 */
+    private static final int STATUS_NO_SHOW_TIMEOUT = 4;
+    /** 订单状态:待使用 */
+    private static final int ORDER_STATUS_TO_USE = 1;
+    /** 订单状态:已过期 */
+    private static final int ORDER_STATUS_EXPIRED = 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) {
+        if (dto.getUserId() == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        if (dto.getStoreId() == null) {
+            throw new RuntimeException("门店ID不能为空");
+        }
+        if (dto.getReservationDate() == null) {
+            throw new RuntimeException("预约日期不能为空");
+        }
+        if (dto.getGuestCount() == null || dto.getGuestCount() < 1) {
+            throw new RuntimeException("预约人数至少为1");
+        }
+
+        UserReservation entity = new UserReservation();
+        BeanUtils.copyProperties(dto, entity, "id", "tableIds");
+        entity.setId(null);
+        entity.setReservationNo(generateReservationNo());
+        if (entity.getStatus() == null) {
+            entity.setStatus(STATUS_PENDING);
+        }
+        this.save(entity);
+
+        saveReservationTables(entity.getId(), dto.getTableIds());
+
+        // 同步创建预订订单(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);
+        }
+        // 免费预订:对应预约状态设为已确认(1),与订单「待使用」一致
+        if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
+            entity.setStatus(STATUS_CONFIRMED);
+            this.updateById(entity);
+        }
+
+        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());
+        // 当store_booking_settings.reservation = 1时,订单状态为待使用(1),不为1时,订单状态为待支付(0)
+        order.setOrderStatus(
+                storeBookingSettingsService.getByStoreId(reservation.getStoreId()) != null && "1".equals(
+                        storeBookingSettingsService.getByStoreId(reservation.getStoreId()).getReservation()) ? 0 : 1);
+        order.setIsMerchantReservation(0);
+        fillOrderFromStoreSettings(order, reservation);
+        // 免费预订:生成核销码,不生成核销二维码 URL(付费订单在支付成功时再生成)
+        if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
+            order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
+        }
+        return order;
+    }
+
+    /**
+     * 按门店预订配置填充订单的收费/订金/取消政策等字段(与 add 一致)
+     */
+    private void fillOrderFromStoreSettings(UserReservationOrder order, UserReservation reservation) {
+        if (reservation == null || reservation.getStoreId() == null) {
+            return;
+        }
+        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);
+        }
+    }
+
+    @Override
+    public boolean updateReservation(UserReservationDTO dto) {
+        if (dto.getId() == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+        // 根据订单id获取预约信息
+        UserReservationOrder userReservationOrder = userReservationOrderMapper.selectById(dto.getId());
+        UserReservation existing = this.getById(userReservationOrder.getReservationId());
+        if (existing == null) {
+            throw new RuntimeException("预约不存在");
+        }
+
+        UserReservation entity = new UserReservation();
+        BeanUtils.copyProperties(dto, entity, "tableIds", "reservationNo");
+        entity.setId(existing.getId());
+        entity.setReservationNo(existing.getReservationNo());
+        this.updateById(entity);
+
+        saveReservationTables(existing.getId(), dto.getTableIds());
+
+        // 与 add 一致:同步 user_reservation_order(无则创建,有则仅待支付时按门店配置刷新)
+        UserReservation updatedReservation = this.getById(existing.getId());
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, existing.getId()).last("LIMIT 1");
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+        if (order == null) {
+            order = buildReservationOrder(updatedReservation);
+            userReservationOrderService.save(order);
+            if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
+                reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
+            }
+            if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
+                this.update(new LambdaUpdateWrapper<UserReservation>()
+                        .eq(UserReservation::getId, existing.getId())
+                        .set(UserReservation::getStatus, STATUS_CONFIRMED));
+            }
+        } else if (order.getOrderStatus() != null && order.getOrderStatus() == 0) {
+            fillOrderFromStoreSettings(order, updatedReservation);
+            userReservationOrderService.updateById(order);
+            if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
+                reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean removeReservation(Integer id) {
+
+        UserReservationOrder one = userReservationOrderService.getOne(new LambdaUpdateWrapper<UserReservationOrder>()
+                .eq(UserReservationOrder::getId, id));
+        if (one == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        // 订单状态置为已取消(4)
+        userReservationOrderService.update(
+                new LambdaUpdateWrapper<UserReservationOrder>()
+                        .eq(UserReservationOrder::getId, one.getId())
+                        .set(UserReservationOrder::getOrderStatus, 4));
+        // 预约不再逻辑删除,仅将 status 置为已取消(3)
+        return this.update(new LambdaUpdateWrapper<UserReservation>()
+                .eq(UserReservation::getId, one.getReservationId())
+                .set(UserReservation::getStatus, STATUS_CANCELLED));
+    }
+
+    @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) {
+            return null;
+        }
+        UserReservationVo vo = new UserReservationVo();
+        BeanUtils.copyProperties(one, vo);
+        vo.setTableIds(listTableIdsByReservationId(id));
+        return vo;
+    }
+
+    @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 (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
+                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) {
+        int current = pageNum == null || pageNum <= 0 ? 1 : pageNum;
+        int size = pageSize == null || pageSize <= 0 ? 10 : pageSize;
+        Page<UserReservation> page = new Page<>(current, size);
+        LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, dateFrom, dateTo);
+        wrapper.orderByDesc(UserReservation::getCreatedTime);
+        IPage<UserReservation> entityPage = this.page(page, wrapper);
+        return entityPage.convert(this::toVoWithTableIds);
+    }
+
+    @Override
+    public List<UserReservationVo> list(Integer userId, Integer storeId, Integer status) {
+        LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, null, null);
+        wrapper.orderByDesc(UserReservation::getCreatedTime);
+        List<UserReservation> list = this.list(wrapper);
+        return list.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
+    }
+
+    @Override
+    public Map<String, Object> getBookingsByStoreId(Integer storeId) {
+        Map<String, Object> list = new HashMap<>();
+        //通过storeId查询商家预设的预订信息状态
+        LambdaQueryWrapper<StoreBookingSettings> eq = new LambdaQueryWrapper<StoreBookingSettings>().eq(StoreBookingSettings::getStoreId, storeId);
+        List<StoreBookingSettings> storeBookingSettings = storeBookingSettingsService.list(eq);
+        list.put("storeBookingSettings", storeBookingSettings);
+        
+        // 查询营业时间:如果有设置信息,则查询对应的营业时间
+        // 特殊营业时间通过 essential_id 关联 essential_holiday_comparison 节假日表
+        List<StoreBookingBusinessHoursVo> storeBookingBusinessHours = new ArrayList<>();
+        if (storeBookingSettings != null && !storeBookingSettings.isEmpty()) {
+            // 获取第一个设置的ID(通常一个门店只有一个设置)
+            StoreBookingSettings settings = storeBookingSettings.get(0);
+            if (settings != null && settings.getId() != null) {
+                List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
+                // 转换为 Vo 并关联节假日信息
+                for (StoreBookingBusinessHours businessHours : businessHoursList) {
+                    StoreBookingBusinessHoursVo vo = new StoreBookingBusinessHoursVo();
+                    BeanUtils.copyProperties(businessHours, vo);
+                    
+                    // 如果是特殊营业时间(business_type = 2)且有关联的节假日ID,查询节假日信息
+                    if (businessHours.getBusinessType() != null && businessHours.getBusinessType() == 2 
+                            && businessHours.getEssentialId() != null) {
+                        try {
+                            EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(businessHours.getEssentialId());
+                            if (holiday != null) {
+                                vo.setHolidayInfo(holiday);
+                                // 从节假日信息中获取日期作为 businessDate
+                                if (holiday.getFestivalDate() != null) {
+                                    vo.setBusinessDate(holiday.getFestivalDate().toString());
+                                } else if (holiday.getStartTime() != null) {
+                                    // 如果 festivalDate 为空,使用 startTime
+                                    java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                                    vo.setBusinessDate(sdf.format(holiday.getStartTime()));
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.warn("查询节假日信息失败,essentialId={}", businessHours.getEssentialId(), e);
+                        }
+                    }
+                    
+                    // 设置 businessDate:优先使用 holidayDate,如果没有则使用 holidayInfo 中的日期
+                    if (vo.getBusinessDate() == null && businessHours.getHolidayDate() != null) {
+                        vo.setBusinessDate(businessHours.getHolidayDate());
+                    }
+                    
+                    // 将 holidayType 的值也放到 businessDate 中
+                    if (businessHours.getHolidayType() != null && !businessHours.getHolidayType().trim().isEmpty()) {
+                        vo.setBusinessDate(businessHours.getHolidayType());
+                    }
+                    
+                    // 将 businessDate 的值也设置到 holidayType(用于回显)
+                    if (vo.getBusinessDate() != null) {
+                        vo.setHolidayType(vo.getBusinessDate());
+                    }
+                    
+                    storeBookingBusinessHours.add(vo);
+                }
+            }
+        }
+        list.put("storeBookingBusinessHours", storeBookingBusinessHours);
+        
+        List<StoreBookingTable> storeBookingTables = storeBookingTableService.list(new LambdaQueryWrapper<StoreBookingTable>().eq(StoreBookingTable::getStoreId, storeId));
+        list.put("storeBookingTables", storeBookingTables);
+        List<StoreBookingCategory> storeBookingCategorys = storeBookingCategoryService.list(new LambdaQueryWrapper<StoreBookingCategory>().eq(StoreBookingCategory::getStoreId, storeId));
+        list.put("storeBookingCategorys", storeBookingCategorys);
+        StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);
+        list.put("storeInfo", storeInfo);
+        return list;
+    }
+
+    /**
+     * 按预定日期查询指定日期的定桌情况。传入 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 该日预约列表,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", null);
+            result.put("tableStatusList", null);
+            return result;
+        }
+        // 门店下所有选座(桌),用于按“选座”维度计算约满
+        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);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        for (int i = 0; i < MAX_DAYS_TO_CHECK; i++) {
+            Date dayStart = cal.getTime();
+            cal.add(Calendar.DAY_OF_MONTH, 1);
+            Date dayEnd = cal.getTime();
+            // 当日、该门店、非取消的预约
+            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);
+            }
+            // 按商户 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)
+                        .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;
+            }
+        }
+        // 连续 MAX_DAYS_TO_CHECK 天所有选座均已约满时,返回最后一天的日期及该日选座状态(全部 full=true),预约列表为空
+        cal.add(Calendar.DAY_OF_MONTH, -1);
+        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;
+    }
+
+    private LambdaQueryWrapper<UserReservation> buildListWrapper(Integer userId, Integer storeId, Integer status,
+                                                                  Date dateFrom, Date dateTo) {
+        LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
+        if (userId != null) {
+            wrapper.eq(UserReservation::getUserId, userId);
+        }
+        if (storeId != null) {
+            wrapper.eq(UserReservation::getStoreId, storeId);
+        }
+        if (status != null) {
+            wrapper.eq(UserReservation::getStatus, status);
+        }
+        if (dateFrom != null) {
+            wrapper.ge(UserReservation::getReservationDate, dateFrom);
+        }
+        if (dateTo != null) {
+            wrapper.le(UserReservation::getReservationDate, dateTo);
+        }
+        return wrapper;
+    }
+
+    private UserReservationVo toVoWithTableIds(UserReservation entity) {
+        UserReservationVo vo = new UserReservationVo();
+        BeanUtils.copyProperties(entity, vo);
+        vo.setTableIds(listTableIdsByReservationId(entity.getId()));
+        return vo;
+    }
+
+    private List<Integer> listTableIdsByReservationId(Integer reservationId) {
+        LambdaQueryWrapper<UserReservationTable> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservationTable::getReservationId, reservationId)
+                .orderByAsc(UserReservationTable::getSort)
+                .orderByAsc(UserReservationTable::getId);
+        List<UserReservationTable> list = userReservationTableMapper.selectList(wrapper);
+        return list.stream().map(UserReservationTable::getTableId).collect(Collectors.toList());
+    }
+
+    /**
+     * 保存预约与桌号关联:先物理删除该预约下原有关联再插入
+     */
+    private void saveReservationTables(Integer reservationId, List<Integer> tableIds) {
+        userReservationTableMapper.physicalDeleteByReservationId(reservationId);
+        if (tableIds != null && !tableIds.isEmpty()) {
+            int sort = 0;
+            for (Integer tableId : tableIds) {
+                UserReservationTable rt = new UserReservationTable();
+                rt.setReservationId(reservationId);
+                rt.setTableId(tableId);
+                rt.setSort(sort++);
+                userReservationTableMapper.insert(rt);
+            }
+        }
+    }
+
+    @Override
+    public List<StoreReservationListVo> getStoreReservationList(Integer storeId, Integer status, Date dateFrom, Date dateTo, Integer orderStatus) {
+        log.info("UserReservationServiceImpl.getStoreReservationList?storeId={}, status={}, dateFrom={}, dateTo={}, orderStatus={}",
+                storeId, status, dateFrom, dateTo, orderStatus);
+        
+        if (storeId == null) {
+            throw new RuntimeException("门店ID不能为空");
+        }
+        
+        return baseMapper.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
+    }
+
+    @Override
+    public boolean cancelReservationByStore(Integer reservationId) {
+        log.info("UserReservationServiceImpl.cancelReservationByStore?reservationId={}", reservationId);
+        
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+        
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        
+        // 检查预约状态,已取消的不能再次取消
+        if (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
+            throw new RuntimeException("预约已取消,不能重复取消");
+        }
+        
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+        
+        if (order == null) {
+            // 如果没有订单,直接更新预约状态为3(已取消)
+            reservation.setStatus(STATUS_CANCELLED); // STATUS_CANCELLED = 3
+            boolean updateResult = this.updateById(reservation);
+            if (!updateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+            log.info("商家端取消预约成功(无订单),reservationId={}", reservationId);
+            return true;
+        }
+        
+        // 判断订单费用类型:0-免费, 1-收费
+        Integer orderCostType = order.getOrderCostType();
+        if (orderCostType == null) {
+            // 如果订单费用类型为空,默认按免费处理
+            orderCostType = 0;
+        }
+        
+        if (orderCostType == 0) {
+            // 免费订单:更新订单状态为4(已取消),更新预约状态为3(已取消)
+            order.setOrderStatus(4); // 4:已取消
+            boolean orderUpdateResult = userReservationOrderService.updateById(order);
+            if (!orderUpdateResult) {
+                throw new RuntimeException("更新订单状态失败");
+            }
+            
+            reservation.setStatus(STATUS_CANCELLED); // STATUS_CANCELLED = 3:已取消
+            boolean reservationUpdateResult = this.updateById(reservation);
+            if (!reservationUpdateResult) {
+                throw new RuntimeException("更新预约状态失败");
+            }
+            
+            log.info("商家端取消预约成功(免费订单),reservationId={}, orderId={}", reservationId, order.getId());
+            return true;
+        } else if (orderCostType == 1) {
+            // 付费订单:功能预留(暂不更新状态,等待后续实现退款逻辑)
+            throw new RuntimeException("付费订单取消功能暂未实现,请稍后再试");
+        } else {
+            throw new RuntimeException("订单费用类型异常,orderCostType=" + orderCostType);
+        }
+    }
+
+    @Override
+    public boolean deleteReservationByStore(Integer reservationId) {
+        log.info("UserReservationServiceImpl.deleteReservationByStore?reservationId={}", reservationId);
+        
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+        
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        
+        // 查询关联的订单信息
+        LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
+        UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
+        
+        if (order == null) {
+            // 如果没有订单,直接删除预约记录
+            boolean deleteResult = this.removeById(reservationId);
+            if (!deleteResult) {
+                throw new RuntimeException("删除预约记录失败");
+            }
+            log.info("商家端删除预订信息成功(无订单),reservationId={}", reservationId);
+            return true;
+        }
+        
+        // 判断订单状态:只有已取消(4)、已退款(7)、已完成(2)状态才能删除
+        Integer orderStatus = order.getOrderStatus();
+        if (orderStatus == null) {
+            throw new RuntimeException("订单状态异常,无法删除");
+        }
+        
+        // 定义可删除的订单状态:2:已完成, 4:已取消, 7:已退款
+        boolean canDelete = orderStatus == 2 || orderStatus == 4 || orderStatus == 7;
+        
+        if (!canDelete) {
+            String statusText = getOrderStatusText(orderStatus);
+            throw new RuntimeException("订单状态为" + statusText + ",不允许删除。只有已取消、已退款、已完成状态的订单可以删除");
+        }
+        
+        // 删除订单记录(逻辑删除)
+        boolean orderDeleteResult = userReservationOrderService.removeById(order.getId());
+        if (!orderDeleteResult) {
+            throw new RuntimeException("删除订单记录失败");
+        }
+        
+        // 删除预约记录(逻辑删除)
+        boolean reservationDeleteResult = this.removeById(reservationId);
+        if (!reservationDeleteResult) {
+            throw new RuntimeException("删除预约记录失败");
+        }
+        
+        log.info("商家端删除预订信息成功,reservationId={}, orderId={}, orderStatus={}", 
+                reservationId, order.getId(), orderStatus);
+        return true;
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private String getOrderStatusText(Integer orderStatus) {
+        if (orderStatus == null) {
+            return "未知";
+        }
+        switch (orderStatus) {
+            case 0: return "待支付";
+            case 1: return "待使用";
+            case 2: return "已完成";
+            case 3: return "已过期";
+            case 4: return "已取消";
+            case 5: return "已关闭";
+            case 6: return "退款中";
+            case 7: return "已退款";
+            case 8: return "商家预订";
+            default: return "未知";
+        }
+    }
+
+    @Override
+    public boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes) {
+        log.info("UserReservationServiceImpl.addTimeByStore?reservationId={}, addTimeStart={}, addTimeMinutes={}", 
+                reservationId, addTimeStart, addTimeMinutes);
+        
+        if (reservationId == null) {
+            throw new RuntimeException("预约ID不能为空");
+        }
+        
+        if (addTimeStart == null || addTimeStart.trim().isEmpty()) {
+            throw new RuntimeException("加时开始时间不能为空");
+        }
+        
+        if (addTimeMinutes == null || addTimeMinutes <= 0) {
+            throw new RuntimeException("加时分钟数必须大于0");
+        }
+        
+        // 验证时间格式 HH:mm
+        if (!addTimeStart.matches("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")) {
+            throw new RuntimeException("加时开始时间格式错误,应为HH:mm格式");
+        }
+        
+        // 查询预约信息
+        UserReservation reservation = this.getById(reservationId);
+        if (reservation == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        
+        // 保存原结束时间用于日志
+        String oldEndTime = reservation.getEndTime();
+        
+        // 计算新的结束时间:加时开始时间 + 加时分钟数
+        String newEndTime = calculateNewEndTime(addTimeStart, addTimeMinutes);
+        
+        // 更新预约结束时间
+        reservation.setEndTime(newEndTime);
+        boolean updateResult = this.updateById(reservation);
+        if (!updateResult) {
+            throw new RuntimeException("更新预约结束时间失败");
+        }
+        
+        log.info("商家端加时成功,reservationId={}, 原结束时间={}, 加时开始时间={}, 加时分钟数={}, 新结束时间={}", 
+                reservationId, oldEndTime, addTimeStart, addTimeMinutes, newEndTime);
+        return true;
+    }
+
+    /**
+     * 计算新的结束时间:加时开始时间 + 加时分钟数
+     *
+     * @param addTimeStart 加时开始时间(HH:mm格式)
+     * @param addTimeMinutes 加时分钟数
+     * @return 新的结束时间(HH:mm格式)
+     */
+    private String calculateNewEndTime(String addTimeStart, Integer addTimeMinutes) {
+        try {
+            // 解析加时开始时间
+            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
+            Date startDate = sdf.parse(addTimeStart);
+            
+            // 加上加时分钟数
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(startDate);
+            calendar.add(Calendar.MINUTE, addTimeMinutes);
+            
+            // 格式化为HH:mm
+            return sdf.format(calendar.getTime());
+        } catch (ParseException e) {
+            log.error("计算新的结束时间失败,addTimeStart={}, addTimeMinutes={}", addTimeStart, addTimeMinutes, e);
+            throw new RuntimeException("时间计算失败:" + e.getMessage());
+        }
+    }
+
+    @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, String jingdu, String weidu) {
+        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.getStoreInfoVoWithDistanceFields(order.getStoreId(), jingdu, weidu));
+            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());
+        }
+
+        // 将 /store/reservationOrder/page 的页面展示逻辑写入详情:复用 page 接口数据
+        ReservationOrderPageVo pageVo = reservationOrderPageService.getPageByOrderId(orderId);
+        if (pageVo != null) {
+            copyPageVoToDetailVo(pageVo, vo);
+        }
+        vo.setRefundType(order.getRefundType());
+        if (order.getReservationId() != null) {
+            UserReservation r = this.getById(order.getReservationId());
+            if (r != null) {
+                vo.setMerchantCancelReason(r.getReason());
+            }
+        }
+        return vo;
+    }
+
+    @Override
+    public int markReservationTimeoutByEndTime() {
+        // 关联查询:订单待使用 + 预约结束时间已过,在库内一次筛出需更新的 reservation_id
+        List<Integer> toUpdateReservationIds = baseMapper.listReservationIdsForTimeoutMark();
+        if (toUpdateReservationIds == null || toUpdateReservationIds.isEmpty()) {
+            return 0;
+        }
+        Date now = new Date();
+        // 1. 更新 user_reservation.status = 未到店超时(4)
+        this.update(new LambdaUpdateWrapper<UserReservation>()
+                .in(UserReservation::getId, toUpdateReservationIds)
+                .set(UserReservation::getStatus, STATUS_NO_SHOW_TIMEOUT)
+                .set(UserReservation::getUpdatedTime, now));
+        // 2. 更新 user_reservation_order.order_status = 已过期(3)
+        userReservationOrderService.update(new LambdaUpdateWrapper<UserReservationOrder>()
+                .in(UserReservationOrder::getReservationId, toUpdateReservationIds)
+                .eq(UserReservationOrder::getOrderStatus, ORDER_STATUS_TO_USE)
+                .set(UserReservationOrder::getOrderStatus, ORDER_STATUS_EXPIRED)
+                .set(UserReservationOrder::getUpdatedTime, now));
+        log.info("预订未到店超时定时任务:更新 reservationIds={} 条为未到店超时/订单已过期", toUpdateReservationIds.size());
+        return toUpdateReservationIds.size();
+    }
+
+    /** 将页面 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();
+        }
+        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);
+    }
+}

+ 53 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategy.java

@@ -0,0 +1,53 @@
+package shop.alien.store.strategy.merchantPayment;
+
+import shop.alien.entity.result.R;
+
+import java.util.Map;
+
+/**
+ * 商户支付策略接口(预订订单等,使用 StorePaymentConfig 按门店配置)
+ * 与 strategy.payment 现有接口隔离,不影响原有 /payment 接口。
+ *
+ * @author system
+ */
+public interface MerchantPaymentStrategy {
+
+    /**
+     * 创建预支付(使用门店支付配置)
+     *
+     * @param storeId    门店ID
+     * @param orderId    预订订单ID user_reservation_order.id
+     * @param amountYuan 支付金额(元,字符串)
+     * @param subject    订单描述
+     * @param userId     用户ID
+     * @return 含 orderStr、outTradeNo、orderSn、orderId 等
+     */
+    R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId);
+
+    /**
+     * 查询支付状态(根据商户订单号查第三方并更新订单与支付单)
+     *
+     * @param storeId    门店ID
+     * @param outTradeNo 商户订单号
+     * @return 支付成功返回 R.success,否则 R.fail
+     */
+    R<Object> queryPayStatus(Integer storeId, String outTradeNo);
+
+    /**
+     * 退款
+     *
+     * @param storeId      门店ID
+     * @param outTradeNo   商户订单号
+     * @param refundAmount 退款金额(元,字符串)
+     * @param refundReason 退款原因
+     * @return 成功 R.data("退款成功"),失败 R.fail
+     */
+    R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason, Integer refundType);
+
+    /**
+     * 策略类型,如 alipay、wechatPay
+     *
+     * @return 支付类型
+     */
+    String getType();
+}

+ 47 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategyFactory.java

@@ -0,0 +1,47 @@
+package shop.alien.store.strategy.merchantPayment;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 商户支付策略工厂(预订订单等,按门店配置)
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+public class MerchantPaymentStrategyFactory {
+
+    @Autowired
+    private List<MerchantPaymentStrategy> merchantPaymentStrategies;
+
+    private final Map<String, MerchantPaymentStrategy> strategyMap = new HashMap<>();
+
+    @PostConstruct
+    public void init() {
+        if (merchantPaymentStrategies != null && !merchantPaymentStrategies.isEmpty()) {
+            for (MerchantPaymentStrategy strategy : merchantPaymentStrategies) {
+                strategyMap.put(strategy.getType(), strategy);
+                log.info("注册商户支付策略: {} -> {}", strategy.getType(), strategy.getClass().getSimpleName());
+            }
+        }
+    }
+
+    public MerchantPaymentStrategy getStrategy(String type) {
+        MerchantPaymentStrategy strategy = strategyMap.get(type);
+        if (strategy == null) {
+            throw new IllegalArgumentException("不支持的商户支付类型: " + type);
+        }
+        return strategy;
+    }
+
+    public boolean supports(String type) {
+        return strategyMap.containsKey(type);
+    }
+}

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

@@ -0,0 +1,403 @@
+package shop.alien.store.strategy.merchantPayment.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AlipayTradeAppPayModel;
+import com.alipay.api.domain.AlipayTradeRefundModel;
+import com.alipay.api.request.AlipayTradeAppPayRequest;
+import com.alipay.api.request.AlipayTradeQueryRequest;
+import com.alipay.api.request.AlipayTradeRefundRequest;
+import com.alipay.api.response.AlipayTradeAppPayResponse;
+import com.alipay.api.response.AlipayTradeQueryResponse;
+import com.alipay.api.response.AlipayTradeRefundResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.RefundRecord;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.store.service.*;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 商户支付宝支付策略(预订订单,使用 StorePaymentConfig 按门店配置)
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrategy {
+
+    /**
+     * 支付宝网关地址
+     */
+    @Value("${payment.aliPay.host}")
+    private String aliPayHost;
+
+    /** 预支付结果 Redis 缓存 key 前缀 */
+    private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:alipay:prepay:order:";
+    /** 预支付缓存过期时间(秒),与支付宝预支付单 15 分钟一致 */
+    private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    /** 防抖 key 前缀:同一订单在有效期内仅允许一次创建预支付 */
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:alipay:prepay:debounce:order:";
+    /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final UserReservationOrderService userReservationOrderService;
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+    private final RefundRecordAsyncService refundRecordAsyncService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
+    private final UserReservationService userReservationService;
+    private final StringRedisTemplate stringRedisTemplate;
+
+    @Override
+    public R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId) {
+        if (storeId == null || orderId == null) {
+            return R.fail("门店ID和订单ID不能为空");
+        }
+        if (StringUtils.isBlank(amountYuan)) {
+            return R.fail("支付金额不能为空");
+        }
+        BigDecimal amountYuanBD;
+        try {
+            amountYuanBD = new BigDecimal(amountYuan).setScale(2, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return R.fail("支付金额格式不正确");
+        }
+        if (amountYuanBD.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付金额必须大于0");
+        }
+        if (StringUtils.isBlank(subject)) {
+            return R.fail("订单描述不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        if (StringUtils.isBlank(config.getAppSecretCert())
+                || config.getAppPublicCert() == null || config.getAppPublicCert().length == 0
+                || config.getAlipayPublicCert() == null || config.getAlipayPublicCert().length == 0
+                || config.getAlipayRootCert() == null || config.getAlipayRootCert().length == 0) {
+            return R.fail("门店支付配置不完整(缺少应用私钥或证书)");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (!order.getStoreId().equals(storeId)) {
+            return R.fail("订单与门店不匹配");
+        }
+        if (order.getPaymentStatus() != null && order.getPaymentStatus() == 1) {
+            return R.fail("订单已支付");
+        }
+        // 校验支付金额与订单订金金额一致(考虑精度,保留2位小数)
+        BigDecimal depositAmount = order.getDepositAmount() != null ? order.getDepositAmount() : BigDecimal.ZERO;
+        if (amountYuanBD.compareTo(depositAmount.setScale(2, RoundingMode.HALF_UP)) != 0) {
+            return R.fail("支付金额与订单订金金额不一致");
+        }
+
+        // 再次调用时优先从 Redis 获取已生成的预支付信息(未过期则直接返回)
+        String redisKey = REDIS_PREPAY_KEY_PREFIX + orderId;
+        String cached = stringRedisTemplate.opsForValue().get(redisKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> data = JSON.parseObject(cached, Map.class);
+                if (data != null && !data.isEmpty()) {
+                    log.info("商户预订订单预支付命中缓存,orderId={}", orderId);
+                    return R.data(data);
+                }
+            } catch (Exception e) {
+                log.warn("解析预支付缓存失败,将重新发起预支付,orderId={}", orderId, e);
+            }
+        }
+
+        // 防抖:同一订单在有效期内只允许一次创建预支付,防止重复调用
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + orderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            log.warn("商户支付宝预支付防抖拦截,orderId={}", orderId);
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
+        // 未命中缓存:先按订单ID+支付类型将原支付单逻辑删除,再生成新预支付(仅删除同 pay_type)
+        int deleted = merchantPaymentOrderService.logicDeleteByOrderIdAndPayType(orderId, PaymentEnum.ALIPAY.getType());
+        if (deleted > 0) {
+            log.info("未命中缓存,已逻辑删除该订单下支付宝支付单 {} 条,orderId={}", deleted, orderId);
+        }
+
+        String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+
+        MerchantPaymentOrder paymentOrder = new MerchantPaymentOrder();
+        paymentOrder.setPaymentNo(merchantPaymentOrderService.generatePaymentNo());
+        paymentOrder.setOrderType("reservation_order");
+        paymentOrder.setOrderId(order.getId());
+        paymentOrder.setOrderSn(order.getOrderSn());
+        paymentOrder.setStoreId(storeId);
+        paymentOrder.setPayType(PaymentEnum.ALIPAY.getType());
+        paymentOrder.setOutTradeNo(outTradeNo);
+        paymentOrder.setPayAmount(amountYuanBD);
+        paymentOrder.setPayStatus(0);
+        paymentOrder.setPayerUserId(userId);
+        paymentOrder.setSubject(subject);
+        paymentOrder.setCreatedTime(new Date());
+        paymentOrder.setUpdatedTime(new Date());
+        merchantPaymentOrderService.save(paymentOrder);
+
+        try {
+            com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
+            AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
+            model.setOutTradeNo(outTradeNo);
+            model.setTotalAmount(amountYuanBD.toPlainString());
+            model.setSubject(subject);
+            // 预支付单有效时间 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()) {
+                return R.fail("预支付失败:" + response.getSubMsg());
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("orderStr", orderStr);
+            data.put("outTradeNo", outTradeNo);
+            data.put("orderSn", order.getOrderSn());
+            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={}, appId={}, aliPayHost={}", storeId, order.getOrderSn(), outTradeNo, config.getAppId(), aliPayHost);
+            return R.data(data);
+        } catch (AlipayApiException e) {
+            log.error("商户预订订单预支付异常,storeId={}, orderId={}", storeId, orderId, e);
+            return R.fail("预支付失败:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("构建支付宝配置异常,storeId={}", storeId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        // 本地已标记已支付,直接返回成功,不再调用支付宝
+        if (order.getPaymentStatus() != null && order.getPaymentStatus() == 1) {
+            return R.success("支付成功");
+        }
+        try {
+            com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+            JSONObject bizContent = new JSONObject();
+            bizContent.put("out_trade_no", outTradeNo);
+            request.setBizContent(bizContent.toJSONString());
+            AlipayTradeQueryResponse response = client.certificateExecute(request);
+            if (!response.isSuccess()) {
+                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)) {
+                Date now = new Date();
+                paymentOrder.setPayStatus(1);
+                paymentOrder.setTradeNo(response.getTradeNo());
+                paymentOrder.setPayTime(now);
+                paymentOrder.setUpdatedTime(now);
+                merchantPaymentOrderService.updateById(paymentOrder);
+
+                order.setPaymentStatus(1);
+                order.setPayTime(now);
+                order.setOutTradeNo(outTradeNo);
+                order.setPaymentMethod("alipay");
+                order.setOrderStatus(1);
+                if (StringUtils.isBlank(order.getVerificationCode())) {
+                    order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
+                }
+                order.setUpdatedTime(now);
+                userReservationOrderService.updateById(order);
+                if (order.getReservationId() != null) {
+                    UserReservation reservation = userReservationService.getById(order.getReservationId());
+                    if (reservation != null) {
+                        reservation.setStatus(1);
+                        userReservationService.updateById(reservation);
+                    }
+                }
+                reservationOrderPaymentTimeoutService.cancelReservationOrderPaymentTimeout(order.getOrderSn());
+                return R.success("支付成功");
+            }
+            if ("TRADE_CLOSED".equals(tradeStatus)) {
+                return R.fail("交易已关闭");
+            }
+            if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
+                return R.fail("等待买家付款");
+            }
+            return R.success("订单状态:" + tradeStatus);
+        } catch (AlipayApiException e) {
+            log.error("查询商户订单支付状态异常,outTradeNo={}", outTradeNo, e);
+            return R.fail("查询异常:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("构建支付宝配置异常,storeId={}", storeId, e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason, Integer refundType) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        if (StringUtils.isBlank(refundAmount) || new BigDecimal(refundAmount).compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("退款金额必须大于0");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (order.getPaymentStatus() == null || order.getPaymentStatus() != 1) {
+            return R.fail("订单未支付或已退款,无法退款");
+        }
+        try {
+            com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+            AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+            model.setOutTradeNo(outTradeNo);
+            model.setRefundAmount(refundAmount);
+            model.setRefundReason(StringUtils.isNotBlank(refundReason) ? refundReason : "用户申请退款");
+            request.setBizModel(model);
+            AlipayTradeRefundResponse response = client.certificateExecute(request);
+            if (!response.isSuccess()) {
+                return R.fail("退款失败:" + response.getSubMsg());
+            }
+            JSONObject responseBody = JSONObject.parseObject(response.getBody());
+            JSONObject refundResponse = responseBody != null ? responseBody.getJSONObject("alipay_trade_refund_response") : null;
+            String tradeNo = refundResponse != null ? refundResponse.getString("trade_no") : null;
+
+            Date now = new Date();
+            BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
+            paymentOrder.setPayStatus(3);
+            paymentOrder.setUpdatedTime(now);
+
+            order.setOrderStatus(7);
+            order.setPaymentStatus(2);
+            order.setRefundAmount(refundAmountDecimal);
+            order.setRefundTime(now);
+            order.setRefundReason(refundReason);
+            order.setUpdatedTime(now);
+            order.setRefundType(refundType);
+            userReservationOrderService.updateById(order);
+
+            RefundRecord record = new RefundRecord();
+            record.setPayType(PaymentEnum.ALIPAY.getType());
+            record.setOutTradeNo(outTradeNo);
+            record.setTransactionId(tradeNo);
+            record.setOutRefundNo(UniqueRandomNumGenerator.generateUniqueCode(19));
+            record.setRefundStatus("SUCCESS");
+            record.setRefundAmount(refundAmountDecimal.multiply(new BigDecimal(100)).longValue());
+            record.setRefundReason(refundReason);
+            record.setOrderId(String.valueOf(order.getId()));
+            record.setStoreId(storeId);
+            record.setUserId(order.getUserId());
+            record.setCreatedTime(now);
+            record.setCreatedUserId(order.getUserId());
+            record.setUpdatedUserId(order.getUserId());
+            record.setDeleteFlag(0);
+            refundRecordAsyncService.completeRefundAsync(paymentOrder, record);
+
+            log.info("商户预订订单退款成功,outTradeNo={}", outTradeNo);
+            return R.data("退款成功");
+        } catch (AlipayApiException e) {
+            log.error("商户预订订单退款异常,outTradeNo={}", outTradeNo, e);
+            return R.fail("退款失败:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("构建支付宝配置异常,storeId={}", storeId, e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.ALIPAY.getType();
+    }
+
+    private com.alipay.api.AlipayConfig buildAlipayConfigFromStore(StorePaymentConfig config) {
+        com.alipay.api.AlipayConfig alipayConfig = new com.alipay.api.AlipayConfig();
+        alipayConfig.setServerUrl(aliPayHost);
+        alipayConfig.setAppId(config.getAppId());
+        alipayConfig.setPrivateKey(config.getAppSecretCert());
+        alipayConfig.setFormat("json");
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+
+        // 使用证书内容(字符串)设置,避免每次请求写临时文件
+        if (config.getAppPublicCert() != null && config.getAppPublicCert().length > 0) {
+            alipayConfig.setAppCertContent(new String(config.getAppPublicCert(), StandardCharsets.UTF_8));
+        }
+        if (config.getAlipayPublicCert() != null && config.getAlipayPublicCert().length > 0) {
+            alipayConfig.setAlipayPublicCertContent(new String(config.getAlipayPublicCert(), StandardCharsets.UTF_8));
+        }
+        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;
+    }
+}

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

@@ -0,0 +1,505 @@
+package shop.alien.store.strategy.merchantPayment.impl;
+
+import com.alibaba.fastjson.JSON;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.RefundRecord;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.UserReservation;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.store.service.MerchantPaymentOrderService;
+import shop.alien.store.service.RefundRecordAsyncService;
+import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
+import shop.alien.store.service.UserReservationOrderService;
+import shop.alien.store.service.UserReservationService;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl;
+import shop.alien.store.util.WXPayUtility;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 商户微信支付策略(预订订单,使用 StorePaymentConfig 按门店配置,业务逻辑与支付宝一致)
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrategy {
+
+    private static final String POSTMETHOD = "POST";
+    private static final String GETMETHOD = "GET";
+    private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:wechat:prepay:order:";
+    private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    /** 防抖 key 前缀:同一订单在有效期内仅允许一次创建预支付 */
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:wechat:prepay:debounce:order:";
+    /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+
+    @Value("${payment.wechatPay.host:https://api.mch.weixin.qq.com}")
+    private String wechatPayApiHost;
+    @Value("${payment.wechatPay.prePayPath:/v3/pay/transactions/app}")
+    private String prePayPath;
+    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath:/v3/pay/transactions/out-trade-no/{out_trade_no}}")
+    private String searchOrderByOutTradeNoPath;
+    @Value("${payment.wechatPay.refundPath:/v3/refund/domestic/refunds}")
+    private String refundPath;
+    @Value("${payment.wechatPay.business.prePayNotifyUrl:}")
+    private String prePayNotifyUrl;
+    @Value("${payment.wechatPay.business.refundNotifyUrl:}")
+    private String refundNotifyUrl;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final UserReservationOrderService userReservationOrderService;
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+    private final RefundRecordAsyncService refundRecordAsyncService;
+    private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
+    private final UserReservationService userReservationService;
+    private final StringRedisTemplate stringRedisTemplate;
+
+    @Override
+    public R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId) {
+        if (storeId == null || orderId == null) {
+            return R.fail("门店ID和订单ID不能为空");
+        }
+        if (StringUtils.isBlank(amountYuan) || new BigDecimal(amountYuan).compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付金额必须大于0");
+        }
+        if (StringUtils.isBlank(subject)) {
+            return R.fail("订单描述不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        if (StringUtils.isBlank(config.getWechatAppId()) || StringUtils.isBlank(config.getWechatMchId())
+                || StringUtils.isBlank(config.getMerchantSerialNumber()) || StringUtils.isBlank(config.getApiV3Key())
+                || StringUtils.isBlank(config.getWechatPayPublicKeyId())
+                || config.getWechatPrivateKeyFile() == null || config.getWechatPrivateKeyFile().length == 0
+                || config.getWechatPayPublicKeyFile() == null || config.getWechatPayPublicKeyFile().length == 0) {
+            return R.fail("门店微信支付配置不完整");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (!order.getStoreId().equals(storeId)) {
+            return R.fail("订单与门店不匹配");
+        }
+        if (order.getPaymentStatus() != null && order.getPaymentStatus() == 1) {
+            return R.fail("订单已支付");
+        }
+
+        String redisKey = REDIS_PREPAY_KEY_PREFIX + orderId;
+        String cached = stringRedisTemplate.opsForValue().get(redisKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> data = JSON.parseObject(cached, Map.class);
+                if (data != null && !data.isEmpty()) {
+                    log.info("商户预订订单微信预支付命中缓存,orderId={}", orderId);
+                    return R.data(data);
+                }
+            } catch (Exception e) {
+                log.warn("解析微信预支付缓存失败,将重新发起,orderId={}", orderId, e);
+            }
+        }
+
+        // 防抖:同一订单在有效期内只允许一次创建预支付,防止重复调用
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + orderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            log.warn("商户微信预支付防抖拦截,orderId={}", orderId);
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
+        int deleted = merchantPaymentOrderService.logicDeleteByOrderIdAndPayType(orderId, PaymentEnum.WECHAT_PAY.getType());
+        if (deleted > 0) {
+            log.info("未命中缓存,已逻辑删除该订单下微信支付单 {} 条,orderId={}", deleted, orderId);
+        }
+
+        String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        BigDecimal amount = new BigDecimal(amountYuan);
+
+        MerchantPaymentOrder paymentOrder = new MerchantPaymentOrder();
+        paymentOrder.setPaymentNo(merchantPaymentOrderService.generatePaymentNo());
+        paymentOrder.setOrderType("reservation_order");
+        paymentOrder.setOrderId(order.getId());
+        paymentOrder.setOrderSn(order.getOrderSn());
+        paymentOrder.setStoreId(storeId);
+        paymentOrder.setPayType(PaymentEnum.WECHAT_PAY.getType());
+        paymentOrder.setOutTradeNo(outTradeNo);
+        paymentOrder.setPayAmount(amount);
+        paymentOrder.setPayStatus(0);
+        paymentOrder.setPayerUserId(userId);
+        paymentOrder.setSubject(subject);
+        paymentOrder.setCreatedTime(new Date());
+        paymentOrder.setUpdatedTime(new Date());
+        merchantPaymentOrderService.save(paymentOrder);
+
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
+
+            WeChatPaymentStrategyImpl.CommonPrepayRequest request = new WeChatPaymentStrategyImpl.CommonPrepayRequest();
+            request.appid = config.getWechatAppId();
+            request.mchid = config.getWechatMchId();
+            request.description = subject;
+            request.outTradeNo = outTradeNo;
+            request.notifyUrl = StringUtils.isNotBlank(prePayNotifyUrl) ? prePayNotifyUrl : "";
+            request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
+            request.amount.total = amount.multiply(new BigDecimal(100)).longValue();
+            request.amount.currency = "CNY";
+
+            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = prePayOrderRun(config, privateKey, request);
+            if (response == null || StringUtils.isBlank(response.prepayId)) {
+                return R.fail("微信预支付失败");
+            }
+
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String message = String.format("%s\n%s\n%s\n%s\n", config.getWechatAppId(), timestamp, nonce, response.prepayId);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            String signStr = Base64.getEncoder().encodeToString(sign.sign());
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("outTradeNo", outTradeNo);
+            data.put("orderSn", order.getOrderSn());
+            data.put("orderId", order.getId());
+            data.put("paymentNo", paymentOrder.getPaymentNo());
+            data.put("prepayId", response.prepayId);
+            data.put("appId", config.getWechatAppId());
+            data.put("mchId", config.getWechatMchId());
+            data.put("sign", signStr);
+            data.put("timestamp", String.valueOf(timestamp));
+            data.put("nonce", nonce);
+            data.put("orderStr", "");
+
+            String cacheJson = JSON.toJSONString(data);
+            stringRedisTemplate.opsForValue().set(redisKey, cacheJson != null ? cacheJson : "{}", REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.info("商户预订订单微信预支付成功并写入缓存,storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
+            return R.data(data);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("商户预订订单微信预支付异常,storeId={}, orderId={}", storeId, orderId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("商户微信预支付异常,storeId={}", storeId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        log.info("queryPayStatus 开始 storeId={} outTradeNo={}", storeId, outTradeNo);
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=参数为空", storeId, outTradeNo);
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=未配置支付参数", storeId, outTradeNo);
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=支付单不存在", storeId, outTradeNo);
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=预订订单不存在 orderId={}", storeId, outTradeNo, paymentOrder.getOrderId());
+            return R.fail("预订订单不存在");
+        }
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
+            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
+
+            WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest req = new WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest();
+            req.transactionId = outTradeNo;
+            req.mchid = config.getWechatMchId();
+            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = searchOrderRun(config, privateKey, wechatPayPublicKey, req);
+            if (response == null) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=微信返回为空", storeId, outTradeNo);
+                return R.fail("查询失败");
+            }
+            log.info("queryPayStatus 微信查单结果 storeId={} outTradeNo={} tradeState={} tradeStateDesc={}", storeId, outTradeNo, response.tradeState, response.tradeStateDesc);
+            if ("SUCCESS".equals(response.tradeState)) {
+                Date now = new Date();
+                paymentOrder.setPayStatus(1);
+                paymentOrder.setTradeNo(response.transactionId);
+                paymentOrder.setPayTime(now);
+                paymentOrder.setUpdatedTime(now);
+                merchantPaymentOrderService.updateById(paymentOrder);
+
+                order.setPaymentStatus(1);
+                order.setPayTime(now);
+                order.setOutTradeNo(outTradeNo);
+                order.setPaymentMethod("wechatPay");
+                order.setOrderStatus(1);
+                if (StringUtils.isBlank(order.getVerificationCode())) {
+                    order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
+                }
+                order.setUpdatedTime(now);
+                userReservationOrderService.updateById(order);
+                if (order.getReservationId() != null) {
+                    UserReservation reservation = userReservationService.getById(order.getReservationId());
+                    if (reservation != null) {
+                        reservation.setStatus(1);
+                        userReservationService.updateById(reservation);
+                    }
+                }
+                reservationOrderPaymentTimeoutService.cancelReservationOrderPaymentTimeout(order.getOrderSn());
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=success tradeState=SUCCESS orderId={}", storeId, outTradeNo, order.getId());
+                return R.success("支付成功");
+            }
+            if ("CLOSED".equals(response.tradeState)) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=交易已关闭", storeId, outTradeNo);
+                return R.fail("交易已关闭");
+            }
+            if ("NOTPAY".equals(response.tradeState) || "USERPAYING".equals(response.tradeState)) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=等待用户付款 tradeState={}", storeId, outTradeNo, response.tradeState);
+                return R.fail("等待用户付款");
+            }
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=其他状态 tradeState={} tradeStateDesc={}", storeId, outTradeNo, response.tradeState, response.tradeStateDesc);
+            return R.fail("订单状态:" + (response.tradeStateDesc != null ? response.tradeStateDesc : response.tradeState));
+        } catch (WXPayUtility.ApiException e) {
+            log.error("queryPayStatus 异常 storeId={} outTradeNo={} ApiException statusCode={} body={}", storeId, outTradeNo, e.getStatusCode(), e.getBody(), e);
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=ApiException", storeId, outTradeNo);
+            return R.fail("查询异常:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("queryPayStatus 异常 storeId={} outTradeNo={}", storeId, outTradeNo, e);
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=Exception", storeId, outTradeNo);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason, Integer refundType) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        if (StringUtils.isBlank(refundAmount) || new BigDecimal(refundAmount).compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("退款金额必须大于0");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (order.getPaymentStatus() == null || order.getPaymentStatus() != 1) {
+            return R.fail("订单未支付或已退款,无法退款");
+        }
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
+            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
+
+            WeChatPaymentStrategyImpl.CreateRequest request = new WeChatPaymentStrategyImpl.CreateRequest();
+            request.outTradeNo = outTradeNo;
+            request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+            request.reason = StringUtils.isNotBlank(refundReason) ? refundReason : "用户申请退款";
+            request.notifyUrl = StringUtils.isNotBlank(refundNotifyUrl) ? refundNotifyUrl : "";
+            request.amount = new WeChatPaymentStrategyImpl.AmountReq();
+            request.amount.refund = new BigDecimal(refundAmount).multiply(new BigDecimal(100)).longValue();
+            // 微信 V3 amount.total 单位为分,depositAmount 为元,需 *100 转分
+            request.amount.total = order.getDepositAmount().multiply(new BigDecimal(100)).longValue();
+            request.amount.currency = "CNY";
+
+            WeChatPaymentStrategyImpl.Refund response = null;
+            WXPayUtility.ApiException lastApiException = null;
+            int maxAttempts = 3;
+            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+                try {
+                    response = refundRun(config, privateKey, wechatPayPublicKey, request);
+                    break;
+                } catch (WXPayUtility.ApiException e) {
+                    lastApiException = e;
+                    if (e.getStatusCode() == 429 && attempt < maxAttempts) {
+                        long delayMs = 2000L * attempt;
+                        log.warn("微信退款 429 限频,{}ms 后重试,第 {}/{} 次,outTradeNo={}", delayMs, attempt, maxAttempts, outTradeNo);
+                        try {
+                            Thread.sleep(delayMs);
+                        } catch (InterruptedException ie) {
+                            Thread.currentThread().interrupt();
+                            return R.fail("操作过于频繁,请稍后再试");
+                        }
+                    } else {
+                        if (e.getStatusCode() == 429) {
+                            break;
+                        }
+                        throw e;
+                    }
+                }
+            }
+            if (response == null) {
+                String msg = lastApiException != null && lastApiException.getStatusCode() == 429
+                        ? (StringUtils.isNotBlank(lastApiException.getErrorMessage()) ? lastApiException.getErrorMessage() : "操作过于频繁,请稍后再试")
+                        : "退款失败";
+                return R.fail(msg);
+            }
+            String status = response.status != null ? response.status.name() : "";
+            if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status)) {
+                return R.fail("退款失败:" + status);
+            }
+
+            Date now = new Date();
+            BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
+            paymentOrder.setPayStatus(3);
+            paymentOrder.setUpdatedTime(now);
+
+            order.setOrderStatus(7);
+            order.setPaymentStatus(2);
+            order.setRefundAmount(refundAmountDecimal);
+            order.setRefundTime(now);
+            order.setRefundReason(refundReason);
+            order.setUpdatedTime(now);
+            order.setRefundType(refundType);
+            userReservationOrderService.updateById(order);
+
+            RefundRecord record = new RefundRecord();
+            record.setPayType(PaymentEnum.WECHAT_PAY.getType());
+            record.setOutTradeNo(outTradeNo);
+            record.setTransactionId(response.transactionId);
+            record.setOutRefundNo(response.outRefundNo != null ? response.outRefundNo : request.outRefundNo);
+            record.setRefundId(response.refundId);
+            record.setRefundStatus("SUCCESS");
+            record.setRefundAmount(refundAmountDecimal.multiply(new BigDecimal(100)).longValue());
+            record.setRefundReason(refundReason);
+            record.setOrderId(String.valueOf(order.getId()));
+            record.setStoreId(storeId);
+            record.setUserId(order.getUserId());
+            record.setCreatedTime(now);
+            record.setCreatedUserId(order.getUserId());
+            record.setUpdatedUserId(order.getUserId());
+            record.setDeleteFlag(0);
+            refundRecordAsyncService.completeRefundAsync(paymentOrder, record);
+
+            log.info("商户预订订单微信退款成功,outTradeNo={}", outTradeNo);
+            return R.data("退款成功");
+        } catch (WXPayUtility.ApiException e) {
+            log.error("商户预订订单微信退款异常,outTradeNo={}", outTradeNo, e);
+            if (e.getStatusCode() == 429) {
+                return R.fail(StringUtils.isNotBlank(e.getErrorMessage()) ? e.getErrorMessage() : "操作过于频繁,请稍后再试");
+            }
+            return R.fail("退款失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("商户微信退款异常,storeId={}", storeId, e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY.getType();
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse prePayOrderRun(StorePaymentConfig config,
+                                                                                  PrivateKey privateKey,
+                                                                                  WeChatPaymentStrategyImpl.CommonPrepayRequest request) throws IOException {
+        String uri = prePayPath;
+        String reqBody = WXPayUtility.toJson(request);
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                PublicKey publicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), publicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse searchOrderRun(StorePaymentConfig config,
+                                                                              PrivateKey privateKey,
+                                                                              PublicKey wechatPayPublicKey,
+                                                                              WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest request) throws IOException {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
+        Map<String, Object> args = new HashMap<>();
+        args.put("mchid", config.getWechatMchId());
+        String queryString = WXPayUtility.urlEncode(args);
+        if (!queryString.isEmpty()) {
+            uri = uri + "?" + queryString;
+        }
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        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()
+                .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) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.Refund refundRun(StorePaymentConfig config,
+                                                       PrivateKey privateKey,
+                                                       PublicKey wechatPayPublicKey,
+                                                       WeChatPaymentStrategyImpl.CreateRequest request) throws IOException {
+        String uri = refundPath;
+        String reqBody = WXPayUtility.toJson(request);
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.Refund.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+
+    /** 仅在使用处将证书 byte[] 转为 String(UTF-8),不参与存储,保证存取一致 */
+    private static String bytesToUtf8String(byte[] bytes) {
+        return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
+    }
+}

+ 198 - 0
alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java

@@ -60,6 +60,13 @@ public class AliSms {
     @Value("${ali.sms.templateAccountCode:}")
     private String templateAccountCode;
 
+    @Value("${ali.sms.templatereFundCode:}")
+    private String templatereFundCode;
+
+    @Value("${ali.sms.templatereRefundCode:}")
+    private String templatereRefundCode;
+
+
 
 
     /**
@@ -347,4 +354,195 @@ public class AliSms {
         }
     }
 
+    /**
+     * 发送商家预约取消短信
+     * 模板内容:您在${dateTime},预订了${storeName},的${number}桌位,已被商家取消,取消原因:${info},请您重新预订
+     * 模板CODE:SMS_501820954
+     * 
+     * @param phone        用户手机号
+     * @param dateTime     预约时间(格式:2026-01-01 14:00)
+     * @param storeName    店铺名称
+     * @param tableNumber  桌号名称(如:A01)
+     * @param cancelReason 取消原因
+     * @return 1-发送成功, null-发送失败
+     */
+    public Integer sendCancelReservationSms(String phone, String dateTime, String storeName, String tableNumber, String cancelReason) {
+        log.info("AliSms.sendCancelReservationSms?phone={}&dateTime={}&storeName={}&tableNumber={}&cancelReason={}", 
+                phone, dateTime, storeName, tableNumber, cancelReason);
+        try {
+            // 参数校验
+            if (phone == null || phone.trim().isEmpty()) {
+                log.warn("发送商家预约取消短信失败:手机号不能为空");
+                return null;
+            }
+            if (dateTime == null || dateTime.trim().isEmpty()) {
+                log.warn("发送商家预约取消短信失败:预约时间不能为空");
+                return null;
+            }
+            if (storeName == null || storeName.trim().isEmpty()) {
+                log.warn("发送商家预约取消短信失败:店铺名称不能为空");
+                return null;
+            }
+            if (tableNumber == null || tableNumber.trim().isEmpty()) {
+                log.warn("发送商家预约取消短信失败:桌号名称不能为空");
+                return null;
+            }
+            if (cancelReason == null || cancelReason.trim().isEmpty()) {
+                log.warn("发送商家预约取消短信失败:取消原因不能为空");
+                return null;
+            }
+
+            Config config = new Config()
+                    .setEndpoint(endPoint)
+                    .setAccessKeyId(accessKeyId)
+                    .setAccessKeySecret(accessKeySecret);
+
+            // 构建模板参数JSON,必须符合阿里云短信模板参数格式
+            // 模板内容:您在${dateTime},预订了${storeName},的${number}桌位,已被商家取消,取消原因:${info},请您重新预订
+            // 变量:dateTime(时间)、storeName(店铺名称)、number(桌号名称)、info(取消原因)
+            String templateParam = String.format("{\"dateTime\":\"%s\",\"storeName\":\"%s\",\"number\":\"%s\",\"content\":\"%s\"}",
+                    escapeJsonString(dateTime), 
+                    escapeJsonString(storeName),
+                    escapeJsonString(tableNumber),
+                    escapeJsonString(cancelReason));
+
+            // 使用商家预约取消短信模板代码
+            String cancelTemplateCode = templatereFundCode;
+            if (cancelTemplateCode == null || cancelTemplateCode.trim().isEmpty()) {
+                cancelTemplateCode = "SMS_501820954"; // 默认模板代码
+            }
+
+            log.info("AliSms.sendCancelReservationSms 准备发送短信,phone={}, dateTime={}, storeName={}, tableNumber={}, cancelReason={}, templateCode={}, templateParam={}", 
+                    phone, dateTime, storeName, tableNumber, cancelReason, cancelTemplateCode, templateParam);
+
+            // 构建发送请求
+            SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                    // 设置签名
+                    .setSignName(signName)
+                    // 设置模板代码(商家预约取消短信模板)
+                    .setTemplateCode(cancelTemplateCode)
+                    // 设置手机号
+                    .setPhoneNumbers(phone)
+                    // 设置模板参数(必须是有效的JSON格式)
+                    .setTemplateParam(templateParam);
+            
+            // 运行时选择,可以设置不同的属性来配置运行时环境的参数。
+            RuntimeOptions runtime = new RuntimeOptions();
+            Client client = new Client(config);
+            // 调用阿里云短信API
+            SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
+            
+            String responseCode = sendSmsResponse.getBody().getCode();
+            String responseMessage = sendSmsResponse.getBody().getMessage();
+            String bizId = sendSmsResponse.getBody().getBizId(); // 业务ID,用于查询发送状态
+            
+            // 记录完整的响应信息
+            log.info("AliSms.sendCancelReservationSms API响应详情,phone={}, responseCode={}, responseMessage={}, bizId={}", 
+                    phone, responseCode, responseMessage, bizId);
+            
+            if (!"OK".equals(responseCode)) {
+                log.error("AliSms.sendCancelReservationSms 短信发送失败,phone={}, dateTime={}, storeName={}, tableNumber={}, cancelReason={}, templateCode={}, templateParam={}, responseCode={}, responseMessage={}, bizId={}", 
+                        phone, dateTime, storeName, tableNumber, cancelReason, cancelTemplateCode, templateParam, responseCode, responseMessage, bizId);
+                return null;
+            }
+            
+            // 即使返回OK,也可能存在其他问题,记录详细信息便于排查
+            log.info("AliSms.sendCancelReservationSms 短信发送成功(API返回OK),phone={}, dateTime={}, storeName={}, tableNumber={}, cancelReason={}, bizId={}, templateCode={}, templateParam={}", 
+                    phone, dateTime, storeName, tableNumber, cancelReason, bizId, cancelTemplateCode, templateParam);
+            
+            // 如果bizId为空,可能是异常情况
+            if (bizId == null || bizId.trim().isEmpty()) {
+                log.warn("AliSms.sendCancelReservationSms 警告:API返回OK但bizId为空,可能存在问题,phone={}, responseMessage={}", 
+                        phone, responseMessage);
+            }
+            
+            return 1;
+        } catch (Exception e) {
+            log.error("AliSms.sendCancelReservationSms ERROR phone={}, dateTime={}, storeName={}, tableNumber={}, cancelReason={}, Msg={}", 
+                    phone, dateTime, storeName, tableNumber, cancelReason, e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 发送订金退款短信
+     * 短信内容:您预付的订金已返还至您的支付账号,请注意查收
+     * 模板CODE:使用 templatereRefundCode 配置
+     * 
+     * @param phone 用户手机号
+     * @return 1-发送成功, null-发送失败
+     */
+    public Integer sendDepositRefundSms(String phone) {
+        log.info("AliSms.sendDepositRefundSms?phone={}", phone);
+        try {
+            // 参数校验
+            if (phone == null || phone.trim().isEmpty()) {
+                log.warn("发送订金退款短信失败:手机号不能为空");
+                return null;
+            }
+
+            Config config = new Config()
+                    .setEndpoint(endPoint)
+                    .setAccessKeyId(accessKeyId)
+                    .setAccessKeySecret(accessKeySecret);
+
+            // 使用订金退款短信模板代码
+            String refundTemplateCode = templatereRefundCode;
+            if (refundTemplateCode == null || refundTemplateCode.trim().isEmpty()) {
+                log.warn("发送订金退款短信失败:模板代码未配置");
+                return null;
+            }
+
+            log.info("AliSms.sendDepositRefundSms 准备发送短信,phone={}, templateCode={}", 
+                    phone, refundTemplateCode);
+
+            // 构建发送请求
+            // 如果模板不需要参数,可以传空JSON对象 {} 或者不传参数
+            SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                    // 设置签名
+                    .setSignName(signName)
+                    // 设置模板代码(订金退款短信模板)
+                    .setTemplateCode(refundTemplateCode)
+                    // 设置手机号
+                    .setPhoneNumbers(phone)
+                    // 设置模板参数(如果模板不需要参数,传空JSON对象)
+                    .setTemplateParam("{}");
+            
+            // 运行时选择,可以设置不同的属性来配置运行时环境的参数。
+            RuntimeOptions runtime = new RuntimeOptions();
+            Client client = new Client(config);
+            // 调用阿里云短信API
+            SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
+            
+            String responseCode = sendSmsResponse.getBody().getCode();
+            String responseMessage = sendSmsResponse.getBody().getMessage();
+            String bizId = sendSmsResponse.getBody().getBizId(); // 业务ID,用于查询发送状态
+            
+            // 记录完整的响应信息
+            log.info("AliSms.sendDepositRefundSms API响应详情,phone={}, responseCode={}, responseMessage={}, bizId={}", 
+                    phone, responseCode, responseMessage, bizId);
+            
+            if (!"OK".equals(responseCode)) {
+                log.error("AliSms.sendDepositRefundSms 短信发送失败,phone={}, templateCode={}, responseCode={}, responseMessage={}, bizId={}", 
+                        phone, refundTemplateCode, responseCode, responseMessage, bizId);
+                return null;
+            }
+            
+            // 即使返回OK,也可能存在其他问题,记录详细信息便于排查
+            log.info("AliSms.sendDepositRefundSms 短信发送成功(API返回OK),phone={}, bizId={}, templateCode={}", 
+                    phone, bizId, refundTemplateCode);
+            
+            // 如果bizId为空,可能是异常情况
+            if (bizId == null || bizId.trim().isEmpty()) {
+                log.warn("AliSms.sendDepositRefundSms 警告:API返回OK但bizId为空,可能存在问题,phone={}, responseMessage={}", 
+                        phone, responseMessage);
+            }
+            
+            return 1;
+        } catch (Exception e) {
+            log.error("AliSms.sendDepositRefundSms ERROR phone={}, Msg={}", phone, e.getMessage(), e);
+            return null;
+        }
+    }
+
 }

+ 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;
+}

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

@@ -0,0 +1,151 @@
+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 shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.entity.store.vo.StoreInfoVo;
+import shop.alien.entity.store.vo.UserReservationVo;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 按订单ID查询的详情:订单 + 门店信息 + 预订设置 + 预订信息 + 定桌信息 + 页面展示字段(与 /store/reservationOrder/page 一致)
+ *
+ * @author system
+ */
+@Data
+@ApiModel(value = "ReservationOrderDetailVo", description = "预订订单详情(订单+门店+预订设置+预订+定桌+页面展示)")
+public class ReservationOrderDetailVo {
+
+    @ApiModelProperty(value = "预订订单(user_reservation_order)")
+    private UserReservationOrder order;
+
+    @ApiModelProperty(value = "门店信息(含 distance、distance2、subwayName、storeLocation)")
+    private StoreInfoVo storeInfo;
+
+    @ApiModelProperty(value = "门店预订设置(store_booking_settings)")
+    private StoreBookingSettings storeBookingSettings;
+
+    @ApiModelProperty(value = "预订/预约信息")
+    private UserReservationVo reservation;
+
+    @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;
+
+    @ApiModelProperty(value = "商家取消原因(user_reservation.reason,商家取消时有值)")
+    private String merchantCancelReason;
+}

+ 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;
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä