package shop.alien.store.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.apache.commons.lang3.StringUtils;
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.config.BaseRedisService;
import shop.alien.store.service.*;
import shop.alien.store.util.ali.AliSms;
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 final BaseRedisService baseRedisService;
    private final AliSms aliSms;
    private final ReservationNoticeAsyncService reservationNoticeAsyncService;
    private final ArrivalReminderNoticeService arrivalReminderNoticeService;

    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;
    /** 到店提醒：短信防重（保持与原 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) {
        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("预约不存在");
        }

        // 修改前先取原时间与桌位（用于商家订单修改提醒）
        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());
        entity.setReservationNo(existing.getReservationNo());
        this.updateById(entity);

        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<>();
        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 LambdaQueryWrapper<UserReservationOrder>()
                .eq(UserReservationOrder::getId, id));
        if (one == null) {
            throw new RuntimeException("预约不存在");
        }
        Integer reservationId = one.getReservationId();

        // 当订单为未支付且为收费订单时，订单状态变为已关闭(5)；否则为已取消(4)
        int orderStatus = 4;
        if (one.getPaymentStatus() != null && one.getPaymentStatus() == 0
                && one.getOrderCostType() != null && one.getOrderCostType() == 1) {
            orderStatus = 5;
        }

        // 订单状态置为已取消(4)或已关闭(5)
        userReservationOrderService.update(
                new LambdaUpdateWrapper<UserReservationOrder>()
                        .eq(UserReservationOrder::getId, one.getId())
                        .set(UserReservationOrder::getOrderStatus, orderStatus));
        // 预约不再逻辑删除，仅将 status 置为已取消(3)
        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
    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 (reservation.getStatus() != null && reservation.getStatus() == 4) {
                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;
                }
            }
            // 当前时间已超过预约结束时间则不回显（endTime 为 String，需与 reservationDate 组合判断）
            if (isReservationEndBeforeNow(reservation.getReservationDate(), reservation.getEndTime())) {
                continue;
            }
            UserReservationVo vo = getDetail(reservation.getId());
            if (vo != null) {
                list.add(vo);
            }
        }
        // 按预约开始时间正序（支持 HH:mm 或 yyyy-MM-dd HH:mm，解析失败排最后）
        list.sort(Comparator.comparingInt(vo -> {
            int m = timeToMinutes(vo.getStartTime());
            return m < 0 ? Integer.MAX_VALUE : m;
        }));
        return list;
    }

    private static Calendar calendarOf(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        return c;
    }

    /**
     * 判断预约是否已结束（当前时刻晚于结束时刻）。endTime 支持 "HH:mm"（需 reservationDate）或 "yyyy-MM-dd HH:mm(:ss)"。
     */
    private static boolean isReservationEndBeforeNow(Date reservationDate, String endTimeStr) {
        if (endTimeStr == null || endTimeStr.trim().isEmpty()) {
            return false;
        }
        String trimmed = endTimeStr.trim();
        Date now = new Date();
        if (trimmed.contains(" ")) {
            for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) {
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat(pattern);
                    sdf.setLenient(false);
                    Date end = sdf.parse(trimmed);
                    return end.before(now);
                } catch (ParseException ignored) {
                    // try next pattern
                }
            }
            return false;
        }
        if (reservationDate == null) {
            return false;
        }
        int minutes = timeToMinutes(trimmed);
        if (minutes < 0) {
            return false;
        }
        Calendar endCal = Calendar.getInstance();
        endCal.setTime(reservationDate);
        endCal.set(Calendar.HOUR_OF_DAY, minutes / 60);
        endCal.set(Calendar.MINUTE, minutes % 60);
        endCal.set(Calendar.SECOND, 0);
        endCal.set(Calendar.MILLISECOND, 0);
        return endCal.getTime().before(now);
    }

    @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)
                        .eq(StoreBookingCategory::getIsDisplay, 1)
                        .orderByDesc(StoreBookingCategory::getSort));
        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" 或 "yyyy-MM-dd HH:mm" / "yyyy-MM-dd HH:mm:ss" 解析为当日 0 点起的分钟数，解析失败返回 -1。
     * 若带年月日（含空格），先去掉日期部分再按时分计算。
     */
    private static int timeToMinutes(String hhmm) {
        if (hhmm == null) {
            return -1;
        }
        hhmm = hhmm.trim();
        if (hhmm.contains(" ")) {
            hhmm = hhmm.substring(hhmm.indexOf(" ") + 1).trim();
        }
        String[] parts = hhmm.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};
        // 先从 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);
            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(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);
        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());
    }

    /** 将桌位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);
    }

    /**
     * 保存预约与桌号关联：先物理删除该预约下原有关联再插入
     */
    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) {
                String reservationReason = r.getReason();
                vo.setMerchantCancelReason(reservationReason);
            }
        }
        return vo;
    }

    @Override
    public int markReservationTimeoutByEndTime() {
        // 关联查询：订单待使用 + 已超过截止时间（retain_position_flag=1 为 start_time+保留分钟，否则仅 start_time）
        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));
        // 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());
        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);
    }
}
