|
|
@@ -1,6 +1,7 @@
|
|
|
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.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
@@ -14,10 +15,13 @@ import shop.alien.entity.store.dto.UserReservationDTO;
|
|
|
import shop.alien.entity.store.vo.StoreMainInfoVo;
|
|
|
import shop.alien.entity.store.vo.StoreReservationListVo;
|
|
|
import shop.alien.entity.store.vo.UserReservationVo;
|
|
|
+import shop.alien.store.vo.BookingTableItemVo;
|
|
|
+import shop.alien.store.vo.ReservationOrderDetailVo;
|
|
|
import shop.alien.mapper.UserReservationMapper;
|
|
|
import shop.alien.mapper.UserReservationTableMapper;
|
|
|
import shop.alien.store.service.*;
|
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
import java.text.ParseException;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.*;
|
|
|
@@ -47,12 +51,16 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
|
|
|
private final UserReservationOrderService userReservationOrderService;
|
|
|
|
|
|
+ private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
|
|
|
+
|
|
|
/** 预约状态:待确认 */
|
|
|
private static final int STATUS_PENDING = 0;
|
|
|
/** 预约状态:已取消(不参与约满统计与展示) */
|
|
|
private static final int STATUS_CANCELLED = 3;
|
|
|
/** 查找首个未约满日期时,最多往后检查的天数 */
|
|
|
private static final int MAX_DAYS_TO_CHECK = 366;
|
|
|
+ /** 全天预订时使用的结束分钟数(24*60,即到次日0点) */
|
|
|
+ private static final int MINUTES_DAY_END = 24 * 60;
|
|
|
|
|
|
@Override
|
|
|
public Integer add(UserReservationDTO dto) {
|
|
|
@@ -79,7 +87,49 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
this.save(entity);
|
|
|
|
|
|
saveReservationTables(entity.getId(), dto.getTableIds());
|
|
|
- return entity.getId();
|
|
|
+
|
|
|
+ // 同步创建预订订单(user_reservation_order),待支付时写入 Redis 15 分钟超时
|
|
|
+ UserReservationOrder order = buildReservationOrder(entity);
|
|
|
+ userReservationOrderService.save(order);
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
|
|
|
+ reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
|
|
|
+ }
|
|
|
+
|
|
|
+ return order.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据预约及门店预订配置构建预订订单
|
|
|
+ */
|
|
|
+ private UserReservationOrder buildReservationOrder(UserReservation reservation) {
|
|
|
+ UserReservationOrder order = new UserReservationOrder();
|
|
|
+ order.setOrderSn(userReservationOrderService.generateOrderSn());
|
|
|
+ order.setReservationId(reservation.getId());
|
|
|
+ order.setUserId(reservation.getUserId());
|
|
|
+ order.setStoreId(reservation.getStoreId());
|
|
|
+ order.setOrderStatus(0);
|
|
|
+ order.setPaymentStatus(0);
|
|
|
+ order.setIsMerchantReservation(0);
|
|
|
+
|
|
|
+ StoreBookingSettings settings = storeBookingSettingsService.getByStoreId(reservation.getStoreId());
|
|
|
+ if (settings != null && "1".equals(settings.getReservation()) && settings.getReservationMoney() != null && settings.getReservationMoney() > 0) {
|
|
|
+ order.setOrderCostType(1);
|
|
|
+ order.setDepositAmount(BigDecimal.valueOf(settings.getReservationMoney()));
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.add(Calendar.MINUTE, 15);
|
|
|
+ order.setPaymentDeadline(cal.getTime());
|
|
|
+ order.setCancellationPolicyType(1);
|
|
|
+ if (settings.getRetainPositionFlag() != null && settings.getRetainPositionFlag() == 1 && settings.getRetentionDuration() != null) {
|
|
|
+ order.setLateArrivalGraceMinutes(settings.getRetentionDuration());
|
|
|
+ }
|
|
|
+ order.setDepositRefundRule("到店就餐24小时后自动原路返回");
|
|
|
+ } else {
|
|
|
+ order.setOrderCostType(0);
|
|
|
+ order.setDepositAmount(BigDecimal.ZERO);
|
|
|
+ order.setCancellationPolicyType(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ return order;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -124,6 +174,58 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ public UserReservationVo getDetailByStoreIdAndReservationTableId(Integer storeId, Integer userReservationTableId) {
|
|
|
+ if (storeId == null || userReservationTableId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ UserReservationTable link = userReservationTableMapper.selectById(userReservationTableId);
|
|
|
+ if (link == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ UserReservation reservation = this.getById(link.getReservationId());
|
|
|
+ if (reservation == null || !storeId.equals(reservation.getStoreId())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return getDetail(reservation.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<UserReservationVo> listDetailByStoreIdAndReservationTableIds(Integer storeId, Integer userReservationTableId, Date reservationDate) {
|
|
|
+ if (storeId == null || userReservationTableId == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<UserReservationTable> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(UserReservationTable::getTableId, userReservationTableId);
|
|
|
+ List<UserReservationTable> userReservationTables = userReservationTableMapper.selectList(wrapper);
|
|
|
+ List<UserReservationVo> list = new ArrayList<>();
|
|
|
+ Calendar calReservation = reservationDate != null ? calendarOf(reservationDate) : null;
|
|
|
+ for (UserReservationTable userReservationTable : userReservationTables) {
|
|
|
+ UserReservation reservation = this.getById(userReservationTable.getReservationId());
|
|
|
+ if (reservation == null || !storeId.equals(reservation.getStoreId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (calReservation != null && reservation.getReservationDate() != null) {
|
|
|
+ Calendar cal = calendarOf(reservation.getReservationDate());
|
|
|
+ if (cal.get(Calendar.YEAR) != calReservation.get(Calendar.YEAR)
|
|
|
+ || cal.get(Calendar.DAY_OF_YEAR) != calReservation.get(Calendar.DAY_OF_YEAR)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ UserReservationVo vo = getDetail(reservation.getId());
|
|
|
+ if (vo != null) {
|
|
|
+ list.add(vo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Calendar calendarOf(Date date) {
|
|
|
+ Calendar c = Calendar.getInstance();
|
|
|
+ c.setTime(date);
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
public IPage<UserReservationVo> pageList(Integer userId, Integer storeId, Integer status,
|
|
|
Date dateFrom, Date dateTo,
|
|
|
Integer pageNum, Integer pageSize) {
|
|
|
@@ -161,29 +263,256 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从今天起查找第一个未约满的日期,并返回该日的预约数据。
|
|
|
- * 判断逻辑:若当天已预约人数(guest_count 之和)>= 门店单时段最大容纳人数,则视为约满,顺延到下一天再判断。
|
|
|
+ * 按预定日期查询指定日期的定桌情况。传入 reservationDate 时仅查询该日,不进行“首个未约满日期”的顺延逻辑。
|
|
|
+ *
|
|
|
+ * @param storeId 门店ID
|
|
|
+ * @param reservationDate 预定日期,为 null 时行为同 findFirstAvailableDayReservations(storeId)
|
|
|
+ * @return Map:date、reservations、tableStatusList
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> findFirstAvailableDayReservations(Integer storeId, Date reservationDate) {
|
|
|
+ if (reservationDate != null) {
|
|
|
+ return getDayBookingStatus(storeId, reservationDate);
|
|
|
+ }
|
|
|
+ return findFirstAvailableDayReservations(storeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 "HH:mm" 解析为当日 0 点起的分钟数,解析失败返回 -1。
|
|
|
+ */
|
|
|
+ private static int timeToMinutes(String hhmm) {
|
|
|
+ if (hhmm == null) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ String[] parts = hhmm.trim().split(":");
|
|
|
+ if (parts.length < 2) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int h = Integer.parseInt(parts[0].trim());
|
|
|
+ int m = Integer.parseInt(parts[1].trim());
|
|
|
+ if (h < 0 || h > 24 || m < 0 || m > 59) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ return h * 60 + m;
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 商户营业时间类型:正常营业时间 */
|
|
|
+ private static final int BUSINESS_TYPE_NORMAL = 1;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 store_booking_settings 取当日可预订时段 [开始分钟, 结束分钟]。
|
|
|
+ * 非全天时用 booking_start_time、booking_end_time;若二者为空则取商户运营时间(store_business_info 正常营业的 start_time/end_time);全天、未配置或仍无效时用 0 到 24*60。
|
|
|
+ */
|
|
|
+ private int[] getBookingRangeMinutes(Integer storeId) {
|
|
|
+ int[] range = new int[]{0, MINUTES_DAY_END};
|
|
|
+ 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) {
|
|
|
+ return range;
|
|
|
+ }
|
|
|
+ int start = timeToMinutes(settings.getBookingStartTime());
|
|
|
+ int end = timeToMinutes(settings.getBookingEndTime());
|
|
|
+ if (start >= 0 && end > start) {
|
|
|
+ range[0] = start;
|
|
|
+ range[1] = end;
|
|
|
+ return range;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 预订开始/结束时间为空或无效时,取商户运营时间(营业时间)
|
|
|
+ StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);
|
|
|
+ if (storeInfo != null && storeInfo.getStoreBusinessInfo() != null && !storeInfo.getStoreBusinessInfo().isEmpty()) {
|
|
|
+ List<StoreBusinessInfo> normalHours = storeInfo.getStoreBusinessInfo().stream()
|
|
|
+ .filter(b -> b.getBusinessType() != null && b.getBusinessType() == BUSINESS_TYPE_NORMAL)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (!normalHours.isEmpty()) {
|
|
|
+ int minStart = MINUTES_DAY_END;
|
|
|
+ int maxEnd = 0;
|
|
|
+ for (StoreBusinessInfo b : normalHours) {
|
|
|
+ int s = timeToMinutes(b.getStartTime());
|
|
|
+ int e = timeToMinutes(b.getEndTime());
|
|
|
+ if (s >= 0) {
|
|
|
+ minStart = Math.min(minStart, s);
|
|
|
+ }
|
|
|
+ if (e > 0) {
|
|
|
+ maxEnd = Math.max(maxEnd, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (minStart < MINUTES_DAY_END && maxEnd > 0 && maxEnd > minStart) {
|
|
|
+ range[0] = minStart;
|
|
|
+ range[1] = maxEnd;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return range;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 合并重叠/相邻时间段,并判断是否完全覆盖 [rangeStart, rangeEnd]。
|
|
|
+ * 将各段限制在 range 内后合并,若覆盖总长度等于 (rangeEnd - rangeStart) 则返回 true。
|
|
|
+ */
|
|
|
+ private static boolean isFullCoverage(List<int[]> segments, int rangeStart, int rangeEnd) {
|
|
|
+ if (segments.isEmpty()) {
|
|
|
+ return rangeStart >= rangeEnd;
|
|
|
+ }
|
|
|
+ List<int[]> clipped = new ArrayList<>();
|
|
|
+ for (int[] seg : segments) {
|
|
|
+ int a = Math.max(seg[0], rangeStart);
|
|
|
+ int b = Math.min(seg[1], rangeEnd);
|
|
|
+ if (a < b) {
|
|
|
+ clipped.add(new int[]{a, b});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (clipped.isEmpty()) {
|
|
|
+ return rangeStart >= rangeEnd;
|
|
|
+ }
|
|
|
+ clipped.sort(Comparator.comparingInt(s -> s[0]));
|
|
|
+ List<int[]> merged = new ArrayList<>();
|
|
|
+ merged.add(clipped.get(0).clone());
|
|
|
+ for (int i = 1; i < clipped.size(); i++) {
|
|
|
+ int[] cur = clipped.get(i);
|
|
|
+ int[] last = merged.get(merged.size() - 1);
|
|
|
+ // 允许最多 1 分钟间隙视为连续(如 09:00 结束与 09:01 开始),避免因微小间隙判为未约满
|
|
|
+ if (cur[0] <= last[1] + 1) {
|
|
|
+ last[1] = Math.max(last[1], cur[1]);
|
|
|
+ } else {
|
|
|
+ merged.add(cur.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ int total = 0;
|
|
|
+ for (int[] m : merged) {
|
|
|
+ total += (m[1] - m[0]);
|
|
|
+ }
|
|
|
+ // 允许总覆盖与目标时长差 1 分钟仍视为约满(兼容边界或舍入误差)
|
|
|
+ return total >= (rangeEnd - rangeStart) - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按“商户可预订时段 + 该桌当日所有预约时间段拼接后是否完全覆盖”计算每桌的 full。
|
|
|
+ * 使用 store_booking_settings 的 booking_start_time、booking_end_time 作为当日可预订起止,与 user_reservation 的 start_time、end_time 及 user_reservation_table 的 table_id 对比,只有全部约满才返回 true。
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> buildTableStatusListForDay(Integer storeId, Date dayStart, Date dayEnd,
|
|
|
+ List<StoreBookingTable> storeTables,
|
|
|
+ List<UserReservation> dayReservations,
|
|
|
+ List<UserReservationTable> dayTableLinks,
|
|
|
+ int bookingStartMin, int bookingEndMin) {
|
|
|
+ Map<Integer, List<UserReservation>> reservationsByTableId = new HashMap<>();
|
|
|
+ Map<Integer, UserReservation> resMap = dayReservations.stream().collect(Collectors.toMap(UserReservation::getId, r -> r, (a, b) -> a));
|
|
|
+ for (UserReservationTable rt : dayTableLinks) {
|
|
|
+ UserReservation r = resMap.get(rt.getReservationId());
|
|
|
+ if (r != null) {
|
|
|
+ reservationsByTableId.computeIfAbsent(rt.getTableId(), k -> new ArrayList<>()).add(r);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return storeTables.stream().map(t -> {
|
|
|
+ Map<String, Object> row = new HashMap<>();
|
|
|
+ row.put("tableId", t.getId());
|
|
|
+ row.put("tableNumber", t.getTableNumber());
|
|
|
+ row.put("seatingCapacity", t.getSeatingCapacity());
|
|
|
+ List<UserReservation> tableDayReservations = reservationsByTableId.getOrDefault(t.getId(), Collections.emptyList());
|
|
|
+ List<int[]> segments = new ArrayList<>();
|
|
|
+ for (UserReservation r : tableDayReservations) {
|
|
|
+ int s = timeToMinutes(r.getStartTime());
|
|
|
+ int e = timeToMinutes(r.getEndTime());
|
|
|
+ if (s >= 0 && e > s) {
|
|
|
+ segments.add(new int[]{s, e});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ boolean full = isFullCoverage(segments, bookingStartMin, bookingEndMin);
|
|
|
+ row.put("full", full);
|
|
|
+ return row;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询指定日期的定桌情况:该日预约列表 + 每个选座的约满标识(full)。
|
|
|
+ * 以 store_booking_settings 的 booking_start_time、booking_end_time 为当日可预订时段,结合 user_reservation(reservation_date、start_time、end_time)与 user_reservation_table(table_id)找出该日每桌所有预约,时间段拼接后与商户时段对比,仅当商户时段被完全覆盖时 full=true。
|
|
|
+ */
|
|
|
+ private Map<String, Object> getDayBookingStatus(Integer storeId, Date reservationDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ if (storeId == null) {
|
|
|
+ result.put("date", null);
|
|
|
+ result.put("reservations", null);
|
|
|
+ result.put("tableStatusList", null);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(reservationDate);
|
|
|
+ cal.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
+ cal.set(Calendar.MINUTE, 0);
|
|
|
+ cal.set(Calendar.SECOND, 0);
|
|
|
+ cal.set(Calendar.MILLISECOND, 0);
|
|
|
+ Date dayStart = cal.getTime();
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ Date dayEnd = cal.getTime();
|
|
|
+
|
|
|
+ List<StoreBookingTable> storeTables = storeBookingTableService.getTableList(storeId, null);
|
|
|
+ int[] range = getBookingRangeMinutes(storeId);
|
|
|
+ int bookingStartMin = range[0];
|
|
|
+ int bookingEndMin = range[1];
|
|
|
+
|
|
|
+ LambdaQueryWrapper<UserReservation> dayResWrapper = new LambdaQueryWrapper<>();
|
|
|
+ dayResWrapper.eq(UserReservation::getStoreId, storeId)
|
|
|
+ .ne(UserReservation::getStatus, STATUS_CANCELLED)
|
|
|
+ .ge(UserReservation::getReservationDate, dayStart)
|
|
|
+ .lt(UserReservation::getReservationDate, dayEnd);
|
|
|
+ List<UserReservation> dayReservations = this.list(dayResWrapper);
|
|
|
+ List<Integer> dayReservationIds = dayReservations.stream().map(UserReservation::getId).collect(Collectors.toList());
|
|
|
+ List<UserReservationTable> dayTableLinks = new ArrayList<>();
|
|
|
+ if (!dayReservationIds.isEmpty()) {
|
|
|
+ LambdaQueryWrapper<UserReservationTable> rtWrapper = new LambdaQueryWrapper<>();
|
|
|
+ rtWrapper.in(UserReservationTable::getReservationId, dayReservationIds);
|
|
|
+ dayTableLinks = userReservationTableMapper.selectList(rtWrapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Map<String, Object>> tableStatusList = buildTableStatusListForDay(storeId, dayStart, dayEnd, storeTables, dayReservations, dayTableLinks, bookingStartMin, bookingEndMin);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<UserReservation> listWrapper = new LambdaQueryWrapper<>();
|
|
|
+ listWrapper.eq(UserReservation::getStoreId, storeId)
|
|
|
+ .ge(UserReservation::getReservationDate, dayStart)
|
|
|
+ .lt(UserReservation::getReservationDate, dayEnd)
|
|
|
+ .orderByAsc(UserReservation::getReservationDate)
|
|
|
+ .orderByAsc(UserReservation::getStartTime);
|
|
|
+ List<UserReservation> reservations = this.list(listWrapper);
|
|
|
+ List<UserReservationVo> voList = reservations.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
|
|
|
+ result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(dayStart));
|
|
|
+ result.put("reservations", voList);
|
|
|
+ result.put("tableStatusList", tableStatusList);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从今天起查找第一个“存在未约满选座”的日期,并返回该日的预约数据及每个选座(桌)的约满状态。
|
|
|
+ * 以 store_booking_settings 的 booking_start_time、booking_end_time 为当日可预订时段,结合 user_reservation(reservation_date、start_time、end_time)与 user_reservation_table(table_id)找出该日每桌所有预约,将用户预约时间段拼接合并后与商户时段对比:仅当商户可预订时段被完全覆盖时该桌 full=true,否则 full=false。
|
|
|
*
|
|
|
* @param storeId 门店ID
|
|
|
- * @return Map:date 为 yyyy-MM-dd 格式的日期,reservations 为该日的预约 VO 列表;未配置或 storeId 为空时 date 可能为 null、reservations 为空列表
|
|
|
+ * @return Map:date 日期(yyyy-MM-dd),reservations 该日预约列表,tableStatusList 该日每个选座的约满标识(full)及桌信息
|
|
|
*/
|
|
|
@Override
|
|
|
public Map<String, Object> findFirstAvailableDayReservations(Integer storeId) {
|
|
|
Map<String, Object> result = new HashMap<>();
|
|
|
if (storeId == null) {
|
|
|
result.put("date", null);
|
|
|
-// result.put("reservations", List.of());
|
|
|
+ result.put("reservations", null);
|
|
|
+ result.put("tableStatusList", null);
|
|
|
return result;
|
|
|
}
|
|
|
- // 取门店预订设置中的单时段最大容纳人数,未配置或为 0 则视为不设上限
|
|
|
- int maxCapacity = Integer.MAX_VALUE;
|
|
|
- List<StoreBookingSettings> settingsList = storeBookingSettingsService.list(
|
|
|
- new LambdaQueryWrapper<StoreBookingSettings>().eq(StoreBookingSettings::getStoreId, storeId));
|
|
|
- if (!settingsList.isEmpty() && settingsList.get(0).getMaxCapacityPerSlot() != null
|
|
|
- && settingsList.get(0).getMaxCapacityPerSlot() > 0) {
|
|
|
- maxCapacity = settingsList.get(0).getMaxCapacityPerSlot();
|
|
|
- }
|
|
|
- // 从今天 00:00:00 开始,按天往后检查
|
|
|
+ // 门店下所有选座(桌),用于按“选座”维度计算约满
|
|
|
+ List<StoreBookingTable> storeTables = storeBookingTableService.getTableList(storeId, null);
|
|
|
+// if (storeTables == null) {
|
|
|
+// storeTables = List.of();
|
|
|
+// }
|
|
|
+ // 商户可预订时段(分钟),用于与用户预约时间段对比判断是否约满
|
|
|
+ int[] range = getBookingRangeMinutes(storeId);
|
|
|
+ int bookingStartMin = range[0];
|
|
|
+ int bookingEndMin = range[1];
|
|
|
+ // 从今天 00:00:00 开始,按天往后检查;以“日期”为一个集合
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
cal.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
cal.set(Calendar.MINUTE, 0);
|
|
|
@@ -193,18 +522,25 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
Date dayStart = cal.getTime();
|
|
|
cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
Date dayEnd = cal.getTime();
|
|
|
- // 统计该日该门店下非取消状态的预约总人数
|
|
|
- LambdaQueryWrapper<UserReservation> countWrapper = new LambdaQueryWrapper<>();
|
|
|
- countWrapper.eq(UserReservation::getStoreId, storeId)
|
|
|
+ // 当日、该门店、非取消的预约
|
|
|
+ LambdaQueryWrapper<UserReservation> dayResWrapper = new LambdaQueryWrapper<>();
|
|
|
+ dayResWrapper.eq(UserReservation::getStoreId, storeId)
|
|
|
.ne(UserReservation::getStatus, STATUS_CANCELLED)
|
|
|
.ge(UserReservation::getReservationDate, dayStart)
|
|
|
.lt(UserReservation::getReservationDate, dayEnd);
|
|
|
- List<UserReservation> dayList = this.list(countWrapper);
|
|
|
- int totalGuests = dayList.stream()
|
|
|
- .mapToInt(r -> r.getGuestCount() == null ? 0 : r.getGuestCount())
|
|
|
- .sum();
|
|
|
- // 未约满:总人数小于上限,返回该日及该日全部预约数据
|
|
|
- if (totalGuests < maxCapacity) {
|
|
|
+ List<UserReservation> dayReservations = this.list(dayResWrapper);
|
|
|
+ List<Integer> dayReservationIds = dayReservations.stream().map(UserReservation::getId).collect(Collectors.toList());
|
|
|
+ List<UserReservationTable> dayTableLinks = new ArrayList<>();
|
|
|
+ if (!dayReservationIds.isEmpty()) {
|
|
|
+ LambdaQueryWrapper<UserReservationTable> rtWrapper = new LambdaQueryWrapper<>();
|
|
|
+ rtWrapper.in(UserReservationTable::getReservationId, dayReservationIds);
|
|
|
+ dayTableLinks = userReservationTableMapper.selectList(rtWrapper);
|
|
|
+ }
|
|
|
+ // 按商户 booking_start_time/booking_end_time 与每桌当日预约时间段拼接对比,仅当全部约满时 full=true
|
|
|
+ List<Map<String, Object>> tableStatusList = buildTableStatusListForDay(storeId, dayStart, dayEnd, storeTables, dayReservations, dayTableLinks, bookingStartMin, bookingEndMin);
|
|
|
+ // 若存在至少一个选座未约满,则返回该日数据
|
|
|
+ boolean hasAvailable = tableStatusList.stream().anyMatch(m -> !Boolean.TRUE.equals(m.get("full")));
|
|
|
+ if (hasAvailable) {
|
|
|
LambdaQueryWrapper<UserReservation> listWrapper = new LambdaQueryWrapper<>();
|
|
|
listWrapper.eq(UserReservation::getStoreId, storeId)
|
|
|
.ge(UserReservation::getReservationDate, dayStart)
|
|
|
@@ -215,13 +551,24 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
List<UserReservationVo> voList = reservations.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
|
|
|
result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(dayStart));
|
|
|
result.put("reservations", voList);
|
|
|
+ result.put("tableStatusList", tableStatusList);
|
|
|
return result;
|
|
|
}
|
|
|
}
|
|
|
- // 连续 MAX_DAYS_TO_CHECK 天都约满时,返回最后检查的日期,预约列表为空
|
|
|
+ // 连续 MAX_DAYS_TO_CHECK 天所有选座均已约满时,返回最后一天的日期及该日选座状态(全部 full=true),预约列表为空
|
|
|
cal.add(Calendar.DAY_OF_MONTH, -1);
|
|
|
- result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
|
|
|
-// result.put("reservations", List.of());
|
|
|
+ Date lastDayStart = cal.getTime();
|
|
|
+ result.put("date", new SimpleDateFormat("yyyy-MM-dd").format(lastDayStart));
|
|
|
+ result.put("reservations", null);
|
|
|
+ List<Map<String, Object>> allFullList = storeTables.stream().map(t -> {
|
|
|
+ Map<String, Object> row = new HashMap<>();
|
|
|
+ row.put("tableId", t.getId());
|
|
|
+ row.put("tableNumber", t.getTableNumber());
|
|
|
+ row.put("seatingCapacity", t.getSeatingCapacity());
|
|
|
+ row.put("full", true);
|
|
|
+ return row;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ result.put("tableStatusList", allFullList);
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
@@ -510,6 +857,73 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void setReservationOrderPaymentTimeoutByOrderId(Integer orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(orderId);
|
|
|
+ if (order == null || order.getOrderSn() == null) {
|
|
|
+ log.warn("预订订单不存在或无订单编号,orderId={}", orderId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ReservationOrderDetailVo getOrderDetailByOrderId(Integer orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ ReservationOrderDetailVo vo = new ReservationOrderDetailVo();
|
|
|
+ vo.setOrder(order);
|
|
|
+
|
|
|
+ if (order.getStoreId() != null) {
|
|
|
+ vo.setStoreInfo(storeInfoService.getById(order.getStoreId()));
|
|
|
+ vo.setStoreBookingSettings(storeBookingSettingsService.getByStoreId(order.getStoreId()));
|
|
|
+ }
|
|
|
+ if (order.getReservationId() != null) {
|
|
|
+ vo.setReservation(getDetail(order.getReservationId()));
|
|
|
+ vo.setTableList(buildBookingTableList(order.getReservationId()));
|
|
|
+ } else {
|
|
|
+ vo.setTableList(Collections.emptyList());
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<BookingTableItemVo> buildBookingTableList(Integer reservationId) {
|
|
|
+ if (reservationId == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<Integer> tableIds = listTableIdsByReservationId(reservationId);
|
|
|
+ if (tableIds == null || tableIds.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<BookingTableItemVo> list = new ArrayList<>();
|
|
|
+ for (Integer tableId : tableIds) {
|
|
|
+ StoreBookingTable table = storeBookingTableService.getById(tableId);
|
|
|
+ if (table == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ BookingTableItemVo item = new BookingTableItemVo();
|
|
|
+ item.setTableId(table.getId());
|
|
|
+ item.setTableNumber(table.getTableNumber());
|
|
|
+ item.setSeatingCapacity(table.getSeatingCapacity());
|
|
|
+ if (table.getCategoryId() != null) {
|
|
|
+ StoreBookingCategory cat = storeBookingCategoryService.getById(table.getCategoryId());
|
|
|
+ if (cat != null) {
|
|
|
+ item.setCategoryName(cat.getCategoryName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ list.add(item);
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
private static String generateReservationNo() {
|
|
|
return "RV" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
|
|
|
}
|