فهرست منبع

Merge branch 'sit' of http://8.152.195.41:3000/alien/alien_cloud into sit

lutong 1 ماه پیش
والد
کامیت
bd9f18eb35
30فایلهای تغییر یافته به همراه685 افزوده شده و 78 حذف شده
  1. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/MerchantPaymentOrder.java
  2. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/RefundRecord.java
  3. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/UserReservationOrder.java
  4. 7 0
      alien-entity/src/main/java/shop/alien/mapper/UserReservationOrderMapper.java
  5. 4 1
      alien-entity/src/main/java/shop/alien/mapper/second/SecondEntrustUserMapper.java
  6. 3 5
      alien-entity/src/main/resources/mapper/StoreReservationMapper.xml
  7. 12 0
      alien-entity/src/main/resources/mapper/UserReservationOrderMapper.xml
  8. 8 0
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  9. 32 0
      alien-job/src/main/java/shop/alien/job/store/RefundRetryJob.java
  10. 5 4
      alien-second/src/main/java/shop/alien/second/controller/SecondEntrustUserController.java
  11. 1 1
      alien-second/src/main/java/shop/alien/second/service/SecondEntrustUserService.java
  12. 2 2
      alien-second/src/main/java/shop/alien/second/service/impl/SecondEntrustUserServiceImpl.java
  13. 11 0
      alien-store/src/main/java/shop/alien/store/controller/ReservationJobController.java
  14. 1 1
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingCategoryController.java
  15. 24 0
      alien-store/src/main/java/shop/alien/store/controller/StoreReservationController.java
  16. 8 0
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentOrderService.java
  17. 18 1
      alien-store/src/main/java/shop/alien/store/service/StoreReservationService.java
  18. 9 0
      alien-store/src/main/java/shop/alien/store/service/UserReservationOrderService.java
  19. 43 42
      alien-store/src/main/java/shop/alien/store/service/impl/LicenseAuditAsyncService.java
  20. 2 2
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserLearningVideoServiceImpl.java
  21. 13 0
      alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentOrderServiceImpl.java
  22. 1 1
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingCategoryServiceImpl.java
  23. 230 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java
  24. 3 3
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingTableServiceImpl.java
  25. 103 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreReservationServiceImpl.java
  26. 1 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java
  27. 6 0
      alien-store/src/main/java/shop/alien/store/service/impl/UserReservationOrderServiceImpl.java
  28. 21 5
      alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java
  29. 56 3
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java
  30. 58 4
      alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

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

@@ -62,7 +62,7 @@ public class MerchantPaymentOrder {
     @TableField("pay_amount")
     private BigDecimal payAmount;
 
-    @ApiModelProperty(value = "支付状态: 0-待支付, 1-已支付, 2-已关闭, 3-已退款")
+    @ApiModelProperty(value = "支付状态: 0-待支付, 1-已支付, 2-已关闭, 3-已退款, 4-退款中")
     @TableField("pay_status")
     private Integer payStatus;
 

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

@@ -54,7 +54,7 @@ public class RefundRecord extends Model<RefundRecord> {
     @TableField("pay_type")
     private String payType;
 
-    @ApiModelProperty(value = "退款状态:SUCCESS-退款成功, CLOSED-退款关闭, PROCESSING-退款处理中, ABNORMAL-退款异常")
+    @ApiModelProperty(value = "退款状态:SUCCESS-退款成功, CLOSED-退款关闭, PROCESSING-退款处理中, ABNORMAL-退款异常, FAIL-退款失败")
     @TableField("refund_status")
     private String refundStatus;
 

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

@@ -46,7 +46,7 @@ public class UserReservationOrder {
     @TableField("order_status")
     private Integer orderStatus;
 
-    @ApiModelProperty(value = "支付状态 0:未支付 1:已支付 2:已退款 3:部分退款")
+    @ApiModelProperty(value = "支付状态 0:未支付 1:已支付 2:已退款 3:部分退款 4:退款中")
     @TableField("payment_status")
     private Integer paymentStatus;
 

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

@@ -56,4 +56,11 @@ public interface UserReservationOrderMapper extends BaseMapper<UserReservationOr
      * @return 订单ID列表
      */
     List<Integer> listOrderIdsForArrivalReminder();
+
+    /**
+     * 查询支付状态为「退款中」(4) 的付费订单ID列表,用于定时任务重试退款
+     *
+     * @return 订单ID列表
+     */
+    List<Integer> listOrderIdsForRefundRetry();
 }

+ 4 - 1
alien-entity/src/main/java/shop/alien/mapper/second/SecondEntrustUserMapper.java

@@ -58,9 +58,12 @@ public interface SecondEntrustUserMapper extends BaseMapper<SecondEntrustUser> {
             "where entrust_user_name = #{entrustUserName} " +
             "and entrust_id_card = #{entrustIdCard} " +
             "and delete_flag = 0 " +
+            "and id = #{id} " +
             "order by created_time desc")
     List<SecondEntrustUser> getByUserNameAndIdCard(@Param("entrustUserName") String entrustUserName, 
-                                                     @Param("entrustIdCard") String entrustIdCard);
+                                                     @Param("entrustIdCard") String entrustIdCard,
+                                                    @Param("id") Integer id
+    );
 
 }
 

+ 3 - 5
alien-entity/src/main/resources/mapper/StoreReservationMapper.xml

@@ -83,7 +83,7 @@
                 WHEN 4 THEN '已取消'
                 WHEN 5 THEN '已关闭'
                 WHEN 6 THEN '退款中'
-                WHEN 7 THEN '退款'
+                WHEN 7 THEN '退款成功'
                 WHEN 8 THEN '商家预订'
                 ELSE '未知'
             END AS order_status_text,
@@ -119,10 +119,8 @@
         GROUP BY
             ur.id
         ORDER BY
-            ur.status DESC,
-            ur.reservation_date DESC,
-            ur.start_time ASC,
-            ur.created_time DESC
+            CASE WHEN ur.status = 1 THEN 0 ELSE 1 END ASC,
+            ur.reservation_date ASC
     </select>
 
 </mapper>

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

@@ -82,4 +82,16 @@
           AND r.start_time IS NOT NULL AND TRIM(r.start_time) != ''
           AND STR_TO_DATE(TRIM(r.start_time), '%Y-%m-%d %H:%i') BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 30 MINUTE)
     </select>
+
+    <!-- 退款重试:支付状态为退款中(4)的付费订单 -->
+    <select id="listOrderIdsForRefundRetry" resultType="java.lang.Integer">
+        SELECT id
+        FROM user_reservation_order
+        WHERE delete_flag = 0
+          AND payment_status = 4
+          AND order_cost_type = 1
+          AND out_trade_no IS NOT NULL AND TRIM(out_trade_no) != ''
+          AND deposit_amount IS NOT NULL AND deposit_amount > 0
+        ORDER BY updated_time ASC
+    </select>
 </mapper>

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

@@ -85,4 +85,12 @@ public interface AlienStoreFeign {
     @org.springframework.web.bind.annotation.PostMapping("/reservation/job/sendArrivalReminder")
     R<Integer> sendArrivalReminder();
 
+    /**
+     * 重试退款定时任务:查询支付状态为退款中的订单并重新发起退款
+     *
+     * @return R.data 为本次退款成功的订单数
+     */
+    @org.springframework.web.bind.annotation.PostMapping("/reservation/job/retryRefundFailed")
+    R<Integer> retryRefundFailed();
+
 }

+ 32 - 0
alien-job/src/main/java/shop/alien/job/store/RefundRetryJob.java

@@ -0,0 +1,32 @@
+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;
+
+/**
+ * 退款重试定时任务:查询支付状态为「退款中」的预订订单并重新发起退款(不发送短信和通知)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RefundRetryJob {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @XxlJob("refundRetryJob")
+    public void refundRetryJob() {
+        log.info("【定时任务】退款重试:开始执行");
+        try {
+            R<Integer> result = alienStoreFeign.retryRefundFailed();
+            int count = (result != null && result.getData() != null) ? result.getData() : 0;
+            log.info("【定时任务】退款重试:执行完成,本次退款成功条数={}", count);
+        } catch (Exception e) {
+            log.error("【定时任务】退款重试:执行异常", e);
+            throw e;
+        }
+    }
+}

+ 5 - 4
alien-second/src/main/java/shop/alien/second/controller/SecondEntrustUserController.java

@@ -188,13 +188,14 @@ public class SecondEntrustUserController {
     @ApiOperationSupport(order = 8)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "entrustUserName", value = "委托人姓名", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "entrustIdCard", value = "委托人身份证号", dataType = "String", paramType = "query", required = true)
+            @ApiImplicitParam(name = "entrustIdCard", value = "委托人身份证号", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "id", value = "id", dataType = "Integer", paramType = "query", required = true)
     })
     @GetMapping("/detail")
-    public R<SecondEntrustUserDetailVo> getEntrustUserDetail(@RequestParam String entrustUserName, @RequestParam String entrustIdCard) {
-        log.info("SecondEntrustUserController.getEntrustUserDetail entrustUserName={}, entrustIdCard={}", entrustUserName, entrustIdCard);
+    public R<SecondEntrustUserDetailVo> getEntrustUserDetail(@RequestParam String entrustUserName, @RequestParam String entrustIdCard, @RequestParam Integer id) {
+        log.info("SecondEntrustUserController.getEntrustUserDetail entrustUserName={}, entrustIdCard={},id={}", entrustUserName, entrustIdCard,id);
         try {
-            SecondEntrustUserDetailVo detailVo = secondEntrustUserService.getEntrustUserDetail(entrustUserName, entrustIdCard);
+            SecondEntrustUserDetailVo detailVo = secondEntrustUserService.getEntrustUserDetail(entrustUserName, entrustIdCard,id);
             return R.data(detailVo);
         } catch (Exception e) {
             log.error("SecondEntrustUserController.getEntrustUserDetail error: {}", e.getMessage(), e);

+ 1 - 1
alien-second/src/main/java/shop/alien/second/service/SecondEntrustUserService.java

@@ -84,7 +84,7 @@ public interface SecondEntrustUserService extends IService<SecondEntrustUser> {
      * @param entrustIdCard 委托人身份证号
      * @return 委托人详情
      */
-    SecondEntrustUserDetailVo getEntrustUserDetail(String entrustUserName, String entrustIdCard) throws Exception;
+    SecondEntrustUserDetailVo getEntrustUserDetail(String entrustUserName, String entrustIdCard,Integer id) throws Exception;
 
 }
 

+ 2 - 2
alien-second/src/main/java/shop/alien/second/service/impl/SecondEntrustUserServiceImpl.java

@@ -227,11 +227,11 @@ public class SecondEntrustUserServiceImpl extends ServiceImpl<SecondEntrustUserM
      * @return 委托人详情
      */
     @Override
-    public SecondEntrustUserDetailVo getEntrustUserDetail(String entrustUserName, String entrustIdCard) throws Exception {
+    public SecondEntrustUserDetailVo getEntrustUserDetail(String entrustUserName, String entrustIdCard,Integer id) throws Exception {
         log.info("SecondEntrustUserServiceImpl.getEntrustUserDetail entrustUserName={}, entrustIdCard={}", entrustUserName, entrustIdCard);
         try {
             // 1. 根据姓名和身份证号查询该人的所有委托记录
-            List<SecondEntrustUser> entrustUsers = secondEntrustUserMapper.getByUserNameAndIdCard(entrustUserName, entrustIdCard);
+            List<SecondEntrustUser> entrustUsers = secondEntrustUserMapper.getByUserNameAndIdCard(entrustUserName, entrustIdCard, id);
             
             if (entrustUsers == null || entrustUsers.isEmpty()) {
                 log.warn("委托人信息不存在, entrustUserName={}, entrustIdCard={}", entrustUserName, entrustIdCard);

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

@@ -8,6 +8,7 @@ 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.StoreReservationService;
 import shop.alien.store.service.UserReservationService;
 
 /**
@@ -21,6 +22,7 @@ import shop.alien.store.service.UserReservationService;
 public class ReservationJobController {
 
     private final UserReservationService userReservationService;
+    private final StoreReservationService storeReservationService;
 
     @ApiOperation("标记「结束时间已过且订单待使用」的预订为未到店超时/已过期")
     @PostMapping("/markTimeout")
@@ -39,4 +41,13 @@ public class ReservationJobController {
         log.info("reservation job: sendArrivalReminder 结束,发送条数={}", count);
         return R.data(count);
     }
+
+    @ApiOperation("重试退款:查询支付状态为退款中的订单并重新发起退款(不发送短信和通知)")
+    @PostMapping("/retryRefundFailed")
+    public R<Integer> retryRefundFailed() {
+        log.info("reservation job: retryRefundFailed 开始");
+        int count = storeReservationService.retryRefundFailedOrders();
+        log.info("reservation job: retryRefundFailed 结束,成功退款条数={}", count);
+        return R.data(count);
+    }
 }

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

@@ -102,7 +102,7 @@ public class StoreBookingCategoryController {
         } catch (Exception e) {
             log.error("新增预订服务分类失败", e);
             // 如果是名称已存在的错误,直接返回友好提示
-            return R.fail("新增失败:" + e.getMessage());
+            return R.fail(e.getMessage());
         }
         return R.success("新增成功");
     }

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

@@ -272,4 +272,28 @@ public class StoreReservationController {
             return R.fail("退款失败:" + e.getMessage());
         }
     }
+
+    @ApiOperationSupport(order = 9)
+    @ApiOperation("通过订单ID退款(根据订单自动带出门店、商户订单号、金额、支付方式,成功时发送通知和短信)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "orderId", value = "预订订单ID(user_reservation_order.id)", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "refundReason", value = "退款原因", required = false, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundType", value = "退款类型 0:用户取消 1:商家退款 2:部分退款等 3.扫码核销成功", required = false, paramType = "query", dataType = "int")
+    })
+    @PostMapping("/refundByOrderId")
+    public R<String> refundByOrderId(
+            @RequestParam Integer orderId,
+            @RequestParam(required = false) String refundReason,
+            @RequestParam(required = false) Integer refundType) {
+        log.info("StoreReservationController.refundByOrderId?orderId={}, refundReason={}, refundType={}", orderId, refundReason, refundType);
+        if (orderId == null) {
+            return R.fail("订单ID不能为空");
+        }
+        try {
+            return storeReservationService.refundByOrderId(orderId, refundReason, refundType);
+        } catch (Exception e) {
+            log.error("通过订单ID退款失败", e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
 }

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

@@ -29,6 +29,14 @@ public interface MerchantPaymentOrderService extends IService<MerchantPaymentOrd
     MerchantPaymentOrder getUnpaidByOrderId(Integer orderId);
 
     /**
+     * 根据业务订单ID查询已支付支付单(pay_status=1),用于按订单退款时获取 payType 等
+     *
+     * @param orderId user_reservation_order.id
+     * @return 已支付单,不存在返回 null
+     */
+    MerchantPaymentOrder getPaidByOrderId(Integer orderId);
+
+    /**
      * 查询近期创建的待支付单(用于无异步回调时的后端轮询同步)
      *
      * @param withinMinutes 在最近多少分钟内创建的

+ 18 - 1
alien-store/src/main/java/shop/alien/store/service/StoreReservationService.java

@@ -84,6 +84,23 @@ public interface StoreReservationService {
      * @param payType 支付类型 alipay/wechatPay
      * @return 退款结果
      */
-    shop.alien.entity.result.R<String> refundByOrder(Integer storeId, String outTradeNo, String refundAmount, 
+    shop.alien.entity.result.R<String> refundByOrder(Integer storeId, String outTradeNo, String refundAmount,
                                                        String refundReason, Integer refundType, String payType);
+
+    /**
+     * 通过预订订单ID退款(根据订单查出门店、商户订单号、金额、支付方式后调用支付退款,成功时发送通知和短信)
+     *
+     * @param orderId     预订订单ID(user_reservation_order.id)
+     * @param refundReason 退款原因,可选
+     * @param refundType   退款类型,可选,默认 1-商家退款
+     * @return 退款结果
+     */
+    shop.alien.entity.result.R<String> refundByOrderId(Integer orderId, String refundReason, Integer refundType);
+
+    /**
+     * 定时任务:查询支付状态为「退款中」的订单并重试退款(不发送短信和通知)
+     *
+     * @return 本次退款成功的订单数
+     */
+    int retryRefundFailedOrders();
 }

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

@@ -3,6 +3,8 @@ package shop.alien.store.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.UserReservationOrder;
 
+import java.util.List;
+
 /**
  * 用户预订订单表 服务类
  *
@@ -40,4 +42,11 @@ public interface UserReservationOrderService extends IService<UserReservationOrd
      * @return 删除行数
      */
     int physicalDeleteByReservationId(Integer reservationId);
+
+    /**
+     * 查询支付状态为「退款中」的付费订单ID列表,用于定时任务重试退款
+     *
+     * @return 订单ID列表
+     */
+    List<Integer> listOrderIdsForRefundRetry();
 }

+ 43 - 42
alien-store/src/main/java/shop/alien/store/service/impl/LicenseAuditAsyncService.java

@@ -126,47 +126,6 @@ public class LicenseAuditAsyncService {
             log.info("{}证照审核结果,门店ID:{},图片URL:{},is_valid={},expiry_date={},is_expired={},remaining_days={},license_type={}",
                     licenseTypeName, storeId, imageUrl, isValid, expiryDateStr, isExpired, remainingDays, licenseType);
 
-            // 如果是营业执照,解析并存入到期时间
-            if (licenseStatus == 1 && StringUtils.isNotEmpty(expiryDateStr)) {
-                try {
-                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-                    Date expiryDate = sdf.parse(expiryDateStr);
-
-                    StoreInfo updateStoreInfo = new StoreInfo();
-                    updateStoreInfo.setId(storeId);
-                    updateStoreInfo.setBusinessLicenseExpirationTime(expiryDate);
-
-                    // 同步更新门店到期时间(expirationTime)
-                    // 门店到期时间 = min(合同到期时间, 营业执照到期时间)
-                    // 由于合同到期时间未单独存储,需要根据旧值推断
-                    StoreInfo currentStore = storeInfoMapper.selectById(storeId);
-                    if (currentStore != null) {
-                        Date currentExpiration = currentStore.getExpirationTime();
-                        Date oldBizExpiration = currentStore.getBusinessLicenseExpirationTime();
-                        if (currentExpiration != null && oldBizExpiration != null
-                                && currentExpiration.before(oldBizExpiration)) {
-                            // 合同到期时间 < 旧营业执照到期时间,说明合同是瓶颈
-                            // 新的门店到期时间 = min(合同到期时间, 新营业执照到期时间)
-                            updateStoreInfo.setExpirationTime(
-                                    expiryDate.before(currentExpiration) ? expiryDate : currentExpiration
-                            );
-                        } else {
-                            // 营业执照是瓶颈(或两者相等、或旧值为空)
-                            // 直接用新的营业执照到期时间更新
-                            updateStoreInfo.setExpirationTime(expiryDate);
-                        }
-                    } else {
-                        updateStoreInfo.setExpirationTime(expiryDate);
-                    }
-
-                    storeInfoMapper.updateById(updateStoreInfo);
-                    log.info("营业执照到期时间已更新,门店ID:{},营业执照到期:{},门店到期:{}",
-                            storeId, expiryDateStr, updateStoreInfo.getExpirationTime());
-                } catch (Exception e) {
-                    log.error("解析营业执照到期时间失败,门店ID:{},expiryDate:{}", storeId, expiryDateStr, e);
-                }
-            }
-
             // 判断审核结果
             boolean needReject = false;
             boolean needApprove = false;
@@ -322,8 +281,50 @@ public class LicenseAuditAsyncService {
                     storeInfoMapper.update(null, new LambdaUpdateWrapper<StoreInfo>()
                             .eq(StoreInfo::getId, storeId)
                             .set(StoreInfo::getBusinessLicenseStatus, 1)
+                            .set(StoreInfo::getUpdateBusinessLicenseTime, new Date())
                     );
-                    
+
+                    // 审核通过后,解析并存入营业执照到期时间
+                    if (StringUtils.isNotEmpty(expiryDateStr)) {
+                        try {
+                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                            Date expiryDate = sdf.parse(expiryDateStr);
+
+                            StoreInfo updateStoreInfo = new StoreInfo();
+                            updateStoreInfo.setId(storeId);
+                            updateStoreInfo.setBusinessLicenseExpirationTime(expiryDate);
+
+                            // 同步更新门店到期时间(expirationTime)
+                            // 门店到期时间 = min(合同到期时间, 营业执照到期时间)
+                            // 由于合同到期时间未单独存储,需要根据旧值推断
+                            StoreInfo currentStore = storeInfoMapper.selectById(storeId);
+                            if (currentStore != null) {
+                                Date currentExpiration = currentStore.getExpirationTime();
+                                Date oldBizExpiration = currentStore.getBusinessLicenseExpirationTime();
+                                if (currentExpiration != null && oldBizExpiration != null
+                                        && currentExpiration.before(oldBizExpiration)) {
+                                    // 合同到期时间 < 旧营业执照到期时间,说明合同是瓶颈
+                                    // 新的门店到期时间 = min(合同到期时间, 新营业执照到期时间)
+                                    updateStoreInfo.setExpirationTime(
+                                            expiryDate.before(currentExpiration) ? expiryDate : currentExpiration
+                                    );
+                                } else {
+                                    // 营业执照是瓶颈(或两者相等、或旧值为空)
+                                    // 直接用新的营业执照到期时间更新
+                                    updateStoreInfo.setExpirationTime(expiryDate);
+                                }
+                            } else {
+                                updateStoreInfo.setExpirationTime(expiryDate);
+                            }
+
+                            storeInfoMapper.updateById(updateStoreInfo);
+                            log.info("营业执照到期时间已更新,门店ID:{},营业执照到期:{},门店到期:{}",
+                                    storeId, expiryDateStr, updateStoreInfo.getExpirationTime());
+                        } catch (Exception e) {
+                            log.error("解析营业执照到期时间失败,门店ID:{},expiryDate:{}", storeId, expiryDateStr, e);
+                        }
+                    }
+
                     // 审核通过后,先逻辑删除旧的营业执照记录
                     LambdaUpdateWrapper<StoreImg> deleteOldImgWrapper = new LambdaUpdateWrapper<>();
                     deleteOldImgWrapper.eq(StoreImg::getStoreId, storeId)

+ 2 - 2
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserLearningVideoServiceImpl.java

@@ -24,8 +24,8 @@ public class LifeUserLearningVideoServiceImpl extends ServiceImpl<LifeUserLearni
     @Override
     public R<String> add(LifeUserLearningVideo lifeUserLearningVideo) throws IOException, InterruptedException {
         log.info("LifeUserLearningVideoServiceImpl.add, param={}", lifeUserLearningVideo);
-        long videoDuration = VideoDurationFFmpeg.getVideoDuration(lifeUserLearningVideo.getVideoUrl());
-        lifeUserLearningVideo.setVideoDuration(String.valueOf(videoDuration));
+//        long videoDuration = VideoDurationFFmpeg.getVideoDuration(lifeUserLearningVideo.getVideoUrl());
+        lifeUserLearningVideo.setVideoDuration(String.valueOf(lifeUserLearningVideo.getVideoDuration()));
         boolean result = this.save(lifeUserLearningVideo);
         if (result) {
             return R.success("新增成功");

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

@@ -70,6 +70,19 @@ public class MerchantPaymentOrderServiceImpl extends ServiceImpl<MerchantPayment
     }
 
     @Override
+    public MerchantPaymentOrder getPaidByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<MerchantPaymentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOrderId, orderId);
+        wrapper.eq(MerchantPaymentOrder::getPayStatus, 1);
+        wrapper.eq(MerchantPaymentOrder::getOrderType, "reservation_order");
+        wrapper.last("LIMIT 1");
+        return this.getOne(wrapper);
+    }
+
+    @Override
     public MerchantPaymentOrder getUnpaidByOrderId(Integer orderId) {
         if (orderId == null) {
             return null;

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

@@ -170,7 +170,7 @@ public class StoreBookingCategoryServiceImpl extends ServiceImpl<StoreBookingCat
                 if (duplicateCategory != null) {
                     log.warn("更新预订服务分类失败:当前门店下分类名称已存在,storeId={}, categoryName={}, id={}", 
                             existingCategory.getStoreId(), categoryName, category.getId());
-                    throw new RuntimeException("此名称已存在不能编辑");
+                    throw new RuntimeException("此名称已存在");
                 }
             }
         }

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

@@ -13,10 +13,12 @@ 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.StoreBusinessInfo;
 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.mapper.StoreBusinessInfoMapper;
 import shop.alien.store.service.StoreBookingBusinessHoursService;
 import shop.alien.store.service.StoreBookingSettingsService;
 import shop.alien.util.common.JwtUtil;
@@ -40,6 +42,7 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
 
     private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
     private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+    private final StoreBusinessInfoMapper storeBusinessInfoMapper;
 
     // 时间格式正则:HH:mm
     private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3]):[0-5][0-9]$");
@@ -225,12 +228,18 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
         
         // 3.1 处理正常营业时间(business_type=1)
         if (dto.getNormalBusinessHours() != null) {
+            // 校验正常营业时间:预约时间的正常时间不能大于门店营业时间的正常时间
+            validateNormalBusinessHours(dto.getStoreId(), dto.getNormalBusinessHours());
+            
             StoreBookingBusinessHours normalHours = convertToBusinessHours(dto.getNormalBusinessHours(), settingsId, 1);
             businessHoursList.add(normalHours);
         }
         
         // 3.2 处理特殊营业时间列表(business_type=2)
         if (dto.getSpecialBusinessHoursList() != null && !dto.getSpecialBusinessHoursList().isEmpty()) {
+            // 校验特殊营业时间:预约时间的特殊时间不能大于门店营业时间的特殊时间
+            validateSpecialBusinessHours(dto.getStoreId(), dto.getSpecialBusinessHoursList());
+            
             int sort = 1;
             for (StoreBookingBusinessHoursDTO specialDto : dto.getSpecialBusinessHoursList()) {
                 StoreBookingBusinessHours specialHours = convertToBusinessHours(specialDto, settingsId, 2);
@@ -361,6 +370,227 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
     }
 
     /**
+     * 校验正常营业时间:预约时间的正常时间不能大于门店营业时间的正常时间
+     *
+     * @param storeId 门店ID
+     * @param normalBusinessHours 预约时间的正常营业时间
+     */
+    private void validateNormalBusinessHours(Integer storeId, StoreBookingBusinessHoursDTO normalBusinessHours) {
+        log.info("开始校验正常营业时间,storeId={}", storeId);
+        
+        if (storeId == null || normalBusinessHours == null) {
+            return;
+        }
+        
+        // 如果是全天营业,跳过校验
+        if (normalBusinessHours.getBookingTimeType() != null && normalBusinessHours.getBookingTimeType() == 1) {
+            return;
+        }
+        
+        // 如果没有开始时间和结束时间,跳过校验
+        if (!StringUtils.hasText(normalBusinessHours.getStartTime()) || 
+            !StringUtils.hasText(normalBusinessHours.getEndTime())) {
+            return;
+        }
+        
+        // 查询门店的正常营业时间(store_business_info,business_type=1)
+        List<StoreBusinessInfo> storeNormalBusinessHours = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, storeId)
+                        .eq(StoreBusinessInfo::getBusinessType, 1) // 正常营业时间
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+        );
+        
+        if (storeNormalBusinessHours == null || storeNormalBusinessHours.isEmpty()) {
+            log.warn("门店未配置正常营业时间,无法校验,storeId={}", storeId);
+            return;
+        }
+        
+        // 查找匹配的门店正常营业时间(正常营业时间通常只有一条,或者按 business_date 匹配)
+        StoreBusinessInfo matchedStoreHours = null;
+        
+        // 如果有 holiday_date,尝试通过 business_date 匹配
+        if (StringUtils.hasText(normalBusinessHours.getHolidayDate())) {
+            matchedStoreHours = storeNormalBusinessHours.stream()
+                    .filter(h -> normalBusinessHours.getHolidayDate().equals(h.getBusinessDate()))
+                    .findFirst()
+                    .orElse(null);
+        }
+        
+        // 如果没有匹配到,使用第一条正常营业时间(通常正常营业时间只有一条)
+        if (matchedStoreHours == null && !storeNormalBusinessHours.isEmpty()) {
+            matchedStoreHours = storeNormalBusinessHours.get(0);
+        }
+        
+        // 如果没有匹配到门店正常营业时间,跳过校验
+        if (matchedStoreHours == null) {
+            log.warn("未找到匹配的门店正常营业时间,跳过校验");
+            return;
+        }
+        
+        // 如果门店正常营业时间没有开始时间和结束时间,跳过校验
+        if (!StringUtils.hasText(matchedStoreHours.getStartTime()) || 
+            !StringUtils.hasText(matchedStoreHours.getEndTime())) {
+            return;
+        }
+        
+        // 比较时间范围
+        String bookingStartTime = normalBusinessHours.getStartTime().trim();
+        String bookingEndTime = normalBusinessHours.getEndTime().trim();
+        String storeStartTime = matchedStoreHours.getStartTime().trim();
+        String storeEndTime = matchedStoreHours.getEndTime().trim();
+        
+        // 比较开始时间:预约时间的开始时间不能早于门店营业时间的开始时间
+        if (compareTime(bookingStartTime, storeStartTime) < 0) {
+            log.error("预约时间的正常时间开始时间早于门店营业时间的正常时间,storeId={}, bookingStartTime={}, storeStartTime={}", 
+                    storeId, bookingStartTime, storeStartTime);
+            throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+        }
+        
+        // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
+        if (compareTime(bookingEndTime, storeEndTime) > 0) {
+            log.error("预约时间的正常时间结束时间晚于门店营业时间的正常时间,storeId={}, bookingEndTime={}, storeEndTime={}", 
+                    storeId, bookingEndTime, storeEndTime);
+            throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+        }
+        
+        log.info("正常营业时间校验通过,storeId={}", storeId);
+    }
+    
+    /**
+     * 校验特殊营业时间:预约时间的特殊时间不能大于门店营业时间的特殊时间
+     *
+     * @param storeId 门店ID
+     * @param specialBusinessHoursList 预约时间的特殊营业时间列表
+     */
+    private void validateSpecialBusinessHours(Integer storeId, List<StoreBookingBusinessHoursDTO> specialBusinessHoursList) {
+        log.info("开始校验特殊营业时间,storeId={}, specialBusinessHoursList.size={}", 
+                storeId, specialBusinessHoursList != null ? specialBusinessHoursList.size() : 0);
+        
+        if (storeId == null || specialBusinessHoursList == null || specialBusinessHoursList.isEmpty()) {
+            return;
+        }
+        
+        // 查询门店的特殊营业时间(store_business_info,business_type=2)
+        List<StoreBusinessInfo> storeSpecialBusinessHours = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, storeId)
+                        .eq(StoreBusinessInfo::getBusinessType, 2) // 特殊营业时间
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+        );
+        
+        if (storeSpecialBusinessHours == null || storeSpecialBusinessHours.isEmpty()) {
+            log.warn("门店未配置特殊营业时间,无法校验,storeId={}", storeId);
+            return;
+        }
+        
+        // 遍历预约时间的特殊营业时间,与门店特殊营业时间进行匹配和比较
+        for (StoreBookingBusinessHoursDTO bookingSpecialHours : specialBusinessHoursList) {
+            // 如果是全天营业,跳过校验
+            if (bookingSpecialHours.getBookingTimeType() != null && bookingSpecialHours.getBookingTimeType() == 1) {
+                continue;
+            }
+            
+            // 如果没有开始时间和结束时间,跳过校验
+            if (!StringUtils.hasText(bookingSpecialHours.getStartTime()) || 
+                !StringUtils.hasText(bookingSpecialHours.getEndTime())) {
+                continue;
+            }
+            
+            // 查找匹配的门店特殊营业时间
+            StoreBusinessInfo matchedStoreHours = null;
+            
+            // 优先通过 essential_id 匹配
+            if (bookingSpecialHours.getEssentialId() != null) {
+                matchedStoreHours = storeSpecialBusinessHours.stream()
+                        .filter(h -> {
+                            if (h.getEssentialId() == null || h.getEssentialId().trim().isEmpty()) {
+                                return false;
+                            }
+                            try {
+                                Integer essentialId = Integer.parseInt(h.getEssentialId().trim());
+                                return essentialId.equals(bookingSpecialHours.getEssentialId());
+                            } catch (NumberFormatException e) {
+                                return false;
+                            }
+                        })
+                        .findFirst()
+                        .orElse(null);
+            }
+            
+            // 如果没有通过 essential_id 匹配到,尝试通过 holiday_date 匹配
+            if (matchedStoreHours == null && StringUtils.hasText(bookingSpecialHours.getHolidayDate())) {
+                matchedStoreHours = storeSpecialBusinessHours.stream()
+                        .filter(h -> bookingSpecialHours.getHolidayDate().equals(h.getBusinessDate()))
+                        .findFirst()
+                        .orElse(null);
+            }
+            
+            // 如果没有匹配到门店特殊营业时间,跳过校验(可能是新增的特殊时间)
+            if (matchedStoreHours == null) {
+                log.warn("未找到匹配的门店特殊营业时间,跳过校验,essentialId={}, holidayDate={}", 
+                        bookingSpecialHours.getEssentialId(), bookingSpecialHours.getHolidayDate());
+                continue;
+            }
+            
+            // 如果门店特殊营业时间没有开始时间和结束时间,跳过校验
+            if (!StringUtils.hasText(matchedStoreHours.getStartTime()) || 
+                !StringUtils.hasText(matchedStoreHours.getEndTime())) {
+                continue;
+            }
+            
+            // 比较时间范围
+            String bookingStartTime = bookingSpecialHours.getStartTime().trim();
+            String bookingEndTime = bookingSpecialHours.getEndTime().trim();
+            String storeStartTime = matchedStoreHours.getStartTime().trim();
+            String storeEndTime = matchedStoreHours.getEndTime().trim();
+            
+            // 比较开始时间:预约时间的开始时间不能早于门店营业时间的开始时间
+            if (compareTime(bookingStartTime, storeStartTime) < 0) {
+                log.error("预约时间的特殊时间开始时间早于门店营业时间的特殊时间,storeId={}, bookingStartTime={}, storeStartTime={}", 
+                        storeId, bookingStartTime, storeStartTime);
+                throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+            }
+            
+            // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
+            if (compareTime(bookingEndTime, storeEndTime) > 0) {
+                log.error("预约时间的特殊时间结束时间晚于门店营业时间的特殊时间,storeId={}, bookingEndTime={}, storeEndTime={}", 
+                        storeId, bookingEndTime, storeEndTime);
+                throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+            }
+        }
+        
+        log.info("特殊营业时间校验通过,storeId={}", storeId);
+    }
+    
+    /**
+     * 比较两个时间(HH:mm格式)
+     * 
+     * @param time1 时间1
+     * @param time2 时间2
+     * @return 负数表示time1 < time2,0表示相等,正数表示time1 > time2
+     */
+    private int compareTime(String time1, String time2) {
+        try {
+            String[] parts1 = time1.split(":");
+            String[] parts2 = time2.split(":");
+            
+            int hour1 = Integer.parseInt(parts1[0]);
+            int minute1 = Integer.parseInt(parts1[1]);
+            int hour2 = Integer.parseInt(parts2[0]);
+            int minute2 = Integer.parseInt(parts2[1]);
+            
+            int totalMinutes1 = hour1 * 60 + minute1;
+            int totalMinutes2 = hour2 * 60 + minute2;
+            
+            return totalMinutes1 - totalMinutes2;
+        } catch (Exception e) {
+            log.error("比较时间失败,time1={}, time2={}", time1, time2, e);
+            throw new RuntimeException("时间格式错误");
+        }
+    }
+    
+    /**
      * 从JWT获取当前登录用户ID
      *
      * @return 用户ID,如果未登录返回null

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

@@ -226,7 +226,7 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
         if (existingTable != null) {
             log.warn("新增预订服务桌号失败:同一分类下桌号已存在,storeId={}, categoryId={}, tableNumber={}", 
                     table.getStoreId(), table.getCategoryId(), tableNumber);
-            throw new RuntimeException("该分类下桌号已存在不能添加");
+            throw new RuntimeException("此桌号已存在");
         }
         
         table.setCreatedUserId(userId);
@@ -305,9 +305,9 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
             log.warn("批量新增预订服务桌号失败:同一分类下桌号已存在,storeId={}, categoryId={}, existingNumbers={}", 
                     storeId, categoryId, existingNumbers);
             if (existingNumbers.size() == 1) {
-                throw new RuntimeException("该分类下桌号已存在不能添加");
+                throw new RuntimeException("此桌号已存在");
             } else {
-                throw new RuntimeException("该分类下以下桌号已存在不能添加:" + String.join("、", existingNumbers));
+                throw new RuntimeException("此桌号已存在:" + String.join("、", existingNumbers));
             }
         }
         

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

@@ -6,6 +6,7 @@ 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.StoreBookingTable;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.UserReservation;
@@ -23,6 +24,7 @@ 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.MerchantPaymentOrderService;
 import shop.alien.store.service.StoreReservationService;
 import shop.alien.store.service.UserReservationOrderService;
 import shop.alien.store.service.StoreBookingBusinessHoursService;
@@ -34,8 +36,11 @@ 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.entity.store.StoreUser;
 import shop.alien.mapper.LifeNoticeMapper;
 import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.util.common.JwtUtil;
 import org.springframework.util.StringUtils;
 import javax.annotation.PostConstruct;
 import java.text.ParseException;
@@ -67,10 +72,12 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
     private final UserReservationTableMapper userReservationTableMapper;
     private final LifeNoticeMapper lifeNoticeMapper;
     private final LifeUserMapper lifeUserMapper;
+    private final StoreUserMapper storeUserMapper;
     private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
     private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
     private final StoreBookingSettingsService storeBookingSettingsService;
     private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
 
     /** 预约状态:已取消 */
     private static final int STATUS_CANCELLED = 3;
@@ -623,6 +630,19 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
             throw new RuntimeException("预约不存在或已被删除");
         }
 
+        // 校验门店:核销码必须属于当前登录的门店
+        Integer currentStoreId = getCurrentStoreId();
+        if (currentStoreId == null) {
+            log.warn("无法获取当前登录门店ID,跳过门店校验");
+        } else {
+            Integer reservationStoreId = reservation.getStoreId();
+            if (reservationStoreId == null || !currentStoreId.equals(reservationStoreId)) {
+                log.error("核销码不属于本门店,当前门店ID={}, 预约门店ID={}, verificationCode={}", 
+                        currentStoreId, reservationStoreId, verificationCode);
+                throw new RuntimeException("此预订码不属于本门店,不予核销");
+            }
+        }
+
         // 校验预约状态(必须是已确认状态才能核销)
         if (reservation.getStatus() == null || reservation.getStatus() != 1) {
             throw new RuntimeException("预约状态不正确,只有已确认的预约才能核销");
@@ -1325,4 +1345,87 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
             return shop.alien.entity.result.R.fail("退款失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 获取当前登录的门店ID
+     * 从JWT中获取用户ID,然后查询StoreUser获取门店ID
+     *
+     * @return 门店ID,如果未登录或查询失败返回null
+     */
+    private Integer getCurrentStoreId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                Integer userId = userInfo.getInteger("userId");
+                if (userId != null) {
+                    StoreUser storeUser = storeUserMapper.selectById(userId);
+                    if (storeUser != null) {
+                        return storeUser.getStoreId();
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录门店ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+
+    @Override
+    public shop.alien.entity.result.R<String> refundByOrderId(Integer orderId, String refundReason, Integer refundType) {
+        log.info("StoreReservationServiceImpl.refundByOrderId?orderId={}, refundReason={}, refundType={}", orderId, refundReason, refundType);
+        if (orderId == null) {
+            return shop.alien.entity.result.R.fail("订单ID不能为空");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return shop.alien.entity.result.R.fail("预订订单不存在");
+        }
+        if (order.getPaymentStatus() == null || order.getPaymentStatus() != 1) {
+            return shop.alien.entity.result.R.fail("订单未支付或已退款,无法退款");
+        }
+        if (order.getOrderCostType() == null || order.getOrderCostType() != 1) {
+            return shop.alien.entity.result.R.fail("免费订单无需退款");
+        }
+        if (order.getOutTradeNo() == null || order.getOutTradeNo().trim().isEmpty()) {
+            return shop.alien.entity.result.R.fail("订单无商户订单号,无法退款");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getPaidByOrderId(orderId);
+        if (paymentOrder == null || paymentOrder.getPayType() == null || paymentOrder.getPayType().trim().isEmpty()) {
+            return shop.alien.entity.result.R.fail("未找到已支付记录或支付方式,无法退款");
+        }
+        String amount = order.getDepositAmount() != null ? order.getDepositAmount().toString() : null;
+        if (amount == null || amount.trim().isEmpty()) {
+            return shop.alien.entity.result.R.fail("订单订金金额异常,无法退款");
+        }
+        String reason = refundReason != null && !refundReason.trim().isEmpty() ? refundReason.trim() : "按订单退款";
+        int type = refundType != null ? refundType : 1;
+        try {
+            MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(paymentOrder.getPayType());
+            return strategy.refund(order.getStoreId(), order.getOutTradeNo(), amount, reason, type);
+        } catch (Exception e) {
+            log.error("通过订单ID退款异常,orderId={}", orderId, e);
+            return shop.alien.entity.result.R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public int retryRefundFailedOrders() {
+        java.util.List<Integer> orderIds = userReservationOrderService.listOrderIdsForRefundRetry();
+        if (orderIds == null || orderIds.isEmpty()) {
+            return 0;
+        }
+        int successCount = 0;
+        for (Integer orderId : orderIds) {
+            try {
+                shop.alien.entity.result.R<String> result = refundByOrderId(orderId, "定时重试退款", 1);
+                if (result != null && shop.alien.entity.result.R.isSuccess(result)) {
+                    successCount++;
+                    log.info("定时重试退款成功,orderId={}", orderId);
+                }
+            } catch (Exception e) {
+                log.error("定时重试退款异常,orderId={}", orderId, e);
+            }
+        }
+        return successCount;
+    }
 }

+ 1 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java

@@ -346,6 +346,7 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
         LambdaQueryWrapper<StoreStaffConfig> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(StoreStaffConfig::getStoreId, storeId)
                 .eq(StoreStaffConfig::getDeleteFlag, 0)
+                .eq(StoreStaffConfig::getStatus, 1)
                 // 只查询staff_position不为空的记录
                 .isNotNull(StoreStaffConfig::getStaffPosition)
                 .ne(StoreStaffConfig::getStaffPosition, "")

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

@@ -14,6 +14,7 @@ import shop.alien.store.service.UserReservationOrderService;
 
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 
 /**
@@ -65,4 +66,9 @@ public class UserReservationOrderServiceImpl extends ServiceImpl<UserReservation
         }
         return baseMapper.physicalDeleteByReservationId(reservationId);
     }
+
+    @Override
+    public List<Integer> listOrderIdsForRefundRetry() {
+        return baseMapper.listOrderIdsForRefundRetry();
+    }
 }

+ 21 - 5
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -361,6 +361,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
             if (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
                 continue;
             }
+
+            if (reservation.getStatus() != null && reservation.getStatus() == 2) {
+                continue;
+            }
+
             if (calReservation != null && reservation.getReservationDate() != null) {
                 Calendar cal = calendarOf(reservation.getReservationDate());
                 if (cal.get(Calendar.YEAR) != calReservation.get(Calendar.YEAR)
@@ -495,13 +500,18 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     }
 
     /**
-     * 将 "HH:mm" 解析为当日 0 点起的分钟数,解析失败返回 -1。
+     * 将 "HH:mm" 或 "yyyy-MM-dd HH:mm" / "yyyy-MM-dd HH:mm:ss" 解析为当日 0 点起的分钟数,解析失败返回 -1。
+     * 若带年月日(含空格),先去掉日期部分再按时分计算。
      */
     private static int timeToMinutes(String hhmm) {
         if (hhmm == null) {
             return -1;
         }
-        String[] parts = hhmm.trim().split(":");
+        hhmm = hhmm.trim();
+        if (hhmm.contains(" ")) {
+            hhmm = hhmm.substring(hhmm.indexOf(" ") + 1).trim();
+        }
+        String[] parts = hhmm.split(":");
         if (parts.length < 2) {
             return -1;
         }
@@ -526,20 +536,26 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
      */
     private int[] getBookingRangeMinutes(Integer storeId) {
         int[] range = new int[]{0, MINUTES_DAY_END};
+        // 先从 store_booking_settings 按 storeId 查设置,再用 settingsId 关联
         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) {
+            List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
+            if (!businessHoursList.isEmpty()) {
+                StoreBookingBusinessHours businessHours = businessHoursList.get(0);
+
+            if (businessHours.getBookingTimeType() != null && businessHours.getBookingTimeType() == 1) {
                 return range;
             }
-            int start = timeToMinutes(settings.getBookingStartTime());
-            int end = timeToMinutes(settings.getBookingEndTime());
+            int start = timeToMinutes(businessHours.getStartTime());
+            int end = timeToMinutes(businessHours.getEndTime());
             if (start >= 0 && end > start) {
                 range[0] = start;
                 range[1] = end;
                 return range;
             }
+            }
         }
         // 预订开始/结束时间为空或无效时,取商户运营时间(营业时间)
         StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);

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

@@ -62,11 +62,16 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
     private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:alipay:prepay:debounce:order:";
     /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
     private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+    /** 支付状态:退款中 */
+    private static final int PAY_STATUS_REFUNDING = 4;
+    /** 退款记录状态:退款失败 */
+    private static final String REFUND_STATUS_FAIL = "FAIL";
 
     private final StorePaymentConfigService storePaymentConfigService;
     private final UserReservationOrderService userReservationOrderService;
     private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordAsyncService refundRecordAsyncService;
+    private final RefundRecordService refundRecordService;
     private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
     private final UserReservationService userReservationService;
     private final StringRedisTemplate stringRedisTemplate;
@@ -324,7 +329,9 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             request.setBizModel(model);
             AlipayTradeRefundResponse response = client.certificateExecute(request);
             if (!response.isSuccess()) {
-                return R.fail("退款失败:" + response.getSubMsg());
+                String failMsg = "退款失败:" + response.getSubMsg();
+                markRefundFailed(order, paymentOrder, failMsg, refundReason, storeId, outTradeNo);
+                return R.fail(failMsg);
             }
             JSONObject responseBody = JSONObject.parseObject(response.getBody());
             JSONObject refundResponse = responseBody != null ? responseBody.getJSONObject("alipay_trade_refund_response") : null;
@@ -344,6 +351,13 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             order.setRefundType(refundType);
             userReservationOrderService.updateById(order);
 
+            //修改用户预约信息表状态
+            UserReservation reservation = userReservationService.getById(order.getReservationId());
+            if (reservation != null) {
+                reservation.setStatus(3);
+                userReservationService.updateById(reservation);
+            }
+
             RefundRecord record = new RefundRecord();
             record.setPayType(PaymentEnum.ALIPAY.getType());
             record.setOutTradeNo(outTradeNo);
@@ -365,10 +379,49 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             return R.data("退款成功");
         } catch (AlipayApiException e) {
             log.error("商户预订订单退款异常,outTradeNo={}", outTradeNo, e);
-            return R.fail("退款失败:" + e.getErrMsg());
+            String errMsg = "退款失败:" + e.getErrMsg();
+            MerchantPaymentOrder payOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+            UserReservationOrder resOrder = payOrder != null ? userReservationOrderService.getById(payOrder.getOrderId()) : null;
+            if (resOrder != null && payOrder != null) {
+                markRefundFailed(resOrder, payOrder, errMsg, refundReason, storeId, outTradeNo);
+            }
+            return R.fail(errMsg);
         } catch (Exception e) {
             log.error("构建支付宝配置异常,storeId={}", storeId, e);
-            return R.fail("退款失败:" + e.getMessage());
+            String errMsg = "退款失败:" + e.getMessage();
+            MerchantPaymentOrder payOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+            UserReservationOrder resOrder = payOrder != null ? userReservationOrderService.getById(payOrder.getOrderId()) : null;
+            if (resOrder != null && payOrder != null) {
+                markRefundFailed(resOrder, payOrder, errMsg, refundReason, storeId, outTradeNo);
+            }
+            return R.fail(errMsg);
+        }
+    }
+
+    /** 退款失败时:订单与支付单置为退款中,并写入退款失败记录 */
+    private void markRefundFailed(UserReservationOrder order, MerchantPaymentOrder paymentOrder,
+                                  String errorMsg, String refundReason, Integer storeId, String outTradeNo) {
+        try {
+            Date now = new Date();
+            order.setPaymentStatus(PAY_STATUS_REFUNDING);
+            order.setUpdatedTime(now);
+            userReservationOrderService.updateById(order);
+            paymentOrder.setPayStatus(PAY_STATUS_REFUNDING);
+            paymentOrder.setUpdatedTime(now);
+            merchantPaymentOrderService.updateById(paymentOrder);
+            RefundRecord record = new RefundRecord();
+            record.setOutTradeNo(outTradeNo);
+            record.setPayType(PaymentEnum.ALIPAY.getType());
+            record.setRefundStatus(REFUND_STATUS_FAIL);
+            record.setOrderId(String.valueOf(order.getId()));
+            record.setStoreId(storeId);
+            record.setUserId(order.getUserId());
+            record.setRefundReason(StringUtils.isNotBlank(refundReason) ? refundReason : "退款失败");
+            record.setErrorMsg(errorMsg != null ? errorMsg : "退款失败");
+            record.setDeleteFlag(0);
+            refundRecordService.save(record);
+        } catch (Exception ex) {
+            log.error("标记退款失败状态异常,outTradeNo={}", outTradeNo, ex);
         }
     }
 

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

@@ -16,6 +16,7 @@ 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.RefundRecordService;
 import shop.alien.store.service.StorePaymentConfigService;
 import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
 import shop.alien.store.service.UserReservationOrderService;
@@ -53,6 +54,10 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:wechat:prepay:debounce:order:";
     /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
     private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+    /** 支付状态:退款中 */
+    private static final int PAY_STATUS_REFUNDING = 4;
+    /** 退款记录状态:退款失败 */
+    private static final String REFUND_STATUS_FAIL = "FAIL";
 
     @Value("${payment.wechatPay.host:https://api.mch.weixin.qq.com}")
     private String wechatPayApiHost;
@@ -71,6 +76,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     private final UserReservationOrderService userReservationOrderService;
     private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordAsyncService refundRecordAsyncService;
+    private final RefundRecordService refundRecordService;
     private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
     private final UserReservationService userReservationService;
     private final StringRedisTemplate stringRedisTemplate;
@@ -346,6 +352,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
                             Thread.sleep(delayMs);
                         } catch (InterruptedException ie) {
                             Thread.currentThread().interrupt();
+                            markRefundFailed(order, paymentOrder, "操作过于频繁,请稍后再试", refundReason, storeId, outTradeNo);
                             return R.fail("操作过于频繁,请稍后再试");
                         }
                     } else {
@@ -360,11 +367,14 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
                 String msg = lastApiException != null && lastApiException.getStatusCode() == 429
                         ? (StringUtils.isNotBlank(lastApiException.getErrorMessage()) ? lastApiException.getErrorMessage() : "操作过于频繁,请稍后再试")
                         : "退款失败";
+                markRefundFailed(order, paymentOrder, msg, refundReason, storeId, outTradeNo);
                 return R.fail(msg);
             }
             String status = response.status != null ? response.status.name() : "";
             if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status)) {
-                return R.fail("退款失败:" + status);
+                String failMsg = "退款失败:" + status;
+                markRefundFailed(order, paymentOrder, failMsg, refundReason, storeId, outTradeNo);
+                return R.fail(failMsg);
             }
 
             Date now = new Date();
@@ -381,6 +391,13 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             order.setRefundType(refundType);
             userReservationOrderService.updateById(order);
 
+            //修改用户预约信息表状态
+            UserReservation reservation = userReservationService.getById(order.getReservationId());
+            if (reservation != null) {
+                reservation.setStatus(3);
+                userReservationService.updateById(reservation);
+            }
+
             RefundRecord record = new RefundRecord();
             record.setPayType(PaymentEnum.WECHAT_PAY.getType());
             record.setOutTradeNo(outTradeNo);
@@ -403,16 +420,53 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             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() : "操作过于频繁,请稍后再试");
+            String errMsg = e.getStatusCode() == 429
+                    ? (StringUtils.isNotBlank(e.getErrorMessage()) ? e.getErrorMessage() : "操作过于频繁,请稍后再试")
+                    : "退款失败:" + e.getMessage();
+            MerchantPaymentOrder payOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+            UserReservationOrder resOrder = payOrder != null ? userReservationOrderService.getById(payOrder.getOrderId()) : null;
+            if (resOrder != null && payOrder != null) {
+                markRefundFailed(resOrder, payOrder, errMsg, refundReason, storeId, outTradeNo);
             }
-            return R.fail("退款失败:" + e.getMessage());
+            return R.fail(errMsg);
         } catch (Exception e) {
             log.error("商户微信退款异常,storeId={}", storeId, e);
+            MerchantPaymentOrder payOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+            UserReservationOrder resOrder = payOrder != null ? userReservationOrderService.getById(payOrder.getOrderId()) : null;
+            if (resOrder != null && payOrder != null) {
+                markRefundFailed(resOrder, payOrder, "退款失败:" + e.getMessage(), refundReason, storeId, outTradeNo);
+            }
             return R.fail("退款失败:" + e.getMessage());
         }
     }
 
+    /** 退款失败时:订单与支付单置为退款中,并写入退款失败记录 */
+    private void markRefundFailed(UserReservationOrder order, MerchantPaymentOrder paymentOrder,
+                                  String errorMsg, String refundReason, Integer storeId, String outTradeNo) {
+        try {
+            Date now = new Date();
+            order.setPaymentStatus(PAY_STATUS_REFUNDING);
+            order.setUpdatedTime(now);
+            userReservationOrderService.updateById(order);
+            paymentOrder.setPayStatus(PAY_STATUS_REFUNDING);
+            paymentOrder.setUpdatedTime(now);
+            merchantPaymentOrderService.updateById(paymentOrder);
+            RefundRecord record = new RefundRecord();
+            record.setOutTradeNo(outTradeNo);
+            record.setPayType(PaymentEnum.WECHAT_PAY.getType());
+            record.setRefundStatus(REFUND_STATUS_FAIL);
+            record.setOrderId(String.valueOf(order.getId()));
+            record.setStoreId(storeId);
+            record.setUserId(order.getUserId());
+            record.setRefundReason(StringUtils.isNotBlank(refundReason) ? refundReason : "退款失败");
+            record.setErrorMsg(errorMsg != null ? errorMsg : "退款失败");
+            record.setDeleteFlag(0);
+            refundRecordService.save(record);
+        } catch (Exception ex) {
+            log.error("标记退款失败状态异常,outTradeNo={}", outTradeNo, ex);
+        }
+    }
+
     @Override
     public String getType() {
         return PaymentEnum.WECHAT_PAY.getType();