# PastReservationServiceImpl.java

    package cloud.wconnect.frontoffice.service.pastreservation.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.cons.StatusCons;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.TransactionUtils;
    import cloud.wconnect.frontoffice.cons.pastreservation.ActionCons;
    import cloud.wconnect.frontoffice.cons.pastreservation.DayTypeEnum;
    import cloud.wconnect.frontoffice.cons.pastreservation.PastReservationStatusEnum;
    import cloud.wconnect.frontoffice.entity.pastreservation.*;
    import cloud.wconnect.frontoffice.mapper.pastreservation.PastReservationMapper;
    import cloud.wconnect.frontoffice.pojo.bo.pastreservation.GuestNameBO;
    import cloud.wconnect.frontoffice.pojo.dto.pastreservation.*;
    import cloud.wconnect.frontoffice.pojo.query.pastreservation.PastReservationQuery;
    import cloud.wconnect.frontoffice.service.pastreservation.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Nullable;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections.MapUtils;
    import org.apache.commons.lang3.StringUtils;
    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.context.annotation.Lazy;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.time.ZonedDateTime;
    import java.time.temporal.ChronoUnit;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 连续入住人员信息表 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2024-10-01
     */
    @Service
    @DS("logbook")
    public class PastReservationServiceImpl extends ServiceImpl<PastReservationMapper, PastReservation> implements IPastReservationService {

        @Resource
        private IPastReservationIntervalDayService pastReservationIntervalDayService;
        @Resource
        private IPastReservationConsecutiveDayService pastReservationConsecutiveDayService;
        @Lazy
        @Resource
        private IPastReservationOpGuestinHouseService pastReservationOpGuestinHouseService;
        @Resource
        private IPastReservationDayLogService pastReservationDayLogService;
        @Autowired
        private IPastReservationCreditLimitService pastReservationCreditLimitService;
        @Resource
        private IPastReservationCommentService pastReservationCommentService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private SqlSessionFactory sqlSessionFactory;
        @Autowired
        private BeanMapper beanMapper;

        /**
         * 查询当前在店的连续入住信息
         *
         * @param propertyId 资产 id
         * @return 连续入住信息
         */
        @Override
        public List<PastReservation> list(Integer propertyId) {
            return list(new QueryWrapper<PastReservation>().lambda()
                    .eq(PastReservation::getPropertyId, propertyId).eq(PastReservation::getDeleted, false));
        }

        /**
         * 查询当前在店的连续入住信息（不包含忽略状态的数据）
         *
         * @param propertyId 资产 id
         * @return 连续入住信息
         */
        @Override
        public List<PastReservation> listExcludeIgnore(Integer propertyId) {
            return list(new QueryWrapper<PastReservation>().lambda()
                    .eq(PastReservation::getPropertyId, propertyId)
                    .eq(PastReservation::getDeleted, false)
                    .eq(PastReservation::getStatus, PastReservationStatusEnum.NORMAL_DISPLAY.getCode())
                    .orderByDesc(PastReservation::getId));
        }

        /**
         * 校验存在
         *
         * @param id 连续入住信息 id
         * @return 连续入住信息
         * @author yyt
         * @date 2024/10/2
         **/
        @Override
        public PastReservation checkExist(long id) {
            PastReservation pastReservation = getById(id);
            if (Objects.isNull(pastReservation)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the past reservation is not found");
            }

            return pastReservation;
        }

        /**
         * 删除连续入住信息
         *
         * @param pastReservation 连续入住信息
         * @param userId          操作人
         * @author yyt
         * @date 2024/10/2
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void delete(PastReservation pastReservation, int userId) {
            long now = DateTimeUtils.nowTimestamp();
            // 标记已删除
            updateById(new PastReservation().setId(pastReservation.getId()).setDeleted(true).setDeletedBy(userId).setDeletedTime(now));

            // 标记入住记录为已删除
            pastReservationOpGuestinHouseService.update(new UpdateWrapper<PastReservationOpGuestinHouse>().lambda()
                    .set(PastReservationOpGuestinHouse::getDeleted, true)
                    .set(PastReservationOpGuestinHouse::getDeletedBy, userId)
                    .set(PastReservationOpGuestinHouse::getDeletedTime, now)
                    .in(PastReservationOpGuestinHouse::getId, Arrays.stream(pastReservation.getMatchedIds().split(",")).toList())
                    .eq(PastReservationOpGuestinHouse::getDeleted, false));
        }

        /**
         * 保存配置
         *
         * @param propertyId 资产 id
         * @param configDTO  配置
         * @param userId     操作人
         * @param logs       操作记录
         * @author yyt
         * @date 2024/10/2
         **/
        @Async
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveDayConfig(Integer propertyId, PastReservationDayConfigDTO configDTO, int userId, List<PastReservationDayLog> logs) {
            if (CollectionUtils.isEmpty(logs)) {
                return;
            }

            boolean flag = true;
            try {
                PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

                // 修改之后的最小连续天数
                Integer newConsecutiveDays = configDTO.getConsecutiveDays().stream()
                        .filter(item -> Objects.nonNull(item.getDay())).min(Comparator.comparing(PastReservationConsecutiveDayDTO::getDay)).get().getDay();

                // 1.修改间隔天数
                Optional<PastReservationDayLog> optionalIntervalDayLog = logs.stream().filter(item -> item.getAction() == ActionCons.MODIFY_INTERVAL).findFirst();
                if (optionalIntervalDayLog.isPresent()) {
                    PastReservationIntervalDay intervalDay = pastReservationIntervalDayService.getByPropertyIdForUpdate(propertyId);
                    pastReservationIntervalDayService.saveOrUpdate(new PastReservationIntervalDay().setId(Objects.isNull(intervalDay) ? null : intervalDay.getId())
                            .setPropertyId(propertyId).setDay(configDTO.getIntervalDay()));

                    resetDataForIntervalDays(propertyDTO, configDTO.getIntervalDay(), newConsecutiveDays, userId);
                }

                // 2.修改notice，3.修改alert，4.修改warning
                resetDataForConsecutiveDays(propertyDTO, newConsecutiveDays, userId);
                // 设置连续天数
                pastReservationConsecutiveDayService.saveOrUpdateBatchRewrite(configDTO.getConsecutiveDays().stream()
                        .map(item -> new PastReservationConsecutiveDay().setPropertyId(propertyId)
                                .setType(item.getType()).setDay(item.getDay()))
                        .sorted(Comparator.comparing(PastReservationConsecutiveDay::getType)).collect(Collectors.toList()));
            } catch (Exception e) {
                flag = false;
                throw new InternalServerErrorException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.SYSTEM_ERROR.getCode(), "failed to save configuration", e);
            } finally {
                if (flag) {
                    updateSuccess(logs);
                } else {
                    updateFail(logs);
                }
            }
        }

        /**
         * 修改间隔天数后重新计算连续入住数据
         *
         * @param propertyDTO     资产
         * @param intervalDay     间隔天数
         * @param consecutiveDays 修改之后的连续天数
         * @param userId          操作人
         * @author yyt
         * @date 2024/10/3
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void resetDataForIntervalDays(PropertyDTO propertyDTO, Integer intervalDay, Integer consecutiveDays, int userId) {
            // 处理在间隔天数之内的入住人员记录的连续入住标识
            Integer propertyId = propertyDTO.getId();
            String timezone = propertyDTO.getTimezone();
            ZonedDateTime dateNow = DateTimeUtils.dateNow(timezone);
            long startDate = dateNow.minusDays(intervalDay).toEpochSecond();
            long endDate = dateNow.plusDays(intervalDay).toEpochSecond();
            pastReservationOpGuestinHouseService.resetGroupKeyForAllRecord(propertyId, startDate, endDate);

            // 重新计算入住天数
            computeInHouseDay(propertyId, userId, null);
        }

        /**
         * 统计用户入住天数
         *
         * @param inHouseRecords 入住记录
         * @param timeZone       时区
         * @return 用户入住天数
         * @Author: yyt
         * 2024/10/3
         */
        private int statisticalInHouseDay(List<PastReservationOpGuestinHouse> inHouseRecords, String timeZone) {
            // 开始
            long fromDate = 0L;
            // 结束
            long toDate = 0L;
            // 总天数
            long totalDays = 0L;
            for (PastReservationOpGuestinHouse record : inHouseRecords) {
                long checkInDate = record.getCheckInDate();
                long checkOutDate = record.getCheckOutDate();

                if (fromDate == 0 && toDate == 0) {
                    // 初始起止范围
                    fromDate = checkInDate;
                    toDate = checkOutDate;
                    continue;
                }

                if (checkInDate > toDate) {
                    // 当前入住记录不在前面的入住记录范围内，计算上个入住记录区间内的天数，并重新赋值起止时间
                    totalDays += ChronoUnit.DAYS.between(DateTimeUtils.toZonedDateTime(fromDate, timeZone).toLocalDate(),
                            DateTimeUtils.toZonedDateTime(toDate, timeZone).toLocalDate());
                    fromDate = checkInDate;
                    toDate = checkOutDate;
                } else {
                    // 当前入住记录在上个入住记录范围内，重新赋值起止时间
                    toDate = Math.max(toDate, checkOutDate);
                }
            }

            // 累计起止时间内的天数
            totalDays += ChronoUnit.DAYS.between(DateTimeUtils.toZonedDateTime(fromDate, timeZone).toLocalDate(),
                    DateTimeUtils.toZonedDateTime(toDate, timeZone).toLocalDate());
            return Long.valueOf(totalDays).intValue();
        }

        /**
         * 批量新增
         *
         * @param pastReservations 连续入住信息
         * @author yyt
         * @date 2024/10/3
         **/
        private void saveBatch(List<PastReservation> pastReservations) {
            if (pastReservations.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            PastReservationMapper mapper = sqlSession.getMapper(PastReservationMapper.class);
            for (int i = 0; i < pastReservations.size(); i++) {
                PastReservation item = pastReservations.get(i);
                mapper.insert(item);

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

            sqlSession.flushStatements();
        }

        /**
         * 批量更新
         *
         * @param pastReservations 连续入住信息
         * @author yyt
         * @date 2024/10/3
         **/
        private void updateBatchById(List<PastReservation> pastReservations) {
            if (pastReservations.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            PastReservationMapper mapper = sqlSession.getMapper(PastReservationMapper.class);
            for (int i = 0; i < pastReservations.size(); i++) {
                PastReservation item = pastReservations.get(i);
                mapper.updateById(item);

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

            sqlSession.flushStatements();
        }

        /**
         * 修改连续入住天数后重新计算连续入住数据
         *
         * @param propertyDTO        资产
         * @param newConsecutiveDays 修改之后的连续天数
         * @param userId             操作人
         * @author yyt
         * @date 2024/10/3
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void resetDataForConsecutiveDays(PropertyDTO propertyDTO, Integer newConsecutiveDays, int userId) {
            Integer propertyId = propertyDTO.getId();
            // 改之前的最小连续天数
            Integer oldConsecutiveDays = pastReservationConsecutiveDayService.listByPropertyIdForUpdate(propertyId)
                    .stream().filter(item -> Objects.nonNull(item.getDay()))
                    .min(Comparator.comparing(PastReservationConsecutiveDay::getDay)).map(PastReservationConsecutiveDay::getDay).orElse(null);
            // 连续天数未变，无需重新计算数据
            if (newConsecutiveDays.equals(oldConsecutiveDays)) return;

            // 若之前没有设置过连续天数 或 连续天数变小（10天->5天），从入住信息中查询当前在店的（排除掉已经在连续入住人员列表中），汇总大于等于5天的人，添加到连续入住列表中
            if (Objects.isNull(oldConsecutiveDays) || newConsecutiveDays < oldConsecutiveDays) {
                // 已经存在的连续入住人员
                List<GuestNameBO> names = list(propertyId).stream()
                        .flatMap(item -> Lists.newArrayList(new GuestNameBO(item.getFirstName(), item.getLastName()),
                                new GuestNameBO(item.getLastName(), item.getFirstName())).stream()).collect(Collectors.toList());
                // 当前在店人员（不包含已删除的数据）
                List<PastReservationOpGuestinHouse> guestInHousesRecords = pastReservationOpGuestinHouseService.listInHouseUser(propertyDTO, names);
                if (CollectionUtils.isEmpty(guestInHousesRecords)) return;

                // 查询连续入住标识对应的入住记录（排除掉已经查询过的入住记录 id、已经删除的数据）
                guestInHousesRecords.addAll(pastReservationOpGuestinHouseService.listByGroupKey(propertyId,
                        guestInHousesRecords.stream().map(PastReservationOpGuestinHouse::getGroupKey).distinct().collect(Collectors.toList()),
                        guestInHousesRecords.stream().map(PastReservationOpGuestinHouse::getId).collect(Collectors.toList())));

                // 汇总入住天数大于等于5天的人，添加到连续入住列表中
                long now = DateTimeUtils.nowTimestamp();
                String timezone = propertyDTO.getTimezone();
                List<PastReservation> addPastReservations = Lists.newArrayListWithExpectedSize(guestInHousesRecords.size());
                guestInHousesRecords.stream()
                        .collect(Collectors.groupingBy(PastReservationOpGuestinHouse::getGroupKey,
                                Collectors.collectingAndThen(Collectors.toList(), list -> list.stream()
                                        .sorted(Comparator.comparing(PastReservationOpGuestinHouse::getCheckInDate))
                                        .collect(Collectors.toList()))))
                        .forEach((groupKey, inHouseRecords) -> {
                            // 统计用户入住天数
                            int totalDays = statisticalInHouseDay(inHouseRecords, timezone);
                            if (totalDays >= newConsecutiveDays) {
                                // 逗号拼接房间号
                                String roomNumberStr = inHouseRecords.stream().map(PastReservationOpGuestinHouse::getRoomNumber)
                                        .filter(StringUtils::isNotEmpty).distinct().collect(Collectors.joining(","));
                                // 获取入住信息数据id
                                String idStr = inHouseRecords.stream().map(item -> item.getId().toString()).collect(Collectors.joining(","));

                                PastReservationOpGuestinHouse opGuestinHouse = inHouseRecords.get(0);

                                // 若入住记录中有忽略的，则置置忽略状态
                                int status = PastReservationStatusEnum.NORMAL_DISPLAY.getCode();
                                Integer statusSetBy;
                                Long statusSetTime;
                                Optional<PastReservationOpGuestinHouse> optional = inHouseRecords.stream()
                                        .filter(item -> item.getStatus() == PastReservationStatusEnum.IGNORE.getCode()).findFirst();
                                if (optional.isPresent()) {
                                    status = PastReservationStatusEnum.IGNORE.getCode();
                                    PastReservationOpGuestinHouse ignoreRecord = optional.get();
                                    statusSetBy = ignoreRecord.getStatusSetBy();
                                    statusSetTime = ignoreRecord.getStatusSetTime();
                                } else {
                                    statusSetTime = now;
                                    statusSetBy = userId;
                                }

                                addPastReservations.add(new PastReservation().setPropertyId(propertyId)
                                        .setFirstName(opGuestinHouse.getFirstName())
                                        .setLastName(opGuestinHouse.getLastName())
                                        .setFullName(opGuestinHouse.getFullName())
                                        .setRoomNumber(roomNumberStr).setDays(totalDays)
                                        .setMatchedIds(idStr).setCreatedBy(userId).setCreatedTime(now)
                                        .setStatus(status).setStatusSetBy(statusSetBy).setStatusSetTime(statusSetTime));
                            }
                        });

                if (CollectionUtils.isNotEmpty(addPastReservations)) {
                    saveBatch(addPastReservations);
                }
            } else {
                // 若连续天数变大（5天->10天），从连续入住列表清除掉（标记已删除）小于10天的数据
                update(new UpdateWrapper<PastReservation>().lambda()
                        .set(PastReservation::getDeleted, true)
                        .set(PastReservation::getDeletedBy, userId)
                        .set(PastReservation::getDeletedTime, DateTimeUtils.nowTimestamp())
                        .eq(PastReservation::getPropertyId, propertyId)
                        .eq(PastReservation::getDeleted, false)
                        .lt(PastReservation::getDays, newConsecutiveDays));
            }
        }

        /**
         * 修改成功
         *
         * @param logs 操作记录
         * @author yyt
         * @date 2024/10/3
         **/
        private void updateSuccess(List<PastReservationDayLog> logs) {
            // 事务提交
            TransactionUtils.afterCommitSyncExecute(() ->
                    pastReservationDayLogService.update(new UpdateWrapper<PastReservationDayLog>()
                    .lambda().set(PastReservationDayLog::getStatus, StatusCons.IMPORT_SUCCESS)
                    .in(PastReservationDayLog::getId, logs.stream().map(PastReservationDayLog::getId).collect(Collectors.toList()))));
        }

        /**
         * 修改失败
         *
         * @param logs 操作记录
         * @author yyt
         * @date 2024/10/3
         **/
        private void updateFail(List<PastReservationDayLog> logs) {
            // 事务回滚
            TransactionUtils.afterCompletionRollbackSyncExecute(() ->
                    pastReservationDayLogService.update(new UpdateWrapper<PastReservationDayLog>()
                    .lambda().set(PastReservationDayLog::getStatus, StatusCons.IMPORT_FAIL)
                    .in(PastReservationDayLog::getId, logs.stream().map(PastReservationDayLog::getId).collect(Collectors.toList()))));
        }

        /**
         * 检查名称重复
         *
         * @param propertyId 资产 id
         * @param firstName  first name
         * @param lastName   last name
         * @param excludeId  排除自身
         * @return true/false
         * @author yyt
         * @date 2024/10/15
         **/
        @Override
        public boolean checkRepeat(Integer propertyId, String firstName, String lastName, Long excludeId) {
            long count = count(new QueryWrapper<PastReservation>().lambda()
                    .eq(PastReservation::getPropertyId, propertyId)
                    .nested(sql ->
                            sql.eq(PastReservation::getFirstName, firstName).eq(PastReservation::getLastName, lastName)
                                    .or()
                                    .eq(PastReservation::getFirstName, lastName).eq(PastReservation::getLastName, firstName)
                    )
                    .eq(PastReservation::getDeleted, false)
                    .ne(Objects.nonNull(excludeId), PastReservation::getId, excludeId));
            return count > BaseConstant.ZERO;
        }

        /**
         * 新增连续入住人员信息
         *
         * @param propertyId 资产 id
         * @param dto        入住信息
         * @param userId     操作人
         * @author yyt
         * @date 2024/10/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public PastReservation save(Integer propertyId, SaveCheckInRecordDTO dto, int userId) {
            // 保存入住记录
            pastReservationOpGuestinHouseService.saveInHouseRecord(propertyId, dto, userId);

            // 计算入住天数
            computeInHouseDay(propertyId, userId, new GuestNameBO(dto.getFirstName(), dto.getLastName()));

            return getOne(propertyId, dto.getFirstName(), dto.getLastName());
        }

        /**
         * 计算入住天数
         *
         * @param propertyId 资产 id
         * @param userId     操作人
         * @param guestName  客户姓名
         * @author yyt
         * @date 2024/10/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void computeInHouseDay(Integer propertyId, int userId, @Nullable GuestNameBO guestName) {
            List<PastReservation> existPastReservations = listByUsers(propertyId, guestName);

            // 查询用户当前在店的入住记录（不包含已删除的数据）
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            List<PastReservationOpGuestinHouse> opGuestInHouses = pastReservationOpGuestinHouseService.listInHouseUserForTargetUser(propertyDTO, guestName);
            if (opGuestInHouses.isEmpty()) {
                if (Objects.nonNull(existPastReservations)) {
                    updateBatchById(existPastReservations.stream()
                            .map(item -> new PastReservation().setId(item.getId()).setDeleted(true).setDeletedBy(userId)
                                    .setDeletedTime(DateTimeUtils.nowTimestamp()))
                            .collect(Collectors.toList()));
                }

                return;
            }

            // 查询连续入住标识对应的入住记录（排除掉已经查询过的入住记录 id、已经删除的数据）
            opGuestInHouses.addAll(pastReservationOpGuestinHouseService.listByGroupKey(propertyId,
                    opGuestInHouses.stream().map(PastReservationOpGuestinHouse::getGroupKey).distinct().collect(Collectors.toList()),
                    opGuestInHouses.stream().map(PastReservationOpGuestinHouse::getId).collect(Collectors.toList())));

            // 连续入住天数
            Integer consecutiveDay = pastReservationConsecutiveDayService.listByPropertyIdForUpdate(propertyId)
                    .stream().filter(item -> Objects.nonNull(item.getDay())).min(Comparator.comparing(PastReservationConsecutiveDay::getDay))
                    .get().getDay();

            long now = DateTimeUtils.nowTimestamp();
            int size = opGuestInHouses.size();
            // 需要新增、修改的连续入住信息
            List<PastReservation> addPastReservations = Lists.newArrayListWithExpectedSize(size);
            List<PastReservation> updatePastReservations = Lists.newArrayListWithExpectedSize(size);

            Map<String, PastReservation> existPastReservationByGuestName = existPastReservations.stream().collect(Collectors.toMap(PastReservation::getGuestName, v -> v, (v1, v2) -> v2));
            Map<String, PastReservation> existPastReservationByGuestNameReversal = existPastReservations.stream().collect(Collectors.toMap(PastReservation::getGuestNameReversal, v -> v, (v1, v2) -> v2));

            opGuestInHouses.stream().collect(Collectors.groupingBy(PastReservationOpGuestinHouse::getGroupKey,
                            Collectors.collectingAndThen(Collectors.toList(),
                                    item -> item.stream().sorted(Comparator.comparing(PastReservationOpGuestinHouse::getCheckInDate))
                                            .collect(Collectors.toList()))))
                    .forEach((groupKey, inHouseRecords) -> {
                        PastReservationOpGuestinHouse opGuestinHouse = inHouseRecords.get(0);
                        String name = opGuestinHouse.getGuestName();
                        PastReservation existPastReservation = existPastReservationByGuestName.get(name);
                        if (Objects.isNull(existPastReservation)) {
                            existPastReservation = existPastReservationByGuestNameReversal.get(name);
                        }

                        // 统计用户入住天数
                        int totalDays = statisticalInHouseDay(inHouseRecords, propertyDTO.getTimezone());
                        if (totalDays >= consecutiveDay) {
                            // 逗号拼接房间号
                            String roomNumberStr = inHouseRecords.stream().map(PastReservationOpGuestinHouse::getRoomNumber)
                                    .filter(StringUtils::isNotEmpty).distinct().collect(Collectors.joining(","));
                            // 获取入住信息数据id
                            String idStr = inHouseRecords.stream().map(item -> item.getId().toString()).collect(Collectors.joining(","));

                            // 若入住记录中有忽略的，则置置忽略状态
                            int status = PastReservationStatusEnum.NORMAL_DISPLAY.getCode();
                            Integer statusSetBy;
                            Long statusSetTime;
                            Optional<PastReservationOpGuestinHouse> optional = inHouseRecords.stream()
                                    .filter(item -> item.getStatus() == PastReservationStatusEnum.IGNORE.getCode()).findFirst();
                            if (optional.isPresent()) {
                                status = PastReservationStatusEnum.IGNORE.getCode();
                                PastReservationOpGuestinHouse ignoreRecord = optional.get();
                                statusSetBy = ignoreRecord.getStatusSetBy();
                                statusSetTime = ignoreRecord.getStatusSetTime();
                            } else {
                                statusSetTime = now;
                                statusSetBy = userId;
                            }

                            // 校验当前用户是否已添加过
                            if (existPastReservation == null) {
                                // 添加此人
                                addPastReservations.add(new PastReservation().setPropertyId(propertyId)
                                        .setFirstName(opGuestinHouse.getFirstName())
                                        .setLastName(opGuestinHouse.getLastName())
                                        .setFullName(opGuestinHouse.getFullName())
                                        .setRoomNumber(roomNumberStr).setDays(totalDays)
                                        .setMatchedIds(idStr).setCreatedBy(userId).setCreatedTime(now)
                                        .setStatus(status).setStatusSetBy(statusSetBy).setStatusSetTime(statusSetTime));
                            } else {
                                // 更新当前入住信息的房间号、入住天数、离店天数、关联入住信息id
                                if (totalDays != existPastReservation.getDays() || !roomNumberStr.equals(existPastReservation.getRoomNumber())
                                        || !idStr.equals(existPastReservation.getMatchedIds()) || status != existPastReservation.getStatus()) {
                                    updatePastReservations.add(new PastReservation().setId(existPastReservation.getId())
                                            .setRoomNumber(roomNumberStr).setDays(totalDays).setMatchedIds(idStr)
                                            .setUpdatedBy(userId).setUpdatedTime(now)
                                            .setStatus(status).setStatusSetBy(statusSetBy).setStatusSetTime(statusSetTime));
                                }
                            }
                        } else {
                            // 入住天数不达标，删除连续入住人员
                            if (Objects.nonNull(existPastReservation)) {
                                updatePastReservations.add(new PastReservation().setId(existPastReservation.getId())
                                        .setDeleted(true).setDeletedBy(userId).setDeletedTime(DateTimeUtils.nowTimestamp()));
                            }
                        }

                        if (Objects.nonNull(existPastReservation)) {
                            // 清除当前用户信息，目的是结束循环后保留下离店人员
                            existPastReservations.remove(existPastReservation);
                        }
                    });

            if (CollectionUtils.isNotEmpty(addPastReservations)) {
                saveBatch(addPastReservations);
            }
            if (CollectionUtils.isNotEmpty(updatePastReservations)) {
                updateBatchById(updatePastReservations);
            }
            if (!existPastReservations.isEmpty()) {
                // 清除离店人员（标记已删除）
                update(new UpdateWrapper<PastReservation>().lambda()
                        .set(PastReservation::getDeleted, true)
                        .set(PastReservation::getDeletedBy, userId)
                        .set(PastReservation::getDeletedTime, DateTimeUtils.nowTimestamp())
                        .in(PastReservation::getId, existPastReservations.stream().map(PastReservation::getId).collect(Collectors.toList())));
            }
        }

        /**
         * 查询用户未删除的连续入住信息
         *
         * @param propertyId 资产 id
         * @param guestName 用户
         * @return 连续入住信息
         * @author yyt
         * @date 2024/10/15
         **/
        private List<PastReservation> listByUsers(Integer propertyId, @Nullable GuestNameBO guestName) {
            String firstName;
            String lastName;
            if (Objects.nonNull(guestName)) {
                firstName = guestName.getFirstName();
                lastName = guestName.getLastName();
            } else {
                lastName = null;
                firstName = null;
            }

            return list(new QueryWrapper<PastReservation>().lambda()
                    .eq(PastReservation::getPropertyId, propertyId)
                    .nested(Objects.nonNull(guestName) && Objects.nonNull(lastName), sql ->
                            sql.eq(PastReservation::getFirstName, firstName).eq(PastReservation::getLastName, lastName)
                                    .or()
                                    .eq(PastReservation::getFirstName, lastName).eq(PastReservation::getLastName, firstName))
                    .eq(PastReservation::getDeleted, false));
        }

        /**
         * 修改客户姓名
         *
         * @param pastReservation    连续入住信息
         * @param updateGuestNameDTO 客户姓名
         * @param userId             操作人
         * @author yyt
         * @date 2024/10/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateGuestName(PastReservation pastReservation, UpdateGuestNameDTO updateGuestNameDTO, int userId) {
            String oldFullName = pastReservation.getFullName();
            long now = DateTimeUtils.nowTimestamp();
            String firstName = updateGuestNameDTO.getFirstName();
            String lastName = updateGuestNameDTO.getLastName();
            String fullName = new GuestNameBO(firstName, lastName).getFullName();
            updateById(new PastReservation().setId(pastReservation.getId()).setFirstName(firstName).setLastName(lastName)
                    .setFullName(fullName).setUpdatedBy(userId).setUpdatedTime(now));

            pastReservationOpGuestinHouseService.update(new UpdateWrapper<PastReservationOpGuestinHouse>()
                    .lambda().set(PastReservationOpGuestinHouse::getFirstName, firstName)
                    .set(PastReservationOpGuestinHouse::getLastName, lastName)
                    .set(PastReservationOpGuestinHouse::getFullName, fullName)
                    .set(PastReservationOpGuestinHouse::getUpdatedBy, userId)
                    .set(PastReservationOpGuestinHouse::getUpdatedTime, now)
                    .in(PastReservationOpGuestinHouse::getId, Arrays.stream(pastReservation.getMatchedIds().split(",")).toList())
                    .eq(PastReservationOpGuestinHouse::getDeleted, false));

            pastReservationCreditLimitService.update(new UpdateWrapper<PastReservationCreditLimit>()
                    .lambda().set(PastReservationCreditLimit::getFullName, fullName)
                    .eq(PastReservationCreditLimit::getPropertyId, pastReservation.getPropertyId())
                    .eq(PastReservationCreditLimit::getFullName, oldFullName));
        }

        /**
         * 保存入住记录
         *
         * @param pastReservation 连续入住信息
         * @param dto             入住记录
         * @param userId          操作人
         * @return ShowRulesDTO
         * @author yyt
         * @date 2024/10/16
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public ShowRulesDTO saveCheckInRecord(PastReservation pastReservation, SaveCheckInRecordDTO dto, int userId) {
            Integer propertyId = pastReservation.getPropertyId();
            Long newRecordId = pastReservationOpGuestinHouseService.saveInHouseRecord(propertyId, dto, userId);

            computeInHouseDay(propertyId, userId, new GuestNameBO(dto.getFirstName(), dto.getLastName()));

            pastReservation = getById(pastReservation.getId());
            List<Long> checkInHouseRecordsIds = Arrays.stream(pastReservation.getMatchedIds().split(",")).map(Long::parseLong).toList();
            boolean showCheckInRecord = checkInHouseRecordsIds.contains(newRecordId);
            if (!showCheckInRecord) {
                showCheckInRecord = pastReservationOpGuestinHouseService.checkCheckInRecordShow(pastReservation, dto.getRoomNumber(), dto.getCheckInDate(), dto.getCheckOutDate());
            }

            return new ShowRulesDTO(showCheckInRecord, true);
        }

        /**
         * 修改入住记录
         *
         * @param pastReservation 连续入住信息
         * @param cid             入住记录 id
         * @param dto             入住记录
         * @param userId          操作人
         * @return ShowRulesDTO
         * @author yyt
         * @date 2024/10/30
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public ShowRulesDTO updateCheckInRecord(PastReservation pastReservation, long cid, SaveCheckInRecordDTO dto, int userId) {
            pastReservationOpGuestinHouseService.updateInHouseRecord(pastReservation, cid, dto, userId);

            computeInHouseDay(pastReservation.getPropertyId(), userId, new GuestNameBO(dto.getFirstName(), dto.getLastName()));

            pastReservation = getById(pastReservation.getId());

            boolean showCheckInRecord = !pastReservation.getDeleted() && (Arrays.stream(pastReservation.getMatchedIds().split(","))
                    .map(Long::parseLong).toList().contains(cid) || pastReservationOpGuestinHouseService.checkCheckInRecordShow(pastReservation, dto.getRoomNumber(), dto.getCheckInDate(), dto.getCheckOutDate()));
            return new ShowRulesDTO(showCheckInRecord, !pastReservation.getDeleted());
        }

        /**
         * 删除入住记录
         *
         * @param pastReservation 连续入住信息
         * @param opGuestinHouse  入住记录
         * @param userId          操作人
         * @return 客户是否显示
         * @author yyt
         * @date 2024/10/16
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public boolean deleteCheckInRecord(PastReservation pastReservation, PastReservationOpGuestinHouse opGuestinHouse, int userId) {
            pastReservationOpGuestinHouseService.deleteCheckInRecord(opGuestinHouse, userId);

            computeInHouseDay(pastReservation.getPropertyId(), userId, new GuestNameBO(opGuestinHouse.getFirstName(), opGuestinHouse.getLastName()));

            return !getById(pastReservation.getId()).getDeleted();
        }

        /**
         * 连续入住列表
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return 连续入住列表
         * @author yyt
         * @date 2024/10/28
         **/
        @Override
        public ListPagedDTO<PastReservationDTO> listPage(PropertyDTO propertyDTO, PastReservationQuery query) {
            Integer propertyId = propertyDTO.getId();
            Page<PastReservation> page = page(new Page<>(query.getPage(), query.getSize()), new QueryWrapper<PastReservation>().lambda()
                    .eq(PastReservation::getPropertyId, propertyId)
                    .eq(PastReservation::getDeleted, false)
                    .ge(Objects.nonNull(query.getMin()), PastReservation::getDays, query.getMin())
                    .lt(Objects.nonNull(query.getMax()), PastReservation::getDays, query.getMax())
                    .like(StringUtils.isNotEmpty(query.getFirstName()), PastReservation::getFirstName, query.getFirstName())
                    .like(StringUtils.isNotEmpty(query.getLastName()), PastReservation::getLastName, query.getLastName())
                    .apply(StringUtils.isNotEmpty(query.getRoomNumber()), "find_in_set({0}, room_number)", query.getRoomNumber())
                    .eq(Objects.nonNull(query.getStatus()), PastReservation::getStatus, query.getStatus())
                    .orderByDesc(PastReservation::getId));

            List<PastReservation> records = page.getRecords();
            if (CollectionUtils.isEmpty(records)) return new ListPagedDTO<>();

            List<Long> ids = records.stream().map(PastReservation::getId).toList();
            Map<Long, Integer> commentCountByPastReservationId = pastReservationCommentService.listByPastReservationId(ids)
                    .stream().collect(Collectors.groupingBy(PastReservationComment::getPtRsvId,
                            Collectors.collectingAndThen(Collectors.toList(), List::size)));

            Set<Integer> userIds = records.stream().map(PastReservation::getStatusSetBy).filter(item -> item > BaseConstant.ZERO).collect(Collectors.toSet());
            Map<Integer, String> nameById = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);

            List<String> fullNames = records.stream().map(PastReservation::getFullName).toList();

            // credit limit
            Map<String, Map<String, PastReservationCreditLimit>> creditLimitInfosByFullName = pastReservationCreditLimitService.list(propertyId, fullNames)
                    .stream().collect(Collectors.groupingBy(item-> item.getFullName().toLowerCase(),
                            Collectors.toMap(PastReservationCreditLimit::getRoomNumber, v -> v)));

            return new ListPagedDTO<>(page.getTotal(), beanMapper.mapList(records, PastReservationDTO.class,
                    item -> {
                        item.setCommentCount(commentCountByPastReservationId.getOrDefault(item.getId(), BaseConstant.ZERO));
                        item.setStatusSetName(nameById.get(item.getStatusSetBy()));

                        String fullName = item.getFullName().toLowerCase();
                        item.setPaymentInfo(packagePaymentInfoDTO(creditLimitInfosByFullName.getOrDefault(fullName, Collections.emptyMap())));
                    }));

        }

        /**
         * 封装 PaymentInfoDTO
         *
         * @param creditLimitByRoomNumber credLimit
         * @return PaymentInfoDTO
         * @author yyt
         * @date 2024/12/28
         **/
        private List<PaymentInfoDTO> packagePaymentInfoDTO(Map<String, PastReservationCreditLimit> creditLimitByRoomNumber) {
            if (MapUtils.isEmpty(creditLimitByRoomNumber)) {
                return Collections.emptyList();
            }

            return creditLimitByRoomNumber.keySet().stream()
                    .map(roomNumber -> {
                        PastReservationCreditLimit creditLimit = creditLimitByRoomNumber.getOrDefault(roomNumber, new PastReservationCreditLimit());
                        return PaymentInfoDTO.builder()
                                .roomNumber(roomNumber)
                                .totalBalance(creditLimit.getTotalBalance())
                                .totalApprovalAmount(creditLimit.getTotalApprovalAmount())
                                .build();
                    }).collect(Collectors.toList());
        }

        /**
         * 查询指定用户
         *
         * @param propertyId 资产 id
         * @param firstName  first name
         * @param lastName   last name
         * @return 指定用户
         * @author yyt
         * @date 2024/10/30
         **/
        @Override
        public PastReservation getOne(Integer propertyId, String firstName, String lastName) {
            return getOne(new QueryWrapper<PastReservation>().lambda().eq(PastReservation::getPropertyId, propertyId)
                    .eq(PastReservation::getDeleted, false)
                    .nested(sql ->
                            sql.eq(PastReservation::getFirstName, firstName).eq(PastReservation::getLastName, lastName)
                                    .or()
                                    .eq(PastReservation::getFirstName, lastName).eq(PastReservation::getLastName, firstName)
                    ));
        }

        /**
         * 忽略 / 撤回
         *
         * @param pastReservation 连续入住人
         * @param status          状态
         * @param userId          操作人
         * @author yyt
         * @date 2024/11/7
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void status(PastReservation pastReservation, int status, int userId) {
            long now = DateTimeUtils.nowTimestamp();
            updateById(new PastReservation().setId(pastReservation.getId()).setStatus(status).setStatusSetBy(userId)
                    .setStatusSetTime(now));

            pastReservationOpGuestinHouseService.update(new UpdateWrapper<PastReservationOpGuestinHouse>()
                    .lambda().set(PastReservationOpGuestinHouse::getStatus, status)
                    .set(PastReservationOpGuestinHouse::getStatusSetBy, userId)
                    .set(PastReservationOpGuestinHouse::getStatusSetTime, now)
                    .in(PastReservationOpGuestinHouse::getId, Arrays.stream(pastReservation.getMatchedIds().split(",")).toList())
                    .eq(PastReservationOpGuestinHouse::getDeleted, false));
        }

        /**
         * 获取 PastReservationDetailDTO
         *
         * @param pastReservation pastReservation
         * @return PastReservationDetailDTO
         * @author yyt
         * @date 2024/12/28
         **/
        @Override
        public PastReservationDetailDTO getPastReservationDetailDTO(PastReservation pastReservation) {
            PastReservationDetailDTO detailDTO = beanMapper.map(pastReservation, PastReservationDetailDTO.class);
            List<PastReservationOpGuestinHouse> opGuestInHouses =
                    pastReservationOpGuestinHouseService.listByIds(Arrays.asList(pastReservation.getMatchedIds().split(",")))
                            .stream().filter(item -> !item.getDeleted())
                            .sorted(Comparator.comparing(PastReservationOpGuestinHouse::getCheckInDate)
                                    .thenComparing(PastReservationOpGuestinHouse::getCheckOutDate).reversed())
                            .toList();

            Integer propertyId = pastReservation.getPropertyId();
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            long currentDate = DateTimeUtils.dateNowTimestamp(timezone);
            List<String> fullNames = Lists.newArrayList(pastReservation.getFullName());
            // credit limit
            Map<String, PastReservationCreditLimit> creditLimitByRoomNumber = pastReservationCreditLimitService.list(propertyId, fullNames)
                    .stream().collect(Collectors.toMap(PastReservationCreditLimit::getRoomNumber, v -> v));

            detailDTO.setCheckInRecords(beanMapper.mapList(opGuestInHouses, PastReservationCheckInRecordDTO.class,
                    dto -> {
                        // 找到当前入住记录，设置对应房间号的 款项信息
                        if (dto.getCheckInDate() <= currentDate && dto.getCheckOutDate() >= currentDate) {
                            PastReservationCreditLimit creditLimit = creditLimitByRoomNumber.getOrDefault(dto.getRoomNumber(), new PastReservationCreditLimit());
                            dto.setTotalBalance(creditLimit.getTotalBalance());
                            dto.setTotalApprovalAmount(creditLimit.getTotalApprovalAmount());
                        }
                    }));
            return detailDTO;
        }

        /**
         * 统计连续入住
         *
         * @param propertyId 资产 id
         * @return 连续入住统计信息
         * @author yyt
         * @date 2026/1/19
         **/
        @Override
        public PastReservationStatisticsDTO statistics(Integer propertyId) {
            List<PastReservation> pastReservations = list(propertyId);
            if (CollectionUtils.isEmpty(pastReservations))
                return new PastReservationStatisticsDTO(BaseConstant.ZERO, BaseConstant.ZERO, BaseConstant.ZERO);

            return statisticsCount(propertyId, pastReservations);
        }

        /**
         * 统计连续入住数量
         *
         * @param propertyId       资产 id
         * @param pastReservations 连续入住信息
         * @author yyt
         * @date 2024/10/2
         **/
        private PastReservationStatisticsDTO statisticsCount(Integer propertyId, List<PastReservation> pastReservations) {
            List<PastReservationConsecutiveDay> consecutiveDays = pastReservationConsecutiveDayService.listByPropertyId(propertyId);
            consecutiveDays = consecutiveDays.stream().filter(item -> Objects.nonNull(item.getDay()))
                    .sorted(Comparator.comparing(PastReservationConsecutiveDay::getDay)).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(consecutiveDays)) return new PastReservationStatisticsDTO(BaseConstant.ZERO, BaseConstant.ZERO, BaseConstant.ZERO);

            List<Map<String, Integer>> rangeList = Lists.newArrayListWithExpectedSize(consecutiveDays.size());
            for (int j = 0; j < consecutiveDays.size(); j++) {
                if (j + 1 == consecutiveDays.size()) {
                    //区间集合
                    HashMap<String, Integer> rangeMap = new HashMap<>();
                    rangeMap.put("min", consecutiveDays.get(j).getDay());
                    rangeMap.put("max", 10000);
                    rangeList.add(rangeMap);
                } else {
                    //区间集合
                    HashMap<String, Integer> rangeMap = new HashMap<>();
                    rangeMap.put("min", consecutiveDays.get(j).getDay());
                    rangeMap.put("max", consecutiveDays.get(j + 1).getDay());
                    rangeList.add(rangeMap);
                }
            }

            // key：配置的连续入住天数 value：天数对应的数据和
            HashMap<Integer, Integer> consecutiveDays2Count = new HashMap<>();

            for (PastReservation item : pastReservations) {
                for (Map<String, Integer> map : rangeList) {
                    Integer min = map.get("min");
                    Integer max = map.get("max");
                    if (item.getDays() >= min && item.getDays() < max) {
                        Integer count = consecutiveDays2Count.get(min);
                        if (Objects.isNull(count)) {
                            consecutiveDays2Count.put(min, 1);
                        } else {
                            consecutiveDays2Count.put(min, count + 1);
                        }
                    }
                }
            }

            Map<Integer, Integer> dayByType = consecutiveDays.stream().collect(Collectors.toMap(PastReservationConsecutiveDay::getType,
                    PastReservationConsecutiveDay::getDay));
            return new PastReservationStatisticsDTO(consecutiveDays2Count.get(dayByType.get(DayTypeEnum.NOTICE.getCode())),
                    consecutiveDays2Count.get(dayByType.get(DayTypeEnum.ALERT.getCode())),
                    consecutiveDays2Count.get(dayByType.get(DayTypeEnum.WARNING.getCode())));
        }

    }