|
|
@@ -0,0 +1,1193 @@
|
|
|
+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;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.context.annotation.Lazy;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import shop.alien.entity.store.*;
|
|
|
+import shop.alien.entity.store.dto.UserReservationDTO;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingBusinessHoursVo;
|
|
|
+import shop.alien.entity.store.vo.StoreMainInfoVo;
|
|
|
+import shop.alien.entity.store.vo.StoreReservationListVo;
|
|
|
+import shop.alien.entity.store.vo.UserReservationVo;
|
|
|
+import shop.alien.mapper.EssentialHolidayComparisonMapper;
|
|
|
+import shop.alien.mapper.UserReservationOrderMapper;
|
|
|
+import shop.alien.store.vo.BookingTableItemVo;
|
|
|
+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.service.*;
|
|
|
+import shop.alien.util.common.UniqueRandomNumGenerator;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.text.ParseException;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.ThreadLocalRandom;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 用户预约 服务实现类
|
|
|
+ *
|
|
|
+ * @author system
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class UserReservationServiceImpl extends ServiceImpl<UserReservationMapper, UserReservation> implements UserReservationService {
|
|
|
+
|
|
|
+ private final UserReservationTableMapper userReservationTableMapper;
|
|
|
+
|
|
|
+ private final StoreBookingSettingsService storeBookingSettingsService;
|
|
|
+
|
|
|
+ private final StoreBookingTableService storeBookingTableService;
|
|
|
+
|
|
|
+ private final StoreBookingCategoryService storeBookingCategoryService;
|
|
|
+
|
|
|
+ private final StoreInfoService storeInfoService;
|
|
|
+
|
|
|
+ private final UserReservationOrderService userReservationOrderService;
|
|
|
+
|
|
|
+ private final ReservationOrderPaymentTimeoutService reservationOrderPaymentTimeoutService;
|
|
|
+
|
|
|
+ private final UserReservationOrderMapper userReservationOrderMapper;
|
|
|
+
|
|
|
+ private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
|
|
|
+
|
|
|
+ private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
|
|
|
+
|
|
|
+ private ReservationOrderPageService reservationOrderPageService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ public void setReservationOrderPageService(@Lazy ReservationOrderPageService reservationOrderPageService) {
|
|
|
+ this.reservationOrderPageService = reservationOrderPageService;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 预约状态:待确认 */
|
|
|
+ private static final int STATUS_PENDING = 0;
|
|
|
+ /** 预约状态:已确认(待使用) */
|
|
|
+ private static final int STATUS_CONFIRMED = 1;
|
|
|
+ /** 预约状态:已取消(不参与约满统计与展示) */
|
|
|
+ private static final int STATUS_CANCELLED = 3;
|
|
|
+ /** 预约状态:未到店超时 */
|
|
|
+ private static final int STATUS_NO_SHOW_TIMEOUT = 4;
|
|
|
+ /** 订单状态:待使用 */
|
|
|
+ private static final int ORDER_STATUS_TO_USE = 1;
|
|
|
+ /** 订单状态:已过期 */
|
|
|
+ private static final int ORDER_STATUS_EXPIRED = 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) {
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
+ throw new RuntimeException("用户ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getStoreId() == null) {
|
|
|
+ throw new RuntimeException("门店ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getReservationDate() == null) {
|
|
|
+ throw new RuntimeException("预约日期不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getGuestCount() == null || dto.getGuestCount() < 1) {
|
|
|
+ throw new RuntimeException("预约人数至少为1");
|
|
|
+ }
|
|
|
+
|
|
|
+ UserReservation entity = new UserReservation();
|
|
|
+ BeanUtils.copyProperties(dto, entity, "id", "tableIds");
|
|
|
+ entity.setId(null);
|
|
|
+ entity.setReservationNo(generateReservationNo());
|
|
|
+ if (entity.getStatus() == null) {
|
|
|
+ entity.setStatus(STATUS_PENDING);
|
|
|
+ }
|
|
|
+ this.save(entity);
|
|
|
+
|
|
|
+ saveReservationTables(entity.getId(), dto.getTableIds());
|
|
|
+
|
|
|
+ // 同步创建预订订单(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);
|
|
|
+ }
|
|
|
+ // 免费预订:对应预约状态设为已确认(1),与订单「待使用」一致
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
|
|
|
+ entity.setStatus(STATUS_CONFIRMED);
|
|
|
+ this.updateById(entity);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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());
|
|
|
+ // 当store_booking_settings.reservation = 1时,订单状态为待使用(1),不为1时,订单状态为待支付(0)
|
|
|
+ order.setOrderStatus(
|
|
|
+ storeBookingSettingsService.getByStoreId(reservation.getStoreId()) != null && "1".equals(
|
|
|
+ storeBookingSettingsService.getByStoreId(reservation.getStoreId()).getReservation()) ? 0 : 1);
|
|
|
+ order.setIsMerchantReservation(0);
|
|
|
+ fillOrderFromStoreSettings(order, reservation);
|
|
|
+ // 免费预订:生成核销码,不生成核销二维码 URL(付费订单在支付成功时再生成)
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
|
|
|
+ order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
|
|
|
+ }
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按门店预订配置填充订单的收费/订金/取消政策等字段(与 add 一致)
|
|
|
+ */
|
|
|
+ private void fillOrderFromStoreSettings(UserReservationOrder order, UserReservation reservation) {
|
|
|
+ if (reservation == null || reservation.getStoreId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean updateReservation(UserReservationDTO dto) {
|
|
|
+ if (dto.getId() == null) {
|
|
|
+ throw new RuntimeException("预约ID不能为空");
|
|
|
+ }
|
|
|
+ // 根据订单id获取预约信息
|
|
|
+ UserReservationOrder userReservationOrder = userReservationOrderMapper.selectById(dto.getId());
|
|
|
+ UserReservation existing = this.getById(userReservationOrder.getReservationId());
|
|
|
+ if (existing == null) {
|
|
|
+ throw new RuntimeException("预约不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ UserReservation entity = new UserReservation();
|
|
|
+ BeanUtils.copyProperties(dto, entity, "tableIds", "reservationNo");
|
|
|
+ entity.setId(existing.getId());
|
|
|
+ entity.setReservationNo(existing.getReservationNo());
|
|
|
+ this.updateById(entity);
|
|
|
+
|
|
|
+ saveReservationTables(existing.getId(), dto.getTableIds());
|
|
|
+
|
|
|
+ // 与 add 一致:同步 user_reservation_order(无则创建,有则仅待支付时按门店配置刷新)
|
|
|
+ UserReservation updatedReservation = this.getById(existing.getId());
|
|
|
+ LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
+ orderWrapper.eq(UserReservationOrder::getReservationId, existing.getId()).last("LIMIT 1");
|
|
|
+ UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
|
|
|
+ if (order == null) {
|
|
|
+ order = buildReservationOrder(updatedReservation);
|
|
|
+ userReservationOrderService.save(order);
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
|
|
|
+ reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
|
|
|
+ }
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
|
|
|
+ this.update(new LambdaUpdateWrapper<UserReservation>()
|
|
|
+ .eq(UserReservation::getId, existing.getId())
|
|
|
+ .set(UserReservation::getStatus, STATUS_CONFIRMED));
|
|
|
+ }
|
|
|
+ } else if (order.getOrderStatus() != null && order.getOrderStatus() == 0) {
|
|
|
+ fillOrderFromStoreSettings(order, updatedReservation);
|
|
|
+ userReservationOrderService.updateById(order);
|
|
|
+ if (order.getOrderCostType() != null && order.getOrderCostType() == 1 && order.getOrderSn() != null) {
|
|
|
+ reservationOrderPaymentTimeoutService.setReservationOrderPaymentTimeout(order.getOrderSn(), 15 * 60);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean removeReservation(Integer id) {
|
|
|
+
|
|
|
+ UserReservationOrder one = userReservationOrderService.getOne(new LambdaUpdateWrapper<UserReservationOrder>()
|
|
|
+ .eq(UserReservationOrder::getId, id));
|
|
|
+ if (one == null) {
|
|
|
+ throw new RuntimeException("预约不存在");
|
|
|
+ }
|
|
|
+ // 订单状态置为已取消(4)
|
|
|
+ userReservationOrderService.update(
|
|
|
+ new LambdaUpdateWrapper<UserReservationOrder>()
|
|
|
+ .eq(UserReservationOrder::getId, one.getId())
|
|
|
+ .set(UserReservationOrder::getOrderStatus, 4));
|
|
|
+ // 预约不再逻辑删除,仅将 status 置为已取消(3)
|
|
|
+ return this.update(new LambdaUpdateWrapper<UserReservation>()
|
|
|
+ .eq(UserReservation::getId, one.getReservationId())
|
|
|
+ .set(UserReservation::getStatus, STATUS_CANCELLED));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean deleteOrderAndReservationByOrderId(Integer orderId) {
|
|
|
+ if (orderId == null) {
|
|
|
+ throw new RuntimeException("预订订单ID不能为空");
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ throw new RuntimeException("预订订单不存在");
|
|
|
+ }
|
|
|
+ Integer reservationId = order.getReservationId();
|
|
|
+ // 1. 逻辑删除订单
|
|
|
+ boolean orderDeleted = userReservationOrderService.removeById(orderId);
|
|
|
+ if (!orderDeleted) {
|
|
|
+ throw new RuntimeException("删除订单失败");
|
|
|
+ }
|
|
|
+ if (reservationId == null) {
|
|
|
+ log.info("预定订单删除成功(无关联预约),orderId={}", orderId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 2. 逻辑删除预约与桌号关联
|
|
|
+ UserReservationTable tableFlag = new UserReservationTable();
|
|
|
+ tableFlag.setDeleteFlag(1);
|
|
|
+ userReservationTableMapper.update(tableFlag, new LambdaUpdateWrapper<UserReservationTable>()
|
|
|
+ .eq(UserReservationTable::getReservationId, reservationId));
|
|
|
+ // 3. 逻辑删除预约
|
|
|
+ boolean reservationDeleted = this.removeById(reservationId);
|
|
|
+ if (!reservationDeleted) {
|
|
|
+ throw new RuntimeException("删除预约信息失败");
|
|
|
+ }
|
|
|
+ log.info("预定订单删除成功(逻辑删除订单及预定信息),orderId={}, reservationId={}", orderId, reservationId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public UserReservationVo getDetail(Integer id) {
|
|
|
+ UserReservation one = this.getById(id);
|
|
|
+ if (one == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ UserReservationVo vo = new UserReservationVo();
|
|
|
+ BeanUtils.copyProperties(one, vo);
|
|
|
+ vo.setTableIds(listTableIdsByReservationId(id));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @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 (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
|
|
|
+ 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) {
|
|
|
+ int current = pageNum == null || pageNum <= 0 ? 1 : pageNum;
|
|
|
+ int size = pageSize == null || pageSize <= 0 ? 10 : pageSize;
|
|
|
+ Page<UserReservation> page = new Page<>(current, size);
|
|
|
+ LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, dateFrom, dateTo);
|
|
|
+ wrapper.orderByDesc(UserReservation::getCreatedTime);
|
|
|
+ IPage<UserReservation> entityPage = this.page(page, wrapper);
|
|
|
+ return entityPage.convert(this::toVoWithTableIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<UserReservationVo> list(Integer userId, Integer storeId, Integer status) {
|
|
|
+ LambdaQueryWrapper<UserReservation> wrapper = buildListWrapper(userId, storeId, status, null, null);
|
|
|
+ wrapper.orderByDesc(UserReservation::getCreatedTime);
|
|
|
+ List<UserReservation> list = this.list(wrapper);
|
|
|
+ return list.stream().map(this::toVoWithTableIds).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getBookingsByStoreId(Integer storeId) {
|
|
|
+ Map<String, Object> list = new HashMap<>();
|
|
|
+ //通过storeId查询商家预设的预订信息状态
|
|
|
+ LambdaQueryWrapper<StoreBookingSettings> eq = new LambdaQueryWrapper<StoreBookingSettings>().eq(StoreBookingSettings::getStoreId, storeId);
|
|
|
+ List<StoreBookingSettings> storeBookingSettings = storeBookingSettingsService.list(eq);
|
|
|
+ list.put("storeBookingSettings", storeBookingSettings);
|
|
|
+
|
|
|
+ // 查询营业时间:如果有设置信息,则查询对应的营业时间
|
|
|
+ // 特殊营业时间通过 essential_id 关联 essential_holiday_comparison 节假日表
|
|
|
+ List<StoreBookingBusinessHoursVo> storeBookingBusinessHours = new ArrayList<>();
|
|
|
+ if (storeBookingSettings != null && !storeBookingSettings.isEmpty()) {
|
|
|
+ // 获取第一个设置的ID(通常一个门店只有一个设置)
|
|
|
+ StoreBookingSettings settings = storeBookingSettings.get(0);
|
|
|
+ if (settings != null && settings.getId() != null) {
|
|
|
+ List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
|
|
|
+ // 转换为 Vo 并关联节假日信息
|
|
|
+ for (StoreBookingBusinessHours businessHours : businessHoursList) {
|
|
|
+ StoreBookingBusinessHoursVo vo = new StoreBookingBusinessHoursVo();
|
|
|
+ BeanUtils.copyProperties(businessHours, vo);
|
|
|
+
|
|
|
+ // 如果是特殊营业时间(business_type = 2)且有关联的节假日ID,查询节假日信息
|
|
|
+ if (businessHours.getBusinessType() != null && businessHours.getBusinessType() == 2
|
|
|
+ && businessHours.getEssentialId() != null) {
|
|
|
+ try {
|
|
|
+ EssentialHolidayComparison holiday = essentialHolidayComparisonMapper.selectById(businessHours.getEssentialId());
|
|
|
+ if (holiday != null) {
|
|
|
+ vo.setHolidayInfo(holiday);
|
|
|
+ // 从节假日信息中获取日期作为 businessDate
|
|
|
+ if (holiday.getFestivalDate() != null) {
|
|
|
+ vo.setBusinessDate(holiday.getFestivalDate().toString());
|
|
|
+ } else if (holiday.getStartTime() != null) {
|
|
|
+ // 如果 festivalDate 为空,使用 startTime
|
|
|
+ java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ vo.setBusinessDate(sdf.format(holiday.getStartTime()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("查询节假日信息失败,essentialId={}", businessHours.getEssentialId(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置 businessDate:优先使用 holidayDate,如果没有则使用 holidayInfo 中的日期
|
|
|
+ if (vo.getBusinessDate() == null && businessHours.getHolidayDate() != null) {
|
|
|
+ vo.setBusinessDate(businessHours.getHolidayDate());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 holidayType 的值也放到 businessDate 中
|
|
|
+ if (businessHours.getHolidayType() != null && !businessHours.getHolidayType().trim().isEmpty()) {
|
|
|
+ vo.setBusinessDate(businessHours.getHolidayType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 businessDate 的值也设置到 holidayType(用于回显)
|
|
|
+ if (vo.getBusinessDate() != null) {
|
|
|
+ vo.setHolidayType(vo.getBusinessDate());
|
|
|
+ }
|
|
|
+
|
|
|
+ storeBookingBusinessHours.add(vo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ list.put("storeBookingBusinessHours", storeBookingBusinessHours);
|
|
|
+
|
|
|
+ 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.put("storeBookingCategorys", storeBookingCategorys);
|
|
|
+ StoreMainInfoVo storeInfo = storeInfoService.getStoreInfo(storeId);
|
|
|
+ list.put("storeInfo", storeInfo);
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按预定日期查询指定日期的定桌情况。传入 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 该日预约列表,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", null);
|
|
|
+ result.put("tableStatusList", null);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ // 门店下所有选座(桌),用于按“选座”维度计算约满
|
|
|
+ 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);
|
|
|
+ cal.set(Calendar.SECOND, 0);
|
|
|
+ cal.set(Calendar.MILLISECOND, 0);
|
|
|
+ for (int i = 0; i < MAX_DAYS_TO_CHECK; i++) {
|
|
|
+ Date dayStart = cal.getTime();
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ Date dayEnd = cal.getTime();
|
|
|
+ // 当日、该门店、非取消的预约
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ // 按商户 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)
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 连续 MAX_DAYS_TO_CHECK 天所有选座均已约满时,返回最后一天的日期及该日选座状态(全部 full=true),预约列表为空
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, -1);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ private LambdaQueryWrapper<UserReservation> buildListWrapper(Integer userId, Integer storeId, Integer status,
|
|
|
+ Date dateFrom, Date dateTo) {
|
|
|
+ LambdaQueryWrapper<UserReservation> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ if (userId != null) {
|
|
|
+ wrapper.eq(UserReservation::getUserId, userId);
|
|
|
+ }
|
|
|
+ if (storeId != null) {
|
|
|
+ wrapper.eq(UserReservation::getStoreId, storeId);
|
|
|
+ }
|
|
|
+ if (status != null) {
|
|
|
+ wrapper.eq(UserReservation::getStatus, status);
|
|
|
+ }
|
|
|
+ if (dateFrom != null) {
|
|
|
+ wrapper.ge(UserReservation::getReservationDate, dateFrom);
|
|
|
+ }
|
|
|
+ if (dateTo != null) {
|
|
|
+ wrapper.le(UserReservation::getReservationDate, dateTo);
|
|
|
+ }
|
|
|
+ return wrapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserReservationVo toVoWithTableIds(UserReservation entity) {
|
|
|
+ UserReservationVo vo = new UserReservationVo();
|
|
|
+ BeanUtils.copyProperties(entity, vo);
|
|
|
+ vo.setTableIds(listTableIdsByReservationId(entity.getId()));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Integer> listTableIdsByReservationId(Integer reservationId) {
|
|
|
+ LambdaQueryWrapper<UserReservationTable> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(UserReservationTable::getReservationId, reservationId)
|
|
|
+ .orderByAsc(UserReservationTable::getSort)
|
|
|
+ .orderByAsc(UserReservationTable::getId);
|
|
|
+ List<UserReservationTable> list = userReservationTableMapper.selectList(wrapper);
|
|
|
+ return list.stream().map(UserReservationTable::getTableId).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存预约与桌号关联:先物理删除该预约下原有关联再插入
|
|
|
+ */
|
|
|
+ private void saveReservationTables(Integer reservationId, List<Integer> tableIds) {
|
|
|
+ userReservationTableMapper.physicalDeleteByReservationId(reservationId);
|
|
|
+ if (tableIds != null && !tableIds.isEmpty()) {
|
|
|
+ int sort = 0;
|
|
|
+ for (Integer tableId : tableIds) {
|
|
|
+ UserReservationTable rt = new UserReservationTable();
|
|
|
+ rt.setReservationId(reservationId);
|
|
|
+ rt.setTableId(tableId);
|
|
|
+ rt.setSort(sort++);
|
|
|
+ userReservationTableMapper.insert(rt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<StoreReservationListVo> getStoreReservationList(Integer storeId, Integer status, Date dateFrom, Date dateTo, Integer orderStatus) {
|
|
|
+ log.info("UserReservationServiceImpl.getStoreReservationList?storeId={}, status={}, dateFrom={}, dateTo={}, orderStatus={}",
|
|
|
+ storeId, status, dateFrom, dateTo, orderStatus);
|
|
|
+
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new RuntimeException("门店ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ return baseMapper.getStoreReservationList(storeId, status, dateFrom, dateTo, orderStatus);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean cancelReservationByStore(Integer reservationId) {
|
|
|
+ log.info("UserReservationServiceImpl.cancelReservationByStore?reservationId={}", reservationId);
|
|
|
+
|
|
|
+ if (reservationId == null) {
|
|
|
+ throw new RuntimeException("预约ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询预约信息
|
|
|
+ UserReservation reservation = this.getById(reservationId);
|
|
|
+ if (reservation == null) {
|
|
|
+ throw new RuntimeException("预约不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查预约状态,已取消的不能再次取消
|
|
|
+ if (reservation.getStatus() != null && reservation.getStatus() == STATUS_CANCELLED) {
|
|
|
+ throw new RuntimeException("预约已取消,不能重复取消");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询关联的订单信息
|
|
|
+ LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
+ orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
|
|
|
+ UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
|
|
|
+
|
|
|
+ if (order == null) {
|
|
|
+ // 如果没有订单,直接更新预约状态为3(已取消)
|
|
|
+ reservation.setStatus(STATUS_CANCELLED); // STATUS_CANCELLED = 3
|
|
|
+ boolean updateResult = this.updateById(reservation);
|
|
|
+ if (!updateResult) {
|
|
|
+ throw new RuntimeException("更新预约状态失败");
|
|
|
+ }
|
|
|
+ log.info("商家端取消预约成功(无订单),reservationId={}", reservationId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断订单费用类型:0-免费, 1-收费
|
|
|
+ Integer orderCostType = order.getOrderCostType();
|
|
|
+ if (orderCostType == null) {
|
|
|
+ // 如果订单费用类型为空,默认按免费处理
|
|
|
+ orderCostType = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (orderCostType == 0) {
|
|
|
+ // 免费订单:更新订单状态为4(已取消),更新预约状态为3(已取消)
|
|
|
+ order.setOrderStatus(4); // 4:已取消
|
|
|
+ boolean orderUpdateResult = userReservationOrderService.updateById(order);
|
|
|
+ if (!orderUpdateResult) {
|
|
|
+ throw new RuntimeException("更新订单状态失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ reservation.setStatus(STATUS_CANCELLED); // STATUS_CANCELLED = 3:已取消
|
|
|
+ boolean reservationUpdateResult = this.updateById(reservation);
|
|
|
+ if (!reservationUpdateResult) {
|
|
|
+ throw new RuntimeException("更新预约状态失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("商家端取消预约成功(免费订单),reservationId={}, orderId={}", reservationId, order.getId());
|
|
|
+ return true;
|
|
|
+ } else if (orderCostType == 1) {
|
|
|
+ // 付费订单:功能预留(暂不更新状态,等待后续实现退款逻辑)
|
|
|
+ throw new RuntimeException("付费订单取消功能暂未实现,请稍后再试");
|
|
|
+ } else {
|
|
|
+ throw new RuntimeException("订单费用类型异常,orderCostType=" + orderCostType);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean deleteReservationByStore(Integer reservationId) {
|
|
|
+ log.info("UserReservationServiceImpl.deleteReservationByStore?reservationId={}", reservationId);
|
|
|
+
|
|
|
+ if (reservationId == null) {
|
|
|
+ throw new RuntimeException("预约ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询预约信息
|
|
|
+ UserReservation reservation = this.getById(reservationId);
|
|
|
+ if (reservation == null) {
|
|
|
+ throw new RuntimeException("预约不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询关联的订单信息
|
|
|
+ LambdaQueryWrapper<UserReservationOrder> orderWrapper = new LambdaQueryWrapper<>();
|
|
|
+ orderWrapper.eq(UserReservationOrder::getReservationId, reservationId);
|
|
|
+ UserReservationOrder order = userReservationOrderService.getOne(orderWrapper);
|
|
|
+
|
|
|
+ if (order == null) {
|
|
|
+ // 如果没有订单,直接删除预约记录
|
|
|
+ boolean deleteResult = this.removeById(reservationId);
|
|
|
+ if (!deleteResult) {
|
|
|
+ throw new RuntimeException("删除预约记录失败");
|
|
|
+ }
|
|
|
+ log.info("商家端删除预订信息成功(无订单),reservationId={}", reservationId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断订单状态:只有已取消(4)、已退款(7)、已完成(2)状态才能删除
|
|
|
+ Integer orderStatus = order.getOrderStatus();
|
|
|
+ if (orderStatus == null) {
|
|
|
+ throw new RuntimeException("订单状态异常,无法删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 定义可删除的订单状态:2:已完成, 4:已取消, 7:已退款
|
|
|
+ boolean canDelete = orderStatus == 2 || orderStatus == 4 || orderStatus == 7;
|
|
|
+
|
|
|
+ if (!canDelete) {
|
|
|
+ String statusText = getOrderStatusText(orderStatus);
|
|
|
+ throw new RuntimeException("订单状态为" + statusText + ",不允许删除。只有已取消、已退款、已完成状态的订单可以删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除订单记录(逻辑删除)
|
|
|
+ boolean orderDeleteResult = userReservationOrderService.removeById(order.getId());
|
|
|
+ if (!orderDeleteResult) {
|
|
|
+ throw new RuntimeException("删除订单记录失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除预约记录(逻辑删除)
|
|
|
+ boolean reservationDeleteResult = this.removeById(reservationId);
|
|
|
+ if (!reservationDeleteResult) {
|
|
|
+ throw new RuntimeException("删除预约记录失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("商家端删除预订信息成功,reservationId={}, orderId={}, orderStatus={}",
|
|
|
+ reservationId, order.getId(), orderStatus);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取订单状态文本
|
|
|
+ */
|
|
|
+ private String getOrderStatusText(Integer orderStatus) {
|
|
|
+ if (orderStatus == null) {
|
|
|
+ return "未知";
|
|
|
+ }
|
|
|
+ switch (orderStatus) {
|
|
|
+ case 0: return "待支付";
|
|
|
+ case 1: return "待使用";
|
|
|
+ case 2: return "已完成";
|
|
|
+ case 3: return "已过期";
|
|
|
+ case 4: return "已取消";
|
|
|
+ case 5: return "已关闭";
|
|
|
+ case 6: return "退款中";
|
|
|
+ case 7: return "已退款";
|
|
|
+ case 8: return "商家预订";
|
|
|
+ default: return "未知";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean addTimeByStore(Integer reservationId, String addTimeStart, Integer addTimeMinutes) {
|
|
|
+ log.info("UserReservationServiceImpl.addTimeByStore?reservationId={}, addTimeStart={}, addTimeMinutes={}",
|
|
|
+ reservationId, addTimeStart, addTimeMinutes);
|
|
|
+
|
|
|
+ if (reservationId == null) {
|
|
|
+ throw new RuntimeException("预约ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (addTimeStart == null || addTimeStart.trim().isEmpty()) {
|
|
|
+ throw new RuntimeException("加时开始时间不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (addTimeMinutes == null || addTimeMinutes <= 0) {
|
|
|
+ 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格式");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询预约信息
|
|
|
+ UserReservation reservation = this.getById(reservationId);
|
|
|
+ if (reservation == null) {
|
|
|
+ throw new RuntimeException("预约不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存原结束时间用于日志
|
|
|
+ String oldEndTime = reservation.getEndTime();
|
|
|
+
|
|
|
+ // 计算新的结束时间:加时开始时间 + 加时分钟数
|
|
|
+ String newEndTime = calculateNewEndTime(addTimeStart, addTimeMinutes);
|
|
|
+
|
|
|
+ // 更新预约结束时间
|
|
|
+ reservation.setEndTime(newEndTime);
|
|
|
+ boolean updateResult = this.updateById(reservation);
|
|
|
+ if (!updateResult) {
|
|
|
+ throw new RuntimeException("更新预约结束时间失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("商家端加时成功,reservationId={}, 原结束时间={}, 加时开始时间={}, 加时分钟数={}, 新结束时间={}",
|
|
|
+ reservationId, oldEndTime, addTimeStart, addTimeMinutes, newEndTime);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算新的结束时间:加时开始时间 + 加时分钟数
|
|
|
+ *
|
|
|
+ * @param addTimeStart 加时开始时间(HH:mm格式)
|
|
|
+ * @param addTimeMinutes 加时分钟数
|
|
|
+ * @return 新的结束时间(HH:mm格式)
|
|
|
+ */
|
|
|
+ private String calculateNewEndTime(String addTimeStart, Integer addTimeMinutes) {
|
|
|
+ try {
|
|
|
+ // 解析加时开始时间
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
|
|
|
+ Date startDate = sdf.parse(addTimeStart);
|
|
|
+
|
|
|
+ // 加上加时分钟数
|
|
|
+ Calendar calendar = Calendar.getInstance();
|
|
|
+ calendar.setTime(startDate);
|
|
|
+ calendar.add(Calendar.MINUTE, addTimeMinutes);
|
|
|
+
|
|
|
+ // 格式化为HH:mm
|
|
|
+ return sdf.format(calendar.getTime());
|
|
|
+ } catch (ParseException e) {
|
|
|
+ log.error("计算新的结束时间失败,addTimeStart={}, addTimeMinutes={}", addTimeStart, addTimeMinutes, e);
|
|
|
+ throw new RuntimeException("时间计算失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @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, String jingdu, String weidu) {
|
|
|
+ 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.getStoreInfoVoWithDistanceFields(order.getStoreId(), jingdu, weidu));
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 /store/reservationOrder/page 的页面展示逻辑写入详情:复用 page 接口数据
|
|
|
+ ReservationOrderPageVo pageVo = reservationOrderPageService.getPageByOrderId(orderId);
|
|
|
+ if (pageVo != null) {
|
|
|
+ copyPageVoToDetailVo(pageVo, vo);
|
|
|
+ }
|
|
|
+ vo.setRefundType(order.getRefundType());
|
|
|
+ if (order.getReservationId() != null) {
|
|
|
+ UserReservation r = this.getById(order.getReservationId());
|
|
|
+ if (r != null) {
|
|
|
+ vo.setMerchantCancelReason(r.getReason());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int markReservationTimeoutByEndTime() {
|
|
|
+ // 关联查询:订单待使用 + 预约结束时间已过,在库内一次筛出需更新的 reservation_id
|
|
|
+ List<Integer> toUpdateReservationIds = baseMapper.listReservationIdsForTimeoutMark();
|
|
|
+ if (toUpdateReservationIds == null || toUpdateReservationIds.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ Date now = new Date();
|
|
|
+ // 1. 更新 user_reservation.status = 未到店超时(4)
|
|
|
+ this.update(new LambdaUpdateWrapper<UserReservation>()
|
|
|
+ .in(UserReservation::getId, toUpdateReservationIds)
|
|
|
+ .set(UserReservation::getStatus, STATUS_NO_SHOW_TIMEOUT)
|
|
|
+ .set(UserReservation::getUpdatedTime, now));
|
|
|
+ // 2. 更新 user_reservation_order.order_status = 已过期(3)
|
|
|
+ userReservationOrderService.update(new LambdaUpdateWrapper<UserReservationOrder>()
|
|
|
+ .in(UserReservationOrder::getReservationId, toUpdateReservationIds)
|
|
|
+ .eq(UserReservationOrder::getOrderStatus, ORDER_STATUS_TO_USE)
|
|
|
+ .set(UserReservationOrder::getOrderStatus, ORDER_STATUS_EXPIRED)
|
|
|
+ .set(UserReservationOrder::getUpdatedTime, now));
|
|
|
+ log.info("预订未到店超时定时任务:更新 reservationIds={} 条为未到店超时/订单已过期", toUpdateReservationIds.size());
|
|
|
+ return toUpdateReservationIds.size();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 将页面 VO 字段复制到详情 VO,供前端订单详情页与 page 接口一致展示 */
|
|
|
+ private void copyPageVoToDetailVo(ReservationOrderPageVo page, ReservationOrderDetailVo detail) {
|
|
|
+ detail.setOrderId(page.getOrderId());
|
|
|
+ detail.setOrderSn(page.getOrderSn());
|
|
|
+ detail.setOrderStatus(page.getOrderStatus());
|
|
|
+ detail.setPaymentStatus(page.getPaymentStatus());
|
|
|
+ detail.setPageTitle(page.getPageTitle());
|
|
|
+ detail.setPageTitleSuffix(page.getPageTitleSuffix());
|
|
|
+ detail.setStatusSubtitle(page.getStatusSubtitle());
|
|
|
+ detail.setPaymentDeadline(page.getPaymentDeadline());
|
|
|
+ detail.setPaymentSecondsLeft(page.getPaymentSecondsLeft());
|
|
|
+ detail.setPaymentDeadlineMinutes(page.getPaymentDeadlineMinutes());
|
|
|
+ detail.setStoreId(page.getStoreId());
|
|
|
+ detail.setStoreName(page.getStoreName());
|
|
|
+ detail.setStoreAddress(page.getStoreAddress());
|
|
|
+ detail.setStoreTel(page.getStoreTel());
|
|
|
+ detail.setStorePosition(page.getStorePosition());
|
|
|
+ detail.setReservationDate(page.getReservationDate());
|
|
|
+ detail.setReservationDateText(page.getReservationDateText());
|
|
|
+ detail.setGuestAndCategoryText(page.getGuestAndCategoryText());
|
|
|
+ detail.setTableNumbersText(page.getTableNumbersText());
|
|
|
+ detail.setDiningTimeSlotText(page.getDiningTimeSlotText());
|
|
|
+ detail.setVerificationCode(page.getVerificationCode());
|
|
|
+ detail.setVerificationUrl(page.getVerificationUrl());
|
|
|
+ detail.setLateArrivalGraceMinutes(page.getLateArrivalGraceMinutes());
|
|
|
+ detail.setSeatRetentionText(page.getSeatRetentionText());
|
|
|
+ detail.setDepositAmount(page.getDepositAmount());
|
|
|
+ detail.setDepositText(page.getDepositText());
|
|
|
+ detail.setDepositRefundRule(page.getDepositRefundRule());
|
|
|
+ detail.setCancellationPolicyType(page.getCancellationPolicyType());
|
|
|
+ detail.setFreeCancellationDeadline(page.getFreeCancellationDeadline());
|
|
|
+ detail.setFreeCancellationDeadlineText(page.getFreeCancellationDeadlineText());
|
|
|
+ detail.setCancellationPolicyText(page.getCancellationPolicyText());
|
|
|
+ detail.setOrderCreatedTime(page.getOrderCreatedTime());
|
|
|
+ detail.setOrderCreatedTimeText(page.getOrderCreatedTimeText());
|
|
|
+ detail.setGuestCount(page.getGuestCount());
|
|
|
+ detail.setLocationTableText(page.getLocationTableText());
|
|
|
+ detail.setDiningTimeText(page.getDiningTimeText());
|
|
|
+ detail.setContactName(page.getContactName());
|
|
|
+ detail.setContactPhone(page.getContactPhone());
|
|
|
+ detail.setContactText(page.getContactText());
|
|
|
+ detail.setPayAmount(page.getPayAmount());
|
|
|
+ detail.setCanContinuePay(page.getCanContinuePay());
|
|
|
+ detail.setCanCancelReservation(page.getCanCancelReservation());
|
|
|
+ detail.setCanModifyReservation(page.getCanModifyReservation());
|
|
|
+ detail.setCanDelete(page.getCanDelete());
|
|
|
+ detail.setCanBookAgain(page.getCanBookAgain());
|
|
|
+ detail.setOutTradeNo(page.getOutTradeNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+}
|