|
|
@@ -20,13 +20,15 @@ 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.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.mapper.LifeNoticeMapper;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
import javax.annotation.PostConstruct;
|
|
|
-
|
|
|
import java.text.ParseException;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.Calendar;
|
|
|
@@ -55,6 +57,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
private final StoreBookingTableService storeBookingTableService;
|
|
|
private final UserReservationTableMapper userReservationTableMapper;
|
|
|
private final LifeNoticeMapper lifeNoticeMapper;
|
|
|
+ private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
|
|
|
|
|
|
/** 预约状态:已取消 */
|
|
|
private static final int STATUS_CANCELLED = 3;
|
|
|
@@ -119,6 +122,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
if (order == null) {
|
|
|
// 如果没有订单,直接更新预约状态为3(已取消)
|
|
|
reservation.setStatus(STATUS_CANCELLED);
|
|
|
+ reservation.setReason(cancelReason); // 保存取消原因
|
|
|
boolean updateResult = this.updateById(reservation);
|
|
|
if (!updateResult) {
|
|
|
throw new RuntimeException("更新预约状态失败");
|
|
|
@@ -145,6 +149,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
reservation.setStatus(STATUS_CANCELLED);
|
|
|
+ reservation.setReason(cancelReason); // 保存取消原因
|
|
|
boolean reservationUpdateResult = this.updateById(reservation);
|
|
|
if (!reservationUpdateResult) {
|
|
|
throw new RuntimeException("更新预约状态失败");
|
|
|
@@ -156,8 +161,42 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
sendCancelReservationSms(reservation, cancelReason);
|
|
|
return true;
|
|
|
} else if (orderCostType == 1) {
|
|
|
- // 付费订单:功能预留(暂不更新状态,等待后续实现退款逻辑)
|
|
|
- throw new RuntimeException("付费订单取消功能暂未实现,请稍后再试");
|
|
|
+ // 付费订单:调用退款接口,然后更新状态
|
|
|
+ try {
|
|
|
+ // 调用退款接口(和核销中的退款逻辑一样)
|
|
|
+ MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
|
|
|
+ strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "商家取消预约退款");
|
|
|
+
|
|
|
+ // 退款成功后,更新订单状态为已退款(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);
|
|
|
}
|
|
|
@@ -264,6 +303,78 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 发送订金退款短信和通知
|
|
|
+ *
|
|
|
+ * @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, userPhone, message);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送订金退款短信和通知异常,reservationId={}", reservation.getId(), e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送订金退款通知
|
|
|
+ *
|
|
|
+ * @param reservation 预约信息
|
|
|
+ * @param phone 用户手机号
|
|
|
+ * @param noticeMessage 通知内容
|
|
|
+ */
|
|
|
+ private void sendDepositRefundNotice(UserReservation reservation, String phone, String noticeMessage) {
|
|
|
+ try {
|
|
|
+ // 构建receiverId:用户端使用 "user_" + 手机号
|
|
|
+ String receiverId = "user_" + phone;
|
|
|
+
|
|
|
+ // 构建通知内容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={}, receiverId={}", reservation.getId(), receiverId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送订金退款通知异常,reservationId={}, phone={}", reservation.getId(), phone, e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 格式化预约时间:日期 + 时间(格式:2026-01-01 14:00)
|
|
|
*/
|
|
|
private String formatReservationDateTime(UserReservation reservation) {
|
|
|
@@ -415,20 +526,26 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public boolean verifyReservationByNo(String reservationNo) {
|
|
|
- log.info("StoreReservationServiceImpl.verifyReservationByNo?reservationNo={}", reservationNo);
|
|
|
+ public boolean verifyReservationByCode(String verificationCode) {
|
|
|
+ log.info("StoreReservationServiceImpl.verifyReservationByCode?verificationCode={}", verificationCode);
|
|
|
|
|
|
- if (reservationNo == null || reservationNo.trim().isEmpty()) {
|
|
|
- throw new RuntimeException("预约号不能为空");
|
|
|
+ if (verificationCode == null || verificationCode.trim().isEmpty()) {
|
|
|
+ throw new RuntimeException("核销码不能为空");
|
|
|
}
|
|
|
|
|
|
- // 根据预约号查询预约信息
|
|
|
- LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
|
|
|
- wrapper.eq(UserReservation::getReservationNo, reservationNo)
|
|
|
- .eq(UserReservation::getDeleteFlag, 0)
|
|
|
+ // 根据核销码查询订单信息
|
|
|
+ LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
+ orderWrapper.eq(UserReservationOrder::getVerificationCode, verificationCode)
|
|
|
+ .eq(UserReservationOrder::getDeleteFlag, 0)
|
|
|
.last("LIMIT 1");
|
|
|
- UserReservation reservation = this.getOne(wrapper);
|
|
|
+ UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
|
|
|
|
|
|
+ if (order == null) {
|
|
|
+ throw new RuntimeException("核销码不存在或订单已被删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据订单关联的预约ID查询预约信息
|
|
|
+ UserReservation reservation = this.getById(order.getReservationId());
|
|
|
if (reservation == null) {
|
|
|
throw new RuntimeException("预约不存在或已被删除");
|
|
|
}
|
|
|
@@ -443,17 +560,6 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
throw new RuntimeException("预约已过期,无法核销");
|
|
|
}
|
|
|
|
|
|
- // 查询关联的订单
|
|
|
- LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
- orderWrapper.eq(UserReservationOrder::getReservationId, reservation.getId())
|
|
|
- .eq(UserReservationOrder::getDeleteFlag, 0)
|
|
|
- .last("LIMIT 1");
|
|
|
- UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
|
|
|
-
|
|
|
- if (order == null) {
|
|
|
- throw new RuntimeException("未找到关联的订单信息");
|
|
|
- }
|
|
|
-
|
|
|
// 更新预约状态为已到店(status = 2)
|
|
|
reservation.setStatus(2); // 已到店
|
|
|
reservation.setActualArrivalTime(new Date());
|
|
|
@@ -469,22 +575,21 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
throw new RuntimeException("更新订单状态失败");
|
|
|
}
|
|
|
|
|
|
- // 计算Redis过期时间(预订结束时间 + 3小时)
|
|
|
- long expireSeconds = calculateExpireSecondsWithBuffer(reservation);
|
|
|
+ //调用退款
|
|
|
+ MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
|
|
|
+ strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款");
|
|
|
|
|
|
- // 将预订信息存储到Redis
|
|
|
+ // 核销成功后,发送订金退款短信和通知
|
|
|
try {
|
|
|
- String redisKey = RESERVATION_VERIFY_PREFIX + reservationNo;
|
|
|
- String reservationJson = objectMapper.writeValueAsString(reservation);
|
|
|
- baseRedisService.setString(redisKey, reservationJson, expireSeconds);
|
|
|
- log.info("预订信息已存储到Redis,key={}, expireSeconds={}(预订结束时间+3小时)", redisKey, expireSeconds);
|
|
|
+ sendDepositRefundSmsAndNotice(reservation, order);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("存储预订信息到Redis失败", e);
|
|
|
- // Redis存储失败不影响核销流程,只记录日志
|
|
|
+ log.error("核销后发送订金退款短信和通知失败,但不影响核销流程,verificationCode={}, orderId={}, error={}",
|
|
|
+ verificationCode, order.getId(), e.getMessage(), e);
|
|
|
+ // 短信和通知发送失败不影响核销流程,只记录日志
|
|
|
}
|
|
|
|
|
|
- log.info("核销预约订单成功,reservationNo={}, reservationId={}, orderId={}",
|
|
|
- reservationNo, reservation.getId(), order.getId());
|
|
|
+ log.info("核销预约订单成功,verificationCode={}, reservationId={}, orderId={}",
|
|
|
+ verificationCode, reservation.getId(), order.getId());
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -515,7 +620,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
// 检查订单状态,只有已完成的订单才能退款
|
|
|
- if (order.getOrderStatus() == null || order.getOrderStatus() != 2) {
|
|
|
+ if (order.getOrderStatus() == null || order.getOrderStatus() != 1) {
|
|
|
log.warn("订单状态不正确,无法退款,订单ID: {}, 订单状态: {}", order.getId(), order.getOrderStatus());
|
|
|
throw new RuntimeException("订单状态不正确,只有已完成的订单才能退款");
|
|
|
}
|
|
|
@@ -617,41 +722,85 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
|
|
|
/**
|
|
|
* 校验预约是否过期
|
|
|
+ * 判断当前时间是否超过预约日期 + 结束时间
|
|
|
*/
|
|
|
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:ss"),
|
|
|
- new SimpleDateFormat("yyyy-MM-dd HH:mm"),
|
|
|
- new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
|
|
|
- new SimpleDateFormat("yyyy/MM/dd HH:mm")
|
|
|
+ new SimpleDateFormat("yyyy-MM-dd HH:mm"), // 完整日期时间格式
|
|
|
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), // 完整日期时间格式(带秒)
|
|
|
+ new SimpleDateFormat("HH:mm") // 只有时间格式
|
|
|
};
|
|
|
-
|
|
|
- Date endDateTime = null;
|
|
|
- for (SimpleDateFormat format : dateTimeFormats) {
|
|
|
+
|
|
|
+ // 先尝试解析为完整日期时间格式
|
|
|
+ boolean parsed = false;
|
|
|
+ for (int i = 0; i < 2; i++) {
|
|
|
try {
|
|
|
- endDateTime = format.parse(endTime);
|
|
|
+ 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("无法解析结束时间格式,endTime={}", endTime);
|
|
|
+ log.error("无法解析结束时间格式,reservationDate={}, endTime={}",
|
|
|
+ reservationDate, endTime);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- return now.after(endDateTime);
|
|
|
+ // 判断当前时间是否超过结束时间
|
|
|
+ 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("校验预约是否过期失败", e);
|
|
|
+ log.error("校验预约是否过期失败,reservationId={}", reservation.getId(), e);
|
|
|
return true;
|
|
|
}
|
|
|
}
|