|
|
@@ -32,7 +32,8 @@ import java.util.concurrent.ThreadLocalRandom;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
- * 点餐流程:填写就餐信息 → 写 user_reservation + user_reservation_table(已到店),并更新 store_table 为就餐中;不产生 user_reservation_order
|
|
|
+ * 点餐流程:填写就餐信息 → 写 user_reservation + user_reservation_table(已到店),并更新 store_table 为就餐中;不产生 user_reservation_order。
|
|
|
+ * <p>到店就餐入参:{@code startTime}、若填写 {@code endTime} 仅允许 {@code yyyy-MM-dd HH:mm}(24h,时分各两位);库内同格式。</p>
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
@@ -44,13 +45,23 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
private final UserReservationTableMapper userReservationTableMapper;
|
|
|
|
|
|
private static final int STATUS_ARRIVED = 2;
|
|
|
+ /** 与 {@link UserReservation#getType()} 一致:小程序/扫码点餐填写到店信息 → app扫码 */
|
|
|
+ private static final int RESERVATION_SOURCE_APP_SCAN = 1;
|
|
|
/** 占桌状态:与其它预订冲突检测范围内 */
|
|
|
private static final ZoneId SHANGHAI = ZoneId.of("Asia/Shanghai");
|
|
|
/** 未填结束时间时,按 N 小时作为占用窗口做冲突判断(与订金预订「结束时间」空值场景对齐) */
|
|
|
private static final int DEFAULT_BLOCK_HOURS_WHEN_NO_END = 4;
|
|
|
+ /** 库内标准;到店入参同此格式 */
|
|
|
+ private static final DateTimeFormatter YMD_HM = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
+ /** 冲突检测:历史行可能含秒、或仅存时分 */
|
|
|
+ private static final DateTimeFormatter[] STORED_DATE_TIME_FORMATTERS = new DateTimeFormatter[]{
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
|
|
+ YMD_HM,
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm:ss"),
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm")
|
|
|
+ };
|
|
|
+ /** 冲突检测:仅存时分的历史行 */
|
|
|
private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
- /** 与订桌/库内既有数据一致:varchar 存 yyyy-MM-dd HH:mm */
|
|
|
- private static final DateTimeFormatter STORE_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
@@ -63,21 +74,34 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
throw new RuntimeException("桌号不存在");
|
|
|
}
|
|
|
|
|
|
- Date reservationDate = dto.getReservationDate();
|
|
|
- if (reservationDate == null) {
|
|
|
- reservationDate = Date.from(LocalDate.now(SHANGHAI).atStartOfDay(SHANGHAI).toInstant());
|
|
|
+ String startRaw = dto.getStartTime() != null ? dto.getStartTime().trim() : null;
|
|
|
+ if (!StringUtils.hasText(startRaw)) {
|
|
|
+ throw new RuntimeException("开始时间不能为空");
|
|
|
}
|
|
|
+ LocalDateTime startLdt = parseYmdHmOrNull(startRaw);
|
|
|
+ if (startLdt == null) {
|
|
|
+ throw new RuntimeException("开始时间须为 yyyy-MM-dd HH:mm,例如 2026-03-28 18:30");
|
|
|
+ }
|
|
|
+ if (dto.getReservationDate() != null) {
|
|
|
+ LocalDate dtoDay = dto.getReservationDate().toInstant().atZone(SHANGHAI).toLocalDate();
|
|
|
+ if (!dtoDay.equals(startLdt.toLocalDate())) {
|
|
|
+ throw new RuntimeException("预约日期须与开始时间的日期一致");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Date reservationDate = Date.from(startLdt.toLocalDate().atStartOfDay(SHANGHAI).toInstant());
|
|
|
|
|
|
- LocalDate day = reservationDate.toInstant().atZone(SHANGHAI).toLocalDate();
|
|
|
- LocalTime newStartLt = parseTimeOfDayOnly(dto.getStartTime());
|
|
|
- if (newStartLt == null) {
|
|
|
- throw new RuntimeException("开始时间格式不正确,请使用 HH:mm");
|
|
|
+ String endRaw = trimOrNull(dto.getEndTime());
|
|
|
+ LocalDateTime endLdt;
|
|
|
+ if (endRaw == null) {
|
|
|
+ endLdt = windowEnd(startLdt.toLocalDate(), startLdt.toLocalTime(), null);
|
|
|
+ } else {
|
|
|
+ endLdt = parseYmdHmOrNull(endRaw);
|
|
|
+ if (endLdt == null) {
|
|
|
+ throw new RuntimeException("结束时间须为 yyyy-MM-dd HH:mm,例如 2026-03-28 20:00");
|
|
|
+ }
|
|
|
}
|
|
|
- LocalTime newEndLt = parseTimeOfDayOnly(dto.getEndTime());
|
|
|
- assertNoTableBookingConflict(table.getId(), table.getStoreId(), day, newStartLt, newEndLt);
|
|
|
|
|
|
- LocalDateTime startLdt = LocalDateTime.of(day, newStartLt);
|
|
|
- LocalDateTime endLdt = windowEnd(day, newStartLt, newEndLt);
|
|
|
+ assertNoTableBookingConflict(table.getId(), table.getStoreId(), startLdt, endLdt);
|
|
|
|
|
|
Date now = new Date();
|
|
|
UserReservation entity = new UserReservation();
|
|
|
@@ -85,11 +109,12 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
entity.setUserId(userId);
|
|
|
entity.setStoreId(table.getStoreId());
|
|
|
entity.setReservationDate(reservationDate);
|
|
|
- entity.setStartTime(startLdt.format(STORE_DATE_TIME));
|
|
|
- entity.setEndTime(endLdt.format(STORE_DATE_TIME));
|
|
|
+ entity.setStartTime(startLdt.format(YMD_HM));
|
|
|
+ entity.setEndTime(endLdt.format(YMD_HM));
|
|
|
entity.setGuestCount(dto.getGuestCount());
|
|
|
entity.setCategoryId(table.getCategoryId());
|
|
|
entity.setStatus(STATUS_ARRIVED);
|
|
|
+ entity.setType(RESERVATION_SOURCE_APP_SCAN);
|
|
|
entity.setActualArrivalTime(now);
|
|
|
entity.setRemark(trimOrNull(dto.getRemark()));
|
|
|
entity.setReservationUserName(trimOrNull(dto.getReservationUserName()));
|
|
|
@@ -108,8 +133,7 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
link.setUpdatedUserId(userId);
|
|
|
userReservationTableMapper.insert(link);
|
|
|
|
|
|
- // 与 getDiningPageInfo 首客传人数一致:提交就餐信息成功后,餐桌置为就餐中并写入人数,
|
|
|
- // 便于 /table-dining-status 与其它用户跳过「选人数」前置。
|
|
|
+ // 与 page-info 首客传人数一致:便于 /table-dining-status
|
|
|
Integer st = table.getStatus();
|
|
|
if (st == null || st == 0 || st == 2) {
|
|
|
table.setStatus(1);
|
|
|
@@ -136,11 +160,23 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
return "RV" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
|
|
|
}
|
|
|
|
|
|
+ /** 到店入参仅接受 yyyy-MM-dd HH:mm */
|
|
|
+ private static LocalDateTime parseYmdHmOrNull(String raw) {
|
|
|
+ if (!StringUtils.hasText(raw)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDateTime.parse(raw.trim(), YMD_HM);
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 与其它有效预订(同桌、同日、状态为待确认/已确认/已到店)时段是否重叠;重叠则不允许到店就餐。
|
|
|
*/
|
|
|
- private void assertNoTableBookingConflict(Integer tableId, Integer storeId, LocalDate day,
|
|
|
- LocalTime newStart, LocalTime newEnd) {
|
|
|
+ private void assertNoTableBookingConflict(Integer tableId, Integer storeId,
|
|
|
+ LocalDateTime newStartLdt, LocalDateTime newEndLdt) {
|
|
|
List<UserReservationTable> links = userReservationTableMapper.selectList(
|
|
|
new LambdaQueryWrapper<UserReservationTable>()
|
|
|
.eq(UserReservationTable::getTableId, tableId)
|
|
|
@@ -155,8 +191,7 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
if (resIds.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
- LocalDateTime newStartLdt = LocalDateTime.of(day, newStart);
|
|
|
- LocalDateTime newEndLdt = windowEnd(day, newStart, newEnd);
|
|
|
+ LocalDate day = newStartLdt.toLocalDate();
|
|
|
|
|
|
List<UserReservation> others = new ArrayList<>(this.listByIds(resIds));
|
|
|
if (others.isEmpty()) {
|
|
|
@@ -173,11 +208,11 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
if (otherDay == null || !day.equals(otherDay)) {
|
|
|
continue;
|
|
|
}
|
|
|
- LocalDateTime oStartLdt = parseStoredStartLdt(otherDay, other.getStartTime());
|
|
|
+ LocalDateTime oStartLdt = parseOtherReservationStart(otherDay, other.getStartTime());
|
|
|
if (oStartLdt == null) {
|
|
|
continue;
|
|
|
}
|
|
|
- LocalDateTime oEndLdt = parseStoredEndLdt(oStartLdt, other.getEndTime());
|
|
|
+ LocalDateTime oEndLdt = parseOtherReservationEnd(oStartLdt, other.getEndTime());
|
|
|
if (newStartLdt.isBefore(oEndLdt) && oStartLdt.isBefore(newEndLdt)) {
|
|
|
log.warn("到店就餐信息与既有预订冲突 tableId={} day={} new=[{} - {}] otherResId={} other=[{} - {}]",
|
|
|
tableId, day, newStartLdt, newEndLdt, other.getId(), oStartLdt, oEndLdt);
|
|
|
@@ -198,38 +233,13 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
return d.toInstant().atZone(SHANGHAI).toLocalDate();
|
|
|
}
|
|
|
|
|
|
- /** 仅时分:HH:mm / H:mm */
|
|
|
- private static LocalTime parseTimeOfDayOnly(String raw) {
|
|
|
- if (!StringUtils.hasText(raw)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- String s = raw.trim();
|
|
|
- if (s.contains(" ")) {
|
|
|
- s = s.substring(s.lastIndexOf(' ') + 1).trim();
|
|
|
- }
|
|
|
- try {
|
|
|
- return LocalTime.parse(s, HM);
|
|
|
- } catch (DateTimeParseException e) {
|
|
|
- try {
|
|
|
- return LocalTime.parse(s, DateTimeFormatter.ofPattern("H:mm"));
|
|
|
- } catch (DateTimeParseException e2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private static LocalDateTime tryParseFullDateTime(String raw) {
|
|
|
+ /** 库内既有预订:整段日期时间(可含秒) */
|
|
|
+ private static LocalDateTime parseStoredDateTimeOrNull(String raw) {
|
|
|
if (!StringUtils.hasText(raw) || !raw.trim().contains(" ")) {
|
|
|
return null;
|
|
|
}
|
|
|
String s = raw.trim();
|
|
|
- DateTimeFormatter[] fmps = new DateTimeFormatter[]{
|
|
|
- DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
|
|
- DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
|
|
|
- DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm:ss"),
|
|
|
- DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm")
|
|
|
- };
|
|
|
- for (DateTimeFormatter f : fmps) {
|
|
|
+ for (DateTimeFormatter f : STORED_DATE_TIME_FORMATTERS) {
|
|
|
try {
|
|
|
return LocalDateTime.parse(s, f);
|
|
|
} catch (DateTimeParseException ignored) {
|
|
|
@@ -238,12 +248,12 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private static LocalDateTime parseStoredStartLdt(LocalDate reservationDay, String raw) {
|
|
|
+ private static LocalDateTime parseOtherReservationStart(LocalDate reservationDay, String raw) {
|
|
|
if (!StringUtils.hasText(raw)) {
|
|
|
return null;
|
|
|
}
|
|
|
String s = raw.trim();
|
|
|
- LocalDateTime full = tryParseFullDateTime(s);
|
|
|
+ LocalDateTime full = parseStoredDateTimeOrNull(s);
|
|
|
if (full != null) {
|
|
|
return full;
|
|
|
}
|
|
|
@@ -254,25 +264,45 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
|
|
|
return LocalDateTime.of(reservationDay, t);
|
|
|
}
|
|
|
|
|
|
- private static LocalDateTime parseStoredEndLdt(LocalDateTime startLdt, String endRaw) {
|
|
|
+ private static LocalDateTime parseOtherReservationEnd(LocalDateTime startLdt, String endRaw) {
|
|
|
if (startLdt == null) {
|
|
|
return null;
|
|
|
}
|
|
|
if (!StringUtils.hasText(endRaw)) {
|
|
|
return windowEnd(startLdt.toLocalDate(), startLdt.toLocalTime(), null);
|
|
|
}
|
|
|
- String s = endRaw.trim();
|
|
|
- LocalDateTime full = tryParseFullDateTime(s);
|
|
|
+ String w = endRaw.trim();
|
|
|
+ LocalDateTime full = parseStoredDateTimeOrNull(w);
|
|
|
if (full != null) {
|
|
|
return full;
|
|
|
}
|
|
|
- LocalTime endT = parseTimeOfDayOnly(s);
|
|
|
+ LocalTime endT = parseTimeOfDayOnly(w);
|
|
|
if (endT == null) {
|
|
|
return windowEnd(startLdt.toLocalDate(), startLdt.toLocalTime(), null);
|
|
|
}
|
|
|
return windowEnd(startLdt.toLocalDate(), startLdt.toLocalTime(), endT);
|
|
|
}
|
|
|
|
|
|
+ /** 仅时分:HH:mm / H:mm(含「日期 + 空格 + 时分」时取末段) */
|
|
|
+ private static LocalTime parseTimeOfDayOnly(String raw) {
|
|
|
+ if (!StringUtils.hasText(raw)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String s = raw.trim();
|
|
|
+ if (s.contains(" ")) {
|
|
|
+ s = s.substring(s.lastIndexOf(' ') + 1).trim();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalTime.parse(s, HM);
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ try {
|
|
|
+ return LocalTime.parse(s, DateTimeFormatter.ofPattern("H:mm"));
|
|
|
+ } catch (DateTimeParseException e2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 预约结束时刻:结束早于或等于开始(同一张日历日上的钟点)则视为跨日至次日;结束时间为空则用默认时长。
|
|
|
*/
|