|
|
@@ -26,7 +26,9 @@ 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;
|
|
|
@@ -57,6 +59,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
private final StoreBookingTableService storeBookingTableService;
|
|
|
private final UserReservationTableMapper userReservationTableMapper;
|
|
|
private final LifeNoticeMapper lifeNoticeMapper;
|
|
|
+ private final LifeUserMapper lifeUserMapper;
|
|
|
private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
|
|
|
|
|
|
/** 预约状态:已取消 */
|
|
|
@@ -261,7 +264,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
// 发送通知(和短信内容一样)
|
|
|
- sendCancelReservationNotice(reservation, phone, noticeMessage);
|
|
|
+ sendCancelReservationNotice(reservation, noticeMessage);
|
|
|
} catch (Exception e) {
|
|
|
// 短信和通知发送失败不影响取消预约流程,只记录日志
|
|
|
log.error("发送商家取消预约短信和通知异常,reservationId={}", reservation.getId(), e);
|
|
|
@@ -271,10 +274,31 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
/**
|
|
|
* 发送商家取消预约通知
|
|
|
*/
|
|
|
- private void sendCancelReservationNotice(UserReservation reservation, String phone, String noticeMessage) {
|
|
|
+ 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_" + phone;
|
|
|
+ String receiverId = "user_" + userPhone.trim();
|
|
|
|
|
|
// 构建通知内容JSON
|
|
|
JSONObject contextJson = new JSONObject();
|
|
|
@@ -295,10 +319,12 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
// 保存通知到数据库
|
|
|
lifeNoticeMapper.insert(lifeNotice);
|
|
|
|
|
|
- log.info("商家取消预约通知发送成功,reservationId={}, receiverId={}", reservation.getId(), receiverId);
|
|
|
+ log.info("商家取消预约通知发送成功,reservationId={}, userId={}, receiverId={}",
|
|
|
+ reservation.getId(), userId, receiverId);
|
|
|
} catch (Exception e) {
|
|
|
// 通知发送失败不影响流程,只记录日志
|
|
|
- log.error("发送商家取消预约通知异常,reservationId={}, phone={}", reservation.getId(), phone, e);
|
|
|
+ log.error("发送商家取消预约通知异常,reservationId={}, userId={}",
|
|
|
+ reservation.getId(), reservation.getUserId(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -329,7 +355,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
// 发送通知
|
|
|
- sendDepositRefundNotice(reservation, userPhone, message);
|
|
|
+ sendDepositRefundNotice(reservation, message);
|
|
|
} catch (Exception e) {
|
|
|
log.error("发送订金退款短信和通知异常,reservationId={}", reservation.getId(), e);
|
|
|
throw e;
|
|
|
@@ -340,13 +366,33 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
* 发送订金退款通知
|
|
|
*
|
|
|
* @param reservation 预约信息
|
|
|
- * @param phone 用户手机号
|
|
|
* @param noticeMessage 通知内容
|
|
|
*/
|
|
|
- private void sendDepositRefundNotice(UserReservation reservation, String phone, String 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_" + phone;
|
|
|
+ String receiverId = "user_" + userPhone.trim();
|
|
|
|
|
|
// 构建通知内容JSON
|
|
|
JSONObject contextJson = new JSONObject();
|
|
|
@@ -367,9 +413,11 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
// 保存通知到数据库
|
|
|
lifeNoticeMapper.insert(lifeNotice);
|
|
|
|
|
|
- log.info("核销后订金退款通知发送成功,reservationId={}, receiverId={}", reservation.getId(), receiverId);
|
|
|
+ log.info("核销后订金退款通知发送成功,reservationId={}, userId={}, receiverId={}",
|
|
|
+ reservation.getId(), userId, receiverId);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("发送订金退款通知异常,reservationId={}, phone={}", reservation.getId(), phone, e);
|
|
|
+ log.error("发送订金退款通知异常,reservationId={}, userId={}",
|
|
|
+ reservation.getId(), reservation.getUserId(), e);
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
@@ -483,9 +531,10 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
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格式");
|
|
|
+ // 验证并标准化时间格式(支持 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");
|
|
|
}
|
|
|
|
|
|
// 查询预约信息
|
|
|
@@ -497,20 +546,31 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
// 保存原结束时间用于日志
|
|
|
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;
|
|
|
- if (compareTime(addTimeStart, reservation.getEndTime()) <= 0) {
|
|
|
- // 加时开始时间在预订结束时间之内,从当前结束时间开始加时
|
|
|
- newEndTime = calculateNewEndTime(reservation.getEndTime(), addTimeMinutes);
|
|
|
+ int timeComparison = compareTime(normalizedAddTimeStart, normalizedEndTime);
|
|
|
+ if (timeComparison <= 0) {
|
|
|
+ // 加时开始时间 <= 预订结束时间:新结束时间 = 预订结束时间 + 加时分钟数
|
|
|
+ newEndTime = calculateNewEndTime(normalizedEndTime, addTimeMinutes);
|
|
|
} else {
|
|
|
- // 加时开始时间超过了预订结束时间,从加时开始时间开始加时
|
|
|
- newEndTime = calculateNewEndTime(addTimeStart, addTimeMinutes);
|
|
|
+ // 加时开始时间 > 预订结束时间:新结束时间 = 加时开始时间 + 加时分钟数
|
|
|
+ // (这已经包含了中间的空挡时间,因为是从加时开始时间开始累加的)
|
|
|
+ newEndTime = calculateNewEndTime(normalizedAddTimeStart, addTimeMinutes);
|
|
|
}
|
|
|
|
|
|
// 校验:不能超过下一个已确认预约的开始时间
|
|
|
- validateAddTimeNotExceedNextReservation(reservation, addTimeStart, newEndTime);
|
|
|
+ validateAddTimeNotExceedNextReservation(reservation, normalizedAddTimeStart, newEndTime);
|
|
|
|
|
|
// 更新预约结束时间
|
|
|
reservation.setEndTime(newEndTime);
|
|
|
@@ -560,6 +620,13 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
throw new RuntimeException("预约已过期,无法核销");
|
|
|
}
|
|
|
|
|
|
+ if(order.getOrderCostType() == 1){
|
|
|
+ //调用退款
|
|
|
+ MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
|
|
|
+ strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
// 更新预约状态为已到店(status = 2)
|
|
|
reservation.setStatus(2); // 已到店
|
|
|
reservation.setActualArrivalTime(new Date());
|
|
|
@@ -575,10 +642,6 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
throw new RuntimeException("更新订单状态失败");
|
|
|
}
|
|
|
|
|
|
- //调用退款
|
|
|
- MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
|
|
|
- strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款");
|
|
|
-
|
|
|
// 核销成功后,发送订金退款短信和通知
|
|
|
try {
|
|
|
sendDepositRefundSmsAndNotice(reservation, order);
|
|
|
@@ -588,6 +651,7 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
// 短信和通知发送失败不影响核销流程,只记录日志
|
|
|
}
|
|
|
|
|
|
+
|
|
|
log.info("核销预约订单成功,verificationCode={}, reservationId={}, orderId={}",
|
|
|
verificationCode, reservation.getId(), order.getId());
|
|
|
return true;
|
|
|
@@ -687,13 +751,57 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 比较两个时间字符串(HH:mm格式)
|
|
|
+ * 标准化时间格式:将 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 {
|
|
|
- SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
|
|
|
- Date date1 = sdf.parse(time1);
|
|
|
- Date date2 = sdf.parse(time2);
|
|
|
+ // 先标准化时间格式
|
|
|
+ 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);
|
|
|
@@ -703,17 +811,22 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
|
|
|
/**
|
|
|
* 计算新的结束时间:加时开始时间 + 加时分钟数
|
|
|
+ * 返回格式:yyyy-MM-dd HH:mm(不含秒)
|
|
|
*/
|
|
|
private String calculateNewEndTime(String addTimeStart, Integer addTimeMinutes) {
|
|
|
try {
|
|
|
- SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
|
|
|
- Date startDate = sdf.parse(addTimeStart);
|
|
|
+ // 解析输入时间(支持 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);
|
|
|
|
|
|
- return sdf.format(calendar.getTime());
|
|
|
+ // 返回格式: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());
|
|
|
@@ -721,6 +834,27 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 计算两个时间之间的分钟差(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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 校验预约是否过期
|
|
|
* 判断当前时间是否超过预约日期 + 结束时间
|
|
|
*/
|