|
|
@@ -1,7 +1,6 @@
|
|
|
package shop.alien.store.service.impl;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
@@ -26,7 +25,9 @@ import shop.alien.store.vo.ReservationOrderDetailVo;
|
|
|
import shop.alien.store.vo.ReservationOrderPageVo;
|
|
|
import shop.alien.mapper.UserReservationMapper;
|
|
|
import shop.alien.mapper.UserReservationTableMapper;
|
|
|
+import shop.alien.store.config.BaseRedisService;
|
|
|
import shop.alien.store.service.*;
|
|
|
+import shop.alien.store.util.ali.AliSms;
|
|
|
import shop.alien.util.common.UniqueRandomNumGenerator;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
@@ -67,6 +68,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
|
|
|
private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
|
|
|
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
+ private final AliSms aliSms;
|
|
|
+ private final ReservationNoticeAsyncService reservationNoticeAsyncService;
|
|
|
+ private final ArrivalReminderNoticeService arrivalReminderNoticeService;
|
|
|
+
|
|
|
private ReservationOrderPageService reservationOrderPageService;
|
|
|
|
|
|
@Autowired
|
|
|
@@ -90,6 +96,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
private static final int MAX_DAYS_TO_CHECK = 366;
|
|
|
/** 全天预订时使用的结束分钟数(24*60,即到次日0点) */
|
|
|
private static final int MINUTES_DAY_END = 24 * 60;
|
|
|
+ /** 到店提醒:短信防重(保持与原 key 一致,避免已标记订单重复发短信) */
|
|
|
+ private static final String REDIS_KEY_ARRIVAL_SMS_PREFIX = "reservation:arrival:reminder:order:";
|
|
|
+ /** 到店提醒:站内通知防重(与短信独立,短信失败可多次重试) */
|
|
|
+ private static final String REDIS_KEY_ARRIVAL_NOTICE_PREFIX = "reservation:arrival:notice:order:";
|
|
|
+ private static final long ARRIVAL_REMINDER_REDIS_TTL_SECONDS = 30 * 60;
|
|
|
|
|
|
@Override
|
|
|
public Integer add(UserReservationDTO dto) {
|
|
|
@@ -192,6 +203,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
throw new RuntimeException("预约不存在");
|
|
|
}
|
|
|
|
|
|
+ // 修改前先取原时间与桌位(用于商家订单修改提醒)
|
|
|
+ String oldDateTime = existing.getStartTime() != null && !existing.getStartTime().trim().isEmpty()
|
|
|
+ ? existing.getStartTime().trim() : "未知时间";
|
|
|
+ String oldTableNumber = tableIdsToTableNumberString(listTableIdsByReservationId(existing.getId()));
|
|
|
+
|
|
|
UserReservation entity = new UserReservation();
|
|
|
BeanUtils.copyProperties(dto, entity, "tableIds", "reservationNo");
|
|
|
entity.setId(existing.getId());
|
|
|
@@ -200,6 +216,12 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
|
|
|
saveReservationTables(existing.getId(), dto.getTableIds());
|
|
|
|
|
|
+ // 修改后文案,并异步给商家发送订单修改提醒
|
|
|
+ String newDateTime = dto.getStartTime() != null && !dto.getStartTime().trim().isEmpty()
|
|
|
+ ? dto.getStartTime().trim() : "未知时间";
|
|
|
+ String newTableNumber = tableIdsToTableNumberString(dto.getTableIds());
|
|
|
+ reservationNoticeAsyncService.sendUpdateReminderToStore(existing.getId(), oldDateTime, oldTableNumber, newDateTime, newTableNumber);
|
|
|
+
|
|
|
// 与 add 一致:同步 user_reservation_order(无则创建,有则仅待支付时按门店配置刷新)
|
|
|
UserReservation updatedReservation = this.getById(existing.getId());
|
|
|
LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
@@ -228,28 +250,37 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
|
|
|
@Override
|
|
|
public boolean removeReservation(Integer id) {
|
|
|
-
|
|
|
- UserReservationOrder one = userReservationOrderService.getOne(new LambdaUpdateWrapper<UserReservationOrder>()
|
|
|
+ UserReservationOrder one = userReservationOrderService.getOne(new LambdaQueryWrapper<UserReservationOrder>()
|
|
|
.eq(UserReservationOrder::getId, id));
|
|
|
if (one == null) {
|
|
|
throw new RuntimeException("预约不存在");
|
|
|
}
|
|
|
+ Integer reservationId = one.getReservationId();
|
|
|
|
|
|
// 当订单为未支付时,订单状态变为已关闭
|
|
|
int orderStatus = 4;
|
|
|
- if(one.getPaymentStatus()!= null && one.getPaymentStatus() == 0){
|
|
|
+ if (one.getPaymentStatus() != null && one.getPaymentStatus() == 0) {
|
|
|
orderStatus = 5;
|
|
|
}
|
|
|
|
|
|
- // 订单状态置为已取消(4)
|
|
|
+ // 订单状态置为已取消(4)或已关闭(5)
|
|
|
userReservationOrderService.update(
|
|
|
new LambdaUpdateWrapper<UserReservationOrder>()
|
|
|
.eq(UserReservationOrder::getId, one.getId())
|
|
|
.set(UserReservationOrder::getOrderStatus, orderStatus));
|
|
|
// 预约不再逻辑删除,仅将 status 置为已取消(3)
|
|
|
- return this.update(new LambdaUpdateWrapper<UserReservation>()
|
|
|
- .eq(UserReservation::getId, one.getReservationId())
|
|
|
+ boolean updated = reservationId != null && this.update(new LambdaUpdateWrapper<UserReservation>()
|
|
|
+ .eq(UserReservation::getId, reservationId)
|
|
|
.set(UserReservation::getStatus, STATUS_CANCELLED));
|
|
|
+ if (reservationId == null) {
|
|
|
+ updated = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 异步查询通知参数并给商家发送订单取消提醒(查询+落库均在异步中)
|
|
|
+ if (updated) {
|
|
|
+ reservationNoticeAsyncService.sendCancelReminderToStore(id);
|
|
|
+ }
|
|
|
+ return updated;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -332,6 +363,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)
|
|
|
@@ -439,7 +475,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
|
|
|
List<StoreBookingTable> storeBookingTables = storeBookingTableService.list(new LambdaQueryWrapper<StoreBookingTable>().eq(StoreBookingTable::getStoreId, storeId));
|
|
|
list.put("storeBookingTables", storeBookingTables);
|
|
|
- List<StoreBookingCategory> storeBookingCategorys = storeBookingCategoryService.list(new LambdaQueryWrapper<StoreBookingCategory>().eq(StoreBookingCategory::getStoreId, storeId));
|
|
|
+ List<StoreBookingCategory> storeBookingCategorys = storeBookingCategoryService.list(
|
|
|
+ new LambdaQueryWrapper<StoreBookingCategory>()
|
|
|
+ .eq(StoreBookingCategory::getStoreId, storeId)
|
|
|
+ .eq(StoreBookingCategory::getIsDisplay, 1)
|
|
|
+ .orderByDesc(StoreBookingCategory::getSort));
|
|
|
list.put("storeBookingCategorys", storeBookingCategorys);
|
|
|
StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);
|
|
|
list.put("storeInfo", storeInfo);
|
|
|
@@ -462,13 +502,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;
|
|
|
}
|
|
|
@@ -493,20 +538,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);
|
|
|
@@ -793,6 +844,21 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
return list.stream().map(UserReservationTable::getTableId).collect(Collectors.toList());
|
|
|
}
|
|
|
|
|
|
+ /** 将桌位ID列表转为桌号文案,如 "A01,A02",空或查不到为 "未知桌号" */
|
|
|
+ private String tableIdsToTableNumberString(List<Integer> tableIds) {
|
|
|
+ if (tableIds == null || tableIds.isEmpty()) {
|
|
|
+ return "未知桌号";
|
|
|
+ }
|
|
|
+ List<String> numbers = tableIds.stream()
|
|
|
+ .map(tableId -> {
|
|
|
+ StoreBookingTable t = storeBookingTableService.getById(tableId);
|
|
|
+ return t != null && t.getTableNumber() != null ? t.getTableNumber().trim() : null;
|
|
|
+ })
|
|
|
+ .filter(n -> n != null && !n.isEmpty())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ return numbers.isEmpty() ? "未知桌号" : String.join(",", numbers);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 保存预约与桌号关联:先物理删除该预约下原有关联再插入
|
|
|
*/
|
|
|
@@ -1111,10 +1177,116 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
.eq(UserReservationOrder::getOrderStatus, ORDER_STATUS_TO_USE)
|
|
|
.set(UserReservationOrder::getOrderStatus, ORDER_STATUS_EXPIRED)
|
|
|
.set(UserReservationOrder::getUpdatedTime, now));
|
|
|
+ // 3. 异步给商家发送订单过期提醒
|
|
|
+ for (Integer reservationId : toUpdateReservationIds) {
|
|
|
+ reservationNoticeAsyncService.sendExpiredReminderToStore(reservationId);
|
|
|
+ }
|
|
|
log.info("预订未到店超时定时任务:更新 reservationIds={} 条为未到店超时/订单已过期", toUpdateReservationIds.size());
|
|
|
return toUpdateReservationIds.size();
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public int sendArrivalReminderSms() {
|
|
|
+ List<Integer> orderIds = userReservationOrderMapper.listOrderIdsForArrivalReminder();
|
|
|
+ if (orderIds == null || orderIds.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ int smsSuccessCount = 0;
|
|
|
+ for (Integer orderId : orderIds) {
|
|
|
+ try {
|
|
|
+ String noticeKey = REDIS_KEY_ARRIVAL_NOTICE_PREFIX + orderId;
|
|
|
+ String smsKey = REDIS_KEY_ARRIVAL_SMS_PREFIX + orderId;
|
|
|
+ if (baseRedisService.hasKey(noticeKey) && baseRedisService.hasKey(smsKey)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (sendArrivalReminderSmsForOrder(orderId)) {
|
|
|
+ smsSuccessCount++;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("到店提醒任务处理异常,orderId={}", orderId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("到店提醒定时任务结束,符合条件订单数={}, 短信成功数={}", orderIds.size(), smsSuccessCount);
|
|
|
+ return smsSuccessCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单订单到店提醒:先写站内通知(与短信是否成功无关,各自 Redis 防重),再发短信。
|
|
|
+ *
|
|
|
+ * @return 本次是否短信发送成功(用于统计)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public boolean sendArrivalReminderSmsForOrder(Integer orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(orderId);
|
|
|
+ if (order == null || order.getReservationId() == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ UserReservation reservation = this.getById(order.getReservationId());
|
|
|
+ if (reservation == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (order.getOrderStatus() == null || order.getOrderStatus() != ORDER_STATUS_TO_USE) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String phone = reservation.getReservationUserPhone();
|
|
|
+ if (phone == null || phone.trim().isEmpty()) {
|
|
|
+ log.warn("到店提醒跳过:预约人电话为空,orderId={}", orderId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ String startTimeForSms = reservation.getStartTime() != null ? reservation.getStartTime().trim() : "";
|
|
|
+ if (startTimeForSms.isEmpty()) {
|
|
|
+ startTimeForSms = "未知时间";
|
|
|
+ }
|
|
|
+ StoreInfo storeInfo = storeInfoService.getById(reservation.getStoreId());
|
|
|
+ String storeName = storeInfo != null && storeInfo.getStoreName() != null ? storeInfo.getStoreName() : "未知店铺";
|
|
|
+ String tableNumbers = resolveReservationTableNumbers(reservation.getId());
|
|
|
+
|
|
|
+ String noticeKey = REDIS_KEY_ARRIVAL_NOTICE_PREFIX + orderId;
|
|
|
+ if (!baseRedisService.hasKey(noticeKey)) {
|
|
|
+ boolean noticeOk = arrivalReminderNoticeService.sendArrivalReminderNotice(
|
|
|
+ phone, orderId, order.getId(), reservation.getId(),
|
|
|
+ reservation.getStartTime(), storeName, tableNumbers);
|
|
|
+ if (noticeOk) {
|
|
|
+ baseRedisService.setString(noticeKey, "1", Long.valueOf(ARRIVAL_REMINDER_REDIS_TTL_SECONDS));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String smsKey = REDIS_KEY_ARRIVAL_SMS_PREFIX + orderId;
|
|
|
+ if (baseRedisService.hasKey(smsKey)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ Integer smsResult = aliSms.sendArrivalReminderSms(phone, startTimeForSms, storeName, tableNumbers);
|
|
|
+ if (smsResult != null && smsResult == 1) {
|
|
|
+ log.info("到店提醒短信发送成功,orderId={}, orderSn={}, phone={}", orderId, order.getOrderSn(), phone);
|
|
|
+ baseRedisService.setString(smsKey, "1", Long.valueOf(ARRIVAL_REMINDER_REDIS_TTL_SECONDS));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveReservationTableNumbers(Integer reservationId) {
|
|
|
+ LambdaQueryWrapper<UserReservationTable> w = new LambdaQueryWrapper<>();
|
|
|
+ w.eq(UserReservationTable::getReservationId, reservationId)
|
|
|
+ .eq(UserReservationTable::getDeleteFlag, 0)
|
|
|
+ .orderByAsc(UserReservationTable::getSort);
|
|
|
+ List<UserReservationTable> links = userReservationTableMapper.selectList(w);
|
|
|
+ if (links == null || links.isEmpty()) {
|
|
|
+ return "未知桌号";
|
|
|
+ }
|
|
|
+ List<String> nums = links.stream()
|
|
|
+ .map(rt -> {
|
|
|
+ StoreBookingTable t = storeBookingTableService.getById(rt.getTableId());
|
|
|
+ return t != null && t.getTableNumber() != null ? t.getTableNumber().trim() : null;
|
|
|
+ })
|
|
|
+ .filter(n -> n != null && !n.isEmpty())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ return nums.isEmpty() ? "未知桌号" : String.join(",", nums);
|
|
|
+ }
|
|
|
+
|
|
|
/** 将页面 VO 字段复制到详情 VO,供前端订单详情页与 page 接口一致展示 */
|
|
|
private void copyPageVoToDetailVo(ReservationOrderPageVo page, ReservationOrderDetailVo detail) {
|
|
|
detail.setOrderId(page.getOrderId());
|