# BookingServiceImpl.java

    package cloud.wconnect.rateshop.service.booking.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.rateshop.cons.booking.AdvanceAbscissaEnum;
    import cloud.wconnect.rateshop.cons.booking.BookingAbscissaCons;
    import cloud.wconnect.rateshop.cons.booking.StayLengthAbscissaEnum;
    import cloud.wconnect.rateshop.entity.booking.*;
    import cloud.wconnect.rateshop.mapper.booking.*;
    import cloud.wconnect.rateshop.pojo.bo.booking.AcrossMonthBasicBO;
    import cloud.wconnect.rateshop.pojo.bo.booking.AcrossQuarterBasicBO;
    import cloud.wconnect.rateshop.pojo.bo.booking.AcrossWeekBasicBO;
    import cloud.wconnect.rateshop.pojo.dto.booking.BookingDTO;
    import cloud.wconnect.rateshop.pojo.query.booking.BookingQuery;
    import cloud.wconnect.rateshop.service.booking.IBookingService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import jakarta.annotation.Resource;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.Month;
    import java.time.YearMonth;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author yyt
     * @since 2024-07-22
     */
    @Service
    @DS("rate_shop")
    public class BookingServiceImpl extends ServiceImpl<BookingMapper, Booking> implements IBookingService {

        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private BookingDayMapper bookingDayMapper;
        @Resource
        private BookingMonthMapper bookingMonthMapper;
        @Resource
        private BookingAdvanceMapper bookingAdvanceMapper;
        @Resource
        private BookingWeekMapper bookingWeekMapper;
        @Autowired
        private SqlSessionFactory sqlSessionFactory;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private BeanMapper beanMapper;

        /**
         * 保存 booking
         *
         * @param booking booking 数据
         * @author yyt
         * @date 2024/7/29
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveRewrite(Booking booking) {
            setBasicCalculateInfo(booking);
            save(booking);

            handleCalculateChartData(booking);
        }

        /**
         * 修改 booking
         *
         * @param booking booking 数据
         * @author yyt
         * @date 2024/7/29
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateRewrite(Booking booking) {
            setBasicCalculateInfo(booking);
            updateById(booking);

            handleCalculateChartData(booking);
        }

        /**
         * 删除 booking
         *
         * @param booking booking
         * @author yyt
         * @date 2024/7/30
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void removeRewrite(Booking booking) {
            removeById(booking.getId());

            handleCalculateChartData(booking);
        }

        /**
         * 设置基础计算信息
         *
         * @param booking booking 数据
         * @author yyt
         * @date 2024/7/29
         **/
        @Override
        public void setBasicCalculateInfo(Booking booking) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(booking.getPropertyId());
            // 入住天数
            long stayDays = DateTimeUtils.numberOfDays(DateTimeUtils.strToLocalDate(booking.getStartDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD),
                    DateTimeUtils.strToLocalDate(booking.getEndDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD));
            stayDays = stayDays == BaseConstant.ZERO ? BaseConstant.ONE : stayDays;
            booking.setStayDays((int) stayDays);

            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            // 平均房价
            booking.setAveragePrice(booking.getTotalAmount().divide(new BigDecimal(stayDays), BaseConstant.DATA_PRECISION, roundingMode));

            // 提前预定天数
            long advanceDays = DateTimeUtils.numberOfDays(DateTimeUtils.strToLocalDate(booking.getBookedDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD),
                    DateTimeUtils.strToLocalDate(booking.getStartDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD));
            booking.setAdvanceDays((int) (advanceDays < BaseConstant.ZERO ? BaseConstant.ZERO : advanceDays));
        }

        /**
         * 处理计算图表数据
         *
         * @param booking booking 数据
         * @author yyt
         * @date 2024/7/31
         **/
        @Override
        public void handleCalculateChartData(Booking booking) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(booking.getPropertyId());
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate startMonth = LocalDate.parse(booking.getBookedDate().toString(), dateTimeFormatter).withDayOfMonth(1);
            calculateChartData(propertyDTO, startMonth, startMonth);
        }

        /**
         * 计算 booking 多个月份不同维度的图表数据
         *
         * @param propertyDTO 资产
         * @param startMonth 开始月
         * @param endMonth 结束月
         * @author yyt
         * @date 2024/7/29
         **/
        @Override
        public void calculateChartData(PropertyDTO propertyDTO, LocalDate startMonth, LocalDate endMonth) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD);
            List<Booking> bookings = listForUpdate(propertyDTO.getId(), Integer.parseInt(startMonth.format(dateTimeFormatter)),
                    Integer.parseInt(endMonth.plusMonths(1).minusDays(1).format(dateTimeFormatter)));

            // 设置不同维度下的图表计算用的基础数据
            Map<Long, List<Booking>> bookingsByYearMonth = bookings.parallelStream()
                    .peek(item -> setChartDataAttribute(item, propertyDTO))
                    .collect(Collectors.groupingBy(Booking::getBookedYearMonth));

            // 计算 入住天数图表需要的数据
            calculateDayChartData(propertyDTO, startMonth, endMonth, bookingsByYearMonth);

            // 计算 月图表需要的数据
            calculateMonthChartData(propertyDTO, startMonth, endMonth, bookingsByYearMonth);

            // 计算 提前入住天数图表需要的数据
            calculateAdvanceChartData(propertyDTO, startMonth, endMonth, bookingsByYearMonth);

            // 计算 星期图表需要的数据
            calculateWeekChartData(propertyDTO, startMonth, endMonth, bookingsByYearMonth);
        }

        /**
         * 计算入住天数图表需要的数据
         *
         * @param propertyDTO         资产
         * @param startMonth          开始月
         * @param endMonth            结束与
         * @param bookingsByYearMonth booking
         * @author yyt
         * @date 2024/7/29
         **/
        private void calculateDayChartData(PropertyDTO propertyDTO, LocalDate startMonth, LocalDate endMonth,
                                           Map<Long, List<Booking>> bookingsByYearMonth) {
            List<BookingDay> bookingDays = Lists.newArrayListWithExpectedSize(12 * 8);
            Integer propertyId = propertyDTO.getId();
            for (LocalDate date = startMonth; !date.isAfter(endMonth); date = date.plusMonths(1)) {
                long yearMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(date.getYear(), date.getMonthValue()));
                List<Booking> bookings = bookingsByYearMonth.getOrDefault(yearMonth, Collections.emptyList());
                Map<Integer, List<Booking>> bookingsByStayDay = bookings.stream().collect(Collectors.groupingBy(Booking::getStayDays));

                bookingDays.addAll(Arrays.stream(StayLengthAbscissaEnum.values()).map(item -> {
                    List<Booking> calculateBookings;
                    int type = item.getType();
                    List<Integer> valueRange = item.getValueRange();
                    if (type == BookingAbscissaCons.FIXED) {
                        Integer fixedValue = valueRange.get(0);
                        calculateBookings = bookingsByStayDay.getOrDefault(fixedValue, Collections.emptyList());
                    } else if (type == BookingAbscissaCons.RANGE) {
                        Integer minValue = valueRange.get(0);
                        Integer maxValue = valueRange.get(1);
                        calculateBookings = bookings.parallelStream()
                                .filter(booking -> booking.getStayDays() >= minValue && booking.getStayDays() <= maxValue).toList();
                    } else {
                        Integer greaterValue = valueRange.get(0);
                        calculateBookings = bookings.parallelStream()
                                .filter(booking -> booking.getStayDays() > greaterValue).toList();
                    }

                    return new BookingDay().setPropertyId(propertyId).setYearMonth((int) yearMonth)
                            .setAbscissa(item.getAbscissa()).setCount(calculateBookings.size())
                            .setRevenue(calculateBookings.stream().map(Booking::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add))
                            .setDays(calculateBookings.stream().map(Booking::getStayDays).reduce(BaseConstant.ZERO, Integer::sum));
                }).toList());
            }

            if (CollectionUtils.isNotEmpty(bookingDays)) {
                bookingDayMapper.saveOrUpdateBatch(bookingDays);
            }
        }

        /**
         * 计算月图表需要的数据
         *
         * @param propertyDTO         资产
         * @param startMonth          开始月
         * @param endMonth            结束与
         * @param bookingsByYearMonth booking
         * @author yyt
         * @date 2024/7/29
         **/
        private void calculateMonthChartData(PropertyDTO propertyDTO, LocalDate startMonth, LocalDate endMonth,
                                             Map<Long, List<Booking>> bookingsByYearMonth) {
            Month[] months = Month.values();
            List<BookingMonth> bookingMonths = Lists.newArrayListWithExpectedSize(12 * 12);
            Integer propertyId = propertyDTO.getId();
            for (LocalDate date = startMonth; !date.isAfter(endMonth); date = date.plusMonths(1)) {
                long yearMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(date.getYear(), date.getMonthValue()));
                List<Booking> bookings = bookingsByYearMonth.getOrDefault(yearMonth, Collections.emptyList());
                Map<Integer, List<Booking>> bookingsByMonth = bookings.stream().collect(Collectors.groupingBy(Booking::getCheckInMonth));

                bookingMonths.addAll(Arrays.stream(months).map(item -> {
                    int month = item.getValue();

                    // 根据预定数据的 横跨月字段（标记此单在入住区间内每个月占用的天数）, 筛选出当前月下的单据
                    List<Booking> calculateBookings = bookings.parallelStream().filter(booking -> {
                        List<AcrossMonthBasicBO> acrossMonthBasicBOS = JsonUtils.convertStringToList(objectMapper,
                                booking.getStayAcrossMonth(), AcrossMonthBasicBO.class);
                        booking.setAcrossMonthBasicBOS(acrossMonthBasicBOS);
                        return acrossMonthBasicBOS.stream().anyMatch(acrossMonthBasicBO -> Objects.equals(acrossMonthBasicBO.getMonth(), month));
                    }).toList();

                    AtomicInteger days = new AtomicInteger(0);

                    return new BookingMonth().setPropertyId(propertyId).setYearMonth((int) yearMonth)
                            .setAbscissa(month).setCount(bookingsByMonth.getOrDefault(month, Collections.emptyList()).size())
                            .setRevenue(calculateBookings.parallelStream().map(booking -> {
                                Optional<AcrossMonthBasicBO> first = booking.getAcrossMonthBasicBOS().stream()
                                        .filter(value -> Objects.equals(value.getMonth(), month)).findFirst();
                                if (first.isPresent()) {
                                    AcrossMonthBasicBO acrossMonthBasicBO = first.get();
                                    days.addAndGet(acrossMonthBasicBO.getDays());
                                    return new BigDecimal(acrossMonthBasicBO.getDays()).multiply(booking.getAveragePrice());
                                } else {
                                    return BigDecimal.ZERO;
                                }
                            }).reduce(BigDecimal.ZERO, BigDecimal::add))
                            .setDays(days.get());
                }).toList());
            }

            if (CollectionUtils.isNotEmpty(bookingMonths)) {
                bookingMonthMapper.saveOrUpdateBatch(bookingMonths);
            }
        }

        /**
         * 计算提前入住天数图表需要的数据
         *
         * @param propertyDTO         资产
         * @param startMonth          开始月
         * @param endMonth            结束与
         * @param bookingsByYearMonth booking
         * @author yyt
         * @date 2024/7/29
         **/
        private void calculateAdvanceChartData(PropertyDTO propertyDTO, LocalDate startMonth, LocalDate endMonth,
                                               Map<Long, List<Booking>> bookingsByYearMonth) {
            List<BookingAdvance> bookingAdvances = Lists.newArrayListWithExpectedSize(12 * 7);
            Integer propertyId = propertyDTO.getId();
            for (LocalDate date = startMonth; !date.isAfter(endMonth); date = date.plusMonths(1)) {
                long yearMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(date.getYear(), date.getMonthValue()));
                List<Booking> bookings = bookingsByYearMonth.getOrDefault(yearMonth, Collections.emptyList());

                bookingAdvances.addAll(Arrays.stream(AdvanceAbscissaEnum.values()).map(item -> {
                    List<Booking> calculateBookings;
                    int type = item.getType();
                    List<Integer> valueRange = item.getValueRange();
                    if (type == BookingAbscissaCons.RANGE) {
                        Integer minValue = valueRange.get(0);
                        Integer maxValue = valueRange.get(1);
                        calculateBookings = bookings.parallelStream()
                                .filter(booking -> booking.getAdvanceDays() >= minValue && booking.getAdvanceDays() <= maxValue).toList();
                    } else {
                        Integer greaterEqualValue = valueRange.get(0);
                        calculateBookings = bookings.parallelStream()
                                .filter(booking -> booking.getAdvanceDays() >= greaterEqualValue).toList();
                    }

                    return new BookingAdvance().setPropertyId(propertyId).setYearMonth((int) yearMonth)
                            .setAbscissa(item.getAbscissa()).setCount(calculateBookings.size())
                            .setRevenue(calculateBookings.parallelStream().map(Booking::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add))
                            .setDays(calculateBookings.parallelStream().map(Booking::getStayDays).reduce(BaseConstant.ZERO, Integer::sum));
                }).toList());
            }

            if (CollectionUtils.isNotEmpty(bookingAdvances)) {
                bookingAdvanceMapper.saveOrUpdateBatch(bookingAdvances);
            }
        }

        /**
         * 计算 星期图表需要的数据
         *
         * @param propertyDTO         资产
         * @param startMonth          开始月
         * @param endMonth            结束与
         * @param bookingsByYearMonth booking
         * @author yyt
         * @date 2024/7/29
         **/
        private void calculateWeekChartData(PropertyDTO propertyDTO, LocalDate startMonth, LocalDate endMonth,
                                            Map<Long, List<Booking>> bookingsByYearMonth) {
            List<BookingWeek> bookingWeeks = Lists.newArrayListWithExpectedSize(48);
            Integer propertyId = propertyDTO.getId();
            DayOfWeek[] weeks = DayOfWeek.values();
            for (LocalDate date = startMonth; !date.isAfter(endMonth); date = date.plusMonths(1)) {
                long yearMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(date.getYear(), date.getMonthValue()));
                List<Booking> bookings = bookingsByYearMonth.getOrDefault(yearMonth, Collections.emptyList());
                Map<Integer, List<Booking>> bookingsByWeek = bookings.stream().collect(Collectors.groupingBy(Booking::getCheckInWeek));

                bookingWeeks.addAll(Arrays.stream(weeks).parallel().map(item -> {
                    int week = item.getValue();

                    // 根据预定数据的 横跨周字段（标记此单在入住区间内每周周几占用的天数）, 筛选出当前周几下的单据
                    List<Booking> calculateBookings = bookings.parallelStream().filter(booking -> {
                        List<AcrossWeekBasicBO> acrossWeekBasicBOS = JsonUtils.convertStringToList(objectMapper,
                                booking.getStayAcrossWeek(), AcrossWeekBasicBO.class);
                        booking.setAcrossWeekBasicBOS(acrossWeekBasicBOS);
                        return acrossWeekBasicBOS.stream().anyMatch(acrossWeekBasicBO -> Objects.equals(acrossWeekBasicBO.getWeek(), week));
                    }).toList();

                    AtomicInteger days = new AtomicInteger(0);

                    return new BookingWeek().setPropertyId(propertyId).setYearMonth((int) yearMonth)
                            .setAbscissa(week).setCount(bookingsByWeek.getOrDefault(week, Collections.emptyList()).size())
                            .setRevenue(calculateBookings.parallelStream().map(booking -> {
                                Optional<AcrossWeekBasicBO> first = booking.getAcrossWeekBasicBOS().stream()
                                        .filter(value -> Objects.equals(value.getWeek(), week)).findFirst();
                                if (first.isPresent()) {
                                    AcrossWeekBasicBO acrossWeekBasicBO = first.get();
                                    days.addAndGet(acrossWeekBasicBO.getDays());
                                    return new BigDecimal(acrossWeekBasicBO.getDays()).multiply(booking.getAveragePrice());
                                } else {
                                    return BigDecimal.ZERO;
                                }
                            }).reduce(BigDecimal.ZERO, BigDecimal::add))
                            .setDays(days.get());
                }).toList());
            }

            if (CollectionUtils.isNotEmpty(bookingWeeks)) {
                bookingWeekMapper.saveOrUpdateBatch(bookingWeeks);
            }
        }

        /**
         * 设置图表展示数据需要的相关属性值
         *
         * @param booking     booking
         * @param propertyDTO 资产
         * @author yyt
         * @date 2024/7/22
         **/
        @Override
        public void setChartDataAttribute(Booking booking, PropertyDTO propertyDTO) {
            setBasicCalculateInfo(booking);

            // 预定日期所在年月
            LocalDate bookedDate = DateTimeUtils.strToLocalDate(booking.getBookedDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            booking.setBookedYearMonth(YearMonthUtils.parseYearMonthToLong(YearMonth.of(bookedDate.getYear(), bookedDate.getMonthValue())));

            LocalDate startDate = DateTimeUtils.strToLocalDate(booking.getStartDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);

            // 入住日期所在季度（1,2,3,4）
            booking.setCheckInQuarter(QuarterUtil.getQuarter(startDate));
            // 入住横跨的季度 [{"quarter":"季度（1,2,3,4）","days":"入住天数"}]
            booking.setStayAcrossQuarter(calculateAcrossQuarter(booking.getStartDate(), booking.getEndDate()));

            // 入住日期所在月
            booking.setCheckInMonth(startDate.getMonthValue());
            // 入住横跨的月 [{"month":"月","days":"入住天数"}]
            booking.setStayAcrossMonth(calculateAcrossMonth(booking.getStartDate(), booking.getEndDate()));

            // 入住日期所在周几（1-7）
            booking.setCheckInWeek(startDate.getDayOfWeek().getValue());
            // 入住横跨的周几 [{"week":"周几（1-7）","days":"入住天数"}]
            booking.setStayAcrossWeek(calculateAcrossWeek(booking.getStartDate(), booking.getEndDate()));
        }

        /**
         * 计算入住横跨的季度
         *
         * @param startDate 开始日期
         * @param endDate   结束日期
         * @return 入住横跨的季度
         * @author yyt
         * @date 2024/7/22
         **/
        @Override
        public String calculateAcrossQuarter(Integer startDate, Integer endDate) {
            List<AcrossQuarterBasicBO> acrossQuarterBasicBOS = Lists.newArrayListWithExpectedSize(12);

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            if (startLocalDate.equals(endLocalDate)) {
                return JsonUtils.convertObjListToString(objectMapper, Lists.newArrayList(new AcrossQuarterBasicBO(QuarterUtil.getQuarter(startLocalDate), 1)));
            }

            int i = BaseConstant.ZERO;
            LocalDate quarterStartDateOfStartDate = QuarterUtil.getQuarterStartDate(startLocalDate);
            LocalDate quarterStartDateOfEndDate = QuarterUtil.getQuarterStartDate(endLocalDate);
            for (LocalDate localDate = quarterStartDateOfStartDate; !localDate.isAfter(quarterStartDateOfEndDate); localDate = localDate.plusMonths(3)) {
                // 年、季度 都一样，说明循环截止
                if (localDate.getYear() == endLocalDate.getYear()
                        && QuarterUtil.getQuarter(localDate) == QuarterUtil.getQuarter(endLocalDate)) {
                    if (i == BaseConstant.ZERO) {
                        handleDayForQuarter(startLocalDate, endLocalDate, false, acrossQuarterBasicBOS);
                    } else {
                        handleDayForQuarter(localDate, endLocalDate, false, acrossQuarterBasicBOS);
                    }
                } else {
                    if (i == BaseConstant.ZERO) {
                        handleDayForQuarter(startLocalDate, QuarterUtil.getQuarterEndDate(startLocalDate),
                                true, acrossQuarterBasicBOS);
                    } else {
                        handleDayForQuarter(localDate, localDate.plusMonths(3).minusDays(1),
                                true, acrossQuarterBasicBOS);
                    }
                }
                i++;
            }

            return JsonUtils.convertObjListToString(objectMapper, acrossQuarterBasicBOS);
        }

        /**
         * 处理两日期区间之间天数
         *
         * @param startLocalDate        开始日期
         * @param endLocalDate          结束日期
         * @param addDay                是否需要累加最后一天，例如：2024-07-01 - 2024-07-03 之间，7月3日也算一天
         * @param acrossQuarterBasicBOS acrossQuarterBasicBOS
         * @author yyt
         * @date 2024/7/23
         **/
        private void handleDayForQuarter(LocalDate startLocalDate, LocalDate endLocalDate, boolean addDay, List<AcrossQuarterBasicBO> acrossQuarterBasicBOS) {
            // 间隔天数
            int days = (int) DateTimeUtils.numberOfDays(startLocalDate, endLocalDate);

            Optional<AcrossQuarterBasicBO> first = acrossQuarterBasicBOS.stream()
                    .filter(acrossQuarterBasicBO -> acrossQuarterBasicBO.getQuarter().equals(QuarterUtil.getQuarter(startLocalDate)))
                    .findFirst();
            if (first.isPresent()) {
                AcrossQuarterBasicBO acrossQuarterBasicBO = first.get();
                acrossQuarterBasicBO.setDays(acrossQuarterBasicBO.getDays() + days + (addDay ? 1 : 0));
            } else {
                acrossQuarterBasicBOS.add(new AcrossQuarterBasicBO(QuarterUtil.getQuarter(startLocalDate),
                        days + (addDay ? 1 : 0)));
            }
        }

        /**
         * 计算入住横跨的月
         *
         * @param startDate 开始日期
         * @param endDate   结束日期
         * @return 入住横跨的月
         * @author yyt
         * @date 2024/7/23
         **/
        @Override
        public String calculateAcrossMonth(Integer startDate, Integer endDate) {
            List<AcrossMonthBasicBO> acrossMonthBasicBOS = Lists.newArrayListWithExpectedSize(36);

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            if (startLocalDate.equals(endLocalDate)) {
                return JsonUtils.convertObjListToString(objectMapper, Lists.newArrayList(new AcrossMonthBasicBO(startLocalDate.getMonthValue(), 1)));
            }

            int i = BaseConstant.ZERO;
            LocalDate startOfMonthOfStartDate = startLocalDate.withDayOfMonth(1);
            LocalDate startOfMonthOfEndDate = endLocalDate.withDayOfMonth(1);
            for (LocalDate localDate = startOfMonthOfStartDate; !localDate.isAfter(startOfMonthOfEndDate); localDate = localDate.plusMonths(1)) {
                // 年 、月 都一样，说明循环截止
                if (localDate.getYear() == startOfMonthOfEndDate.getYear() && localDate.getMonthValue() == startOfMonthOfEndDate.getMonthValue()) {
                    if (i == BaseConstant.ZERO) {
                        handleDayForMonth(startLocalDate, endLocalDate, false, acrossMonthBasicBOS);
                    } else {
                        handleDayForMonth(localDate, endLocalDate, false, acrossMonthBasicBOS);
                    }
                } else {
                    if (i == BaseConstant.ZERO) {
                        handleDayForMonth(startLocalDate, startLocalDate.withDayOfMonth(1).plusMonths(1).minusDays(1),
                                true, acrossMonthBasicBOS);
                    } else {
                        handleDayForMonth(localDate, localDate.plusMonths(1).minusDays(1),
                                true, acrossMonthBasicBOS);
                    }
                }
                i++;
            }

            return JsonUtils.convertObjListToString(objectMapper, acrossMonthBasicBOS);
        }

        /**
         * 处理两日期区间之间天数
         *
         * @param startLocalDate      开始日期
         * @param endLocalDate        结束日期
         * @param addDay              是否需要累加最后一天，例如：2024-07-01 - 2024-07-03 之间，7月3日也算一天
         * @param acrossMonthBasicBOS acrossMonthBasicBOS
         * @author yyt
         * @date 2024/7/23
         **/
        private void handleDayForMonth(LocalDate startLocalDate, LocalDate endLocalDate, boolean addDay, List<AcrossMonthBasicBO> acrossMonthBasicBOS) {
            // 间隔天数
            int days = (int) DateTimeUtils.numberOfDays(startLocalDate, endLocalDate);

            Optional<AcrossMonthBasicBO> first = acrossMonthBasicBOS.stream()
                    .filter(acrossQuarterBasicBO -> acrossQuarterBasicBO.getMonth() == startLocalDate.getMonthValue())
                    .findFirst();
            if (first.isPresent()) {
                AcrossMonthBasicBO acrossMonthBasicBO = first.get();
                acrossMonthBasicBO.setDays(acrossMonthBasicBO.getDays() + days + (addDay ? 1 : 0));
            } else {
                acrossMonthBasicBOS.add(new AcrossMonthBasicBO(startLocalDate.getMonthValue(), days + (addDay ? 1 : 0)));
            }
        }

        /**
         * 计算入住横跨的周几
         *
         * @param startDate 开始日期
         * @param endDate   结束日期
         * @return 入住横跨的周几
         * @author yyt
         * @date 2024/7/23
         **/
        @Override
        public String calculateAcrossWeek(Integer startDate, Integer endDate) {
            List<AcrossWeekBasicBO> acrossWeekBasicBOS = Lists.newArrayListWithExpectedSize(144);

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            if (startLocalDate.equals(endLocalDate)) {
                return JsonUtils.convertObjListToString(objectMapper, Lists.newArrayList(new AcrossWeekBasicBO(startLocalDate.getDayOfWeek().getValue(), 1)));
            }

            for (LocalDate localDate = startLocalDate; !localDate.isAfter(endLocalDate); localDate = localDate.plusDays(1)) {
                int day = localDate.equals(endLocalDate) ? BaseConstant.ZERO : BaseConstant.ONE;
                int week = localDate.getDayOfWeek().getValue();
                Optional<AcrossWeekBasicBO> first = acrossWeekBasicBOS.stream()
                        .filter(acrossQuarterBasicBO -> acrossQuarterBasicBO.getWeek() == week)
                        .findFirst();
                if (first.isPresent()) {
                    AcrossWeekBasicBO acrossWeekBasicBO = first.get();
                    acrossWeekBasicBO.setDays(acrossWeekBasicBO.getDays() + day);
                } else {
                    if (day > BaseConstant.ZERO) {
                        acrossWeekBasicBOS.add(new AcrossWeekBasicBO(week, BaseConstant.ONE));
                    }
                }
            }

            return JsonUtils.convertObjListToString(objectMapper, acrossWeekBasicBOS);
        }

        /**
         * 检查存在
         *
         * @param id booking id
         * @return booking
         * @author yyt
         * @date 2024/7/23
         **/
        @Override
        public Booking checkExist(long id) {
            Booking booking = getById(id);
            if (Objects.isNull(booking)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the booking is not found;id:" + id);
            }
            return booking;
        }

        /**
         * 查询预定日期在 指定资产、指定区间内的 booking
         *
         * @param propertyId 资产
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return booking
         * @author yyt
         * @date 2024/7/23
         **/
        @Override
        public List<Booking> listForUpdate(Integer propertyId, Integer startDate, Integer endDate) {
            return list(new QueryWrapper<Booking>().lambda().eq(Booking::getPropertyId, propertyId)
                    .between(Booking::getBookedDate, startDate, endDate)
                    .select(Booking::getPropertyId, Booking::getStartDate, Booking::getEndDate, Booking::getTotalAmount, Booking::getBookedDate)
                    .last("for update"));
        }

        /**
         * 批量保存
         *
         * @param bookings booking
         * @author yyt
         * @date 2024/7/24
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveBatchRewrite(List<Booking> bookings) {
            if (bookings.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            BookingMapper mapper = sqlSession.getMapper(BookingMapper.class);
            for (int i = 0; i < bookings.size(); i++) {
                Booking booking = bookings.get(i);
                mapper.insert(booking);

                //每 200 条提交一次
                if ((i + 1) % 200 == 0) {
                    sqlSession.flushStatements();
                }
            }

            sqlSession.flushStatements();
        }

        /**
         * 批量删除 booking
         *
         * @param propertyDTO 资产
         * @param bookings    booking
         * @author yyt
         * @date 2024/8/2
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void removeBatchRewrite(PropertyDTO propertyDTO, List<Booking> bookings) {
            removeByIds(bookings.stream().map(Booking::getId).collect(Collectors.toList()));

            bookings.sort(Comparator.comparing(Booking::getBookedDate));

            LocalDate startMonth = DateTimeUtils.strToLocalDate(bookings.get(0).getBookedDate().toString(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).withDayOfMonth(1);
            LocalDate endMonth = DateTimeUtils.strToLocalDate(bookings.get(bookings.size() - 1).getBookedDate().toString(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).withDayOfMonth(1);
            calculateChartData(propertyDTO, startMonth, endMonth);
        }

        /**
         * 根据日期区间删除 booking
         *
         * @param propertyDTO 资产
         * @param startDate   开始日期
         * @param endDate     结束日期
         * @author yyt
         * @date 2024/8/2
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void removeBatchByDateRange(PropertyDTO propertyDTO, Integer startDate, Integer endDate) {
            remove(new QueryWrapper<Booking>().lambda().eq(Booking::getPropertyId, propertyDTO.getId())
                    .between(Booking::getBookedDate, startDate, endDate));

            LocalDate startMonth = DateTimeUtils.strToLocalDate(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).withDayOfMonth(1);
            LocalDate endMonth = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).withDayOfMonth(1);
            calculateChartData(propertyDTO, startMonth, endMonth);
        }

        /**
         * 查询 booking 数据
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @return booking 数据
         * @author yyt
         * @date 2026/1/19
         **/
        @Override
        public ListPagedDTO<BookingDTO> listBookingDTO(int propertyId, BookingQuery query) {
            Page<Booking> page = page(new Page<>(query.getPage(), query.getSize()), new QueryWrapper<Booking>().lambda()
                    .eq(Booking::getPropertyId, propertyId)
                    .between(Booking::getBookedDate, query.getStartDate(), query.getEndDate())
                    .orderByDesc(Booking::getBookedDate));

            page.getRecords().forEach(this::setBasicCalculateInfo);

            return new ListPagedDTO<>(page.getTotal(), beanMapper.mapList(page.getRecords(), BookingDTO.class));
        }

        /**
         * 保存 booking
         *
         * @param propertyId 资产 id
         * @param bookingDTO booking 数据
         * @param userId     操作人
         * @author yyt
         * @date 2026/1/19
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(Integer propertyId, BookingDTO bookingDTO, int userId) {
            Booking booking = beanMapper.map(bookingDTO, Booking.class);
            booking.setPropertyId(propertyId)
                    .setUpdatedBy(userId)
                    .setUpdatedTime(DateTimeUtils.nowTimestamp());
            saveRewrite(booking);
        }

        /**
         * 更新 booking
         *
         * @param booking    booking
         * @param bookingDTO booking 数据
         * @param userId     操作人
         * @author yyt
         * @date 2026/1/19
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void update(Booking booking, BookingDTO bookingDTO, int userId) {
            booking.setStartDate(bookingDTO.getStartDate())
                    .setEndDate(bookingDTO.getEndDate())
                    .setTotalAmount(bookingDTO.getTotalAmount())
                    .setBookedDate(bookingDTO.getBookedDate())
                    .setUpdatedBy(userId)
                    .setUpdatedTime(DateTimeUtils.nowTimestamp());
            updateRewrite(booking);
        }

    }