# CompRoomComplimentaryServiceImpl.java

    package cloud.wconnect.frontoffice.service.comproom.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary;
    import cloud.wconnect.frontoffice.entity.comproom.RqCompRoomForm;
    import cloud.wconnect.frontoffice.mapper.comproom.CompRoomComplimentaryMapper;
    import cloud.wconnect.frontoffice.mapper.comproom.RqCompRoomFormMapper;
    import cloud.wconnect.frontoffice.pojo.bo.comproom.GuestsInHouseComplimentaryBO;
    import cloud.wconnect.frontoffice.pojo.query.comproom.CompRoomComplimentaryQuery;
    import cloud.wconnect.frontoffice.service.comproom.ICompRoomComplimentaryService;
    import cloud.wconnect.frontoffice.service.comproom.IRqCompRoomFormService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    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 lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import jakarta.annotation.Nullable;

    import java.text.MessageFormat;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * comp room功能，存储Guests In-House Complimentary报告中的数据 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2023-08-05
     */
    @Slf4j
    @Service
    @DS("logbook")
    public class CompRoomComplimentaryServiceImpl extends ServiceImpl<CompRoomComplimentaryMapper, CompRoomComplimentary> implements ICompRoomComplimentaryService {

        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        @Lazy
        private IRqCompRoomFormService rqCompRoomFormService;
        @Autowired
        private RqCompRoomFormMapper compRoomFormMapper;

        /**
         * 处理免费房入住信息
         *
         * @param propertyDTO 资产dto
         * @param boList      每日的免费房入住信息
         * @param theDate     报告日期（yyyyMMdd）
         * @author lichen
         * @date 2023/8/5
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleGuestsInHouseComplimentaryData(PropertyDTO propertyDTO, List<GuestsInHouseComplimentaryBO> boList, Long theDate) {
            if (CollectionUtils.isEmpty(boList)){
                // 提前离店
                this.earlyCheckOut(propertyDTO, Collections.emptyList());
                return;
            }

            List<CompRoomComplimentary> reportList = convertTo(propertyDTO, boList, theDate);

            Set<Long> checkInDates = reportList.stream().map(CompRoomComplimentary::getCheckInDate).collect(Collectors.toSet());
            Set<String> roomNumbers = reportList.stream().map(CompRoomComplimentary::getRoomNumber).collect(Collectors.toSet());

            // 根据资产id+入住日期+房间号查询已经存在的免费房数据
            List<CompRoomComplimentary> existDataList = listByPropertyIdAndCheckInDatesAndRoomNumbers(propertyDTO.getId(), checkInDates, roomNumbers);
            Map<String, CompRoomComplimentary> uniqueKey2ExistData = existDataList.stream().collect(Collectors.toMap(CompRoomComplimentary::generateKey, Function.identity()));

            List<CompRoomComplimentary> insertList = Lists.newArrayListWithCapacity(reportList.size());
            List<CompRoomComplimentary> updateList = Lists.newArrayListWithCapacity(reportList.size());
            // 待关联的报告数据
            List<CompRoomComplimentary> waitAssociated = Lists.newArrayListWithCapacity(reportList.size());

            for (CompRoomComplimentary reportData : reportList) {
                CompRoomComplimentary existData = uniqueKey2ExistData.get(reportData.generateKey());
                if (Objects.nonNull(existData)) {
                    // IWC中存在目标数据时，执行修改
                    reportData.setId(existData.getId());
                    reportData.setRelId(existData.getRelId());
                    updateList.add(reportData);
                } else {
                    // IWC中不存在目标数据时，执行新增
                    // 新增的数据 设置为未关联
                    reportData.setRelId(0L);
                    insertList.add(reportData);
                }
            }

            // 执行新增
            saveBatch(insertList);
            waitAssociated.addAll(insertList);

            // 执行修改
            updateBatchById(updateList);
            // 修改的数据集合中筛选出未关联的数据
            waitAssociated.addAll(updateList
                    .stream()
                    .filter(item -> item.getRelId() == 0)
                    .toList());

            // 提前离店
            this.earlyCheckOut(propertyDTO, reportList);

            // 报告数据与IWC手动创建的数据关联
            if (CollectionUtils.isNotEmpty(waitAssociated)) {
                doAssociated(waitAssociated, propertyDTO);
            }
        }

        /**
         * 提前离店
         *
         * @param propertyDTO 资产DTO
         * @param reportList  报告数据集合
         * @author lichen
         * @date 2025/12/19
         */
        private void earlyCheckOut(PropertyDTO propertyDTO, List<CompRoomComplimentary> reportList) {
            // 查询在住的数据
            List<CompRoomComplimentary> inHouseList = listInHouse(propertyDTO);
            if (CollectionUtils.isNotEmpty(inHouseList)) {
                ArrayList<CompRoomComplimentary> earlyCheckOutList = Lists.newArrayListWithExpectedSize(inHouseList.size());

                // 当前日期前一天
                long beforeDay = DateTimeUtils.dateNow(propertyDTO.getTimezone()).minusDays(1).toEpochSecond();

                // 报告中的数据集合
                List<String> uniqueKeyByReportData = reportList.stream().map(CompRoomComplimentary::generateKey).toList();
                for (CompRoomComplimentary inHouseData : inHouseList) {
                    String key = inHouseData.generateKey();
                    // 报告中没有在住数据时，提前离店
                    if (!uniqueKeyByReportData.contains(key)) {
                        CompRoomComplimentary earlyCheckOut = new CompRoomComplimentary();
                        earlyCheckOut.setId(inHouseData.getId());
                        earlyCheckOut.setCheckOutDate(beforeDay);
                        earlyCheckOutList.add(earlyCheckOut);
                    }
                }

                // 更新为提前离店
                updateBatchById(earlyCheckOutList);
            }
        }

        /**
         * 转换数据
         *
         * @param propertyDTO 资产DTO
         * @param boList      报告BO
         * @param theDate     报告日期（yyyyMMdd）
         * @return java.util.List<cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary>
         * @author lichen
         * @date 2025/12/19
         */
        private List<CompRoomComplimentary> convertTo(PropertyDTO propertyDTO, List<GuestsInHouseComplimentaryBO> boList, Long theDate) {
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            String timezone = propertyDTO.getTimezone();

            List<CompRoomComplimentary> reportList = beanMapper.mapList(boList, CompRoomComplimentary.class, (bo, entity) -> {
                entity.setPropertyId(propertyDTO.getId());
                entity.setCheckInDate(DateTimeUtils.dateStrToEpochSecond(bo.getArrivalDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone));
                entity.setCheckOutDate(DateTimeUtils.dateStrToEpochSecond(bo.getDepartureDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone));
                entity.setTheDate(DateTimeUtils.dateStrToEpochSecond(theDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD, timezone));
                entity.setSyncTime(nowTimestamp);
                entity.setComments(JsonUtils.convertObjListToString(objectMapper, bo.getCommentList()));
            });

            // 去重
            return reportList.stream()
                    .collect(Collectors.collectingAndThen(
                            Collectors.toCollection(
                                    () -> new TreeSet<>(Comparator.comparing(CompRoomComplimentary::generateKey))),
                            ArrayList::new));
        }

        /**
         * 待关联的报告数据执行关联
         * 1. 先根据房间号+入住日期关联w-connect数据 2. 再根据客户名称+入住日期关联w-connect数据
         *
         * @param complimentaryList 报告数据集合
         * @param propertyDTO       资产DTO
         * @return void
         * @author lichen
         * @date 2024/1/24
         */
        private void doAssociated(List<CompRoomComplimentary> complimentaryList, PropertyDTO propertyDTO) {
            //根据房间号+入住日期关联w-connect数据
            List<Long> alreadyAssociatedReportIdsByRoomNumber = doAssociatedByRoomNumber(complimentaryList, propertyDTO);

            //未关联的数据
            List<CompRoomComplimentary> unAssociatedByRoomNumber = complimentaryList.stream()
                    .filter(item -> !alreadyAssociatedReportIdsByRoomNumber.contains(item.getId())).toList();

            //再根据客户名称+入住日期关联w-connect数据
            doAssociatedByGuestName(unAssociatedByRoomNumber, propertyDTO);
        }

        /**
         * 根据客户名称+入住日期关联w-connect数据
         *
         * @param complimentaryList 报告数据集合
         * @param propertyDTO       资产DTO
         * @return void
         * @author lichen
         * @date 2024/1/4
         */
        private void doAssociatedByGuestName(List<CompRoomComplimentary> complimentaryList, PropertyDTO propertyDTO) {
            if (CollectionUtils.isEmpty(complimentaryList)) return;

            List<RqCompRoomForm> conditionList = Lists.newArrayListWithCapacity(complimentaryList.size());
            for (CompRoomComplimentary compRoomComplimentary : complimentaryList) {
                Long checkInDate = compRoomComplimentary.getCheckInDate();
                //获取不同顺序的客人姓名（姓，名 或 名，姓）
                List<String> differentOrderGuestNames = getDifferentOrderGuestName(compRoomComplimentary.getGuestName());

                List<RqCompRoomForm> compRoomForms = differentOrderGuestNames.stream()
                        .map(guestName -> new RqCompRoomForm().setCheckInDate(checkInDate).setGuestName(guestName))
                        .toList();
                conditionList.addAll(compRoomForms);
            }

            /**
             * 根据资产id+客人姓名+入住日期查询w-connect维护的comp room数据,并根据checkInDate+GuestName去重
             * 去重原因：如果重名客户同一天入住免费房，则用其中一条处理关联逻辑，这样做的好处是可以让系统使用者更清晰的看到有问题的关联情况
             */
            List<RqCompRoomForm> compRoomList = compRoomFormMapper.listByGuestNameAndCheckInDate(propertyDTO.getId(), conditionList)
                    .stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(
                            Comparator.comparing(RqCompRoomForm::generateKeyByCheckInDateAndGuestName))), ArrayList::new));

            if (CollectionUtils.isEmpty(compRoomList)) return;

            //需要修改的报告数据集合
            List<CompRoomComplimentary> updateCompRoomComplimentaryList = Lists.newArrayListWithCapacity(complimentaryList.size());
            //需要修改的w-connect数据集合
            List<RqCompRoomForm> updateCompRoomList = Lists.newArrayListWithCapacity(compRoomList.size());

            for (RqCompRoomForm compRoom : compRoomList) {
                //获取w-connect数据关联的报告id集合
                List<Long> relReportIds = Lists.newArrayList();
                if (StringUtils.isNotBlank(compRoom.getRelIds())) {
                    relReportIds = Arrays.stream(compRoom.getRelIds().split(","))
                            .map(Long::valueOf).collect(Collectors.toList());
                }

                //是否有关联
                boolean isHaveAssociated = false;

                for (CompRoomComplimentary compRoomComplimentary : complimentaryList) {
                    //判断是否已经关联w-connect数据
                    if (compRoomComplimentary.getRelId() > 0) continue;

                    //判断w-connect的数据是否和报告中的数据匹配
                    if (compareGuestName(compRoom.getGuestName(), compRoomComplimentary.getGuestName()) &&
                            compRoom.getCheckInDate().equals(compRoomComplimentary.getCheckInDate())) {
                        isHaveAssociated = true;

                        //报告数据设置关联的w-connect数据id
                        compRoomComplimentary.setRelId(compRoom.getId());
                        updateCompRoomComplimentaryList.add(compRoomComplimentary);

                        //把报告数据id添加到集合中
                        relReportIds.add(compRoomComplimentary.getId());
                    }
                }

                if (isHaveAssociated) {
                    //w-connect的数据设置为已关联
                    compRoom.setIsRel(true);
                    //w-connect的数据设置关联的报告id
                    compRoom.setRelIds(StringUtils.join(relReportIds.stream().distinct().toList(), ","));
                    updateCompRoomList.add(compRoom);
                }
            }

            rqCompRoomFormService.updateBatchById(updateCompRoomList);
            updateBatchById(updateCompRoomComplimentaryList);
        }

        /**
         * 获取不同顺序的客人姓名（姓，名 或 名，姓）
         *
         * @param guestName
         * @return java.util.List<java.lang.String>
         * @author lichen
         * @date 2024/1/6
         */
        private List<String> getDifferentOrderGuestName(String guestName) {
            List<String> separatedGuestNames = Lists.newArrayList(guestName.split(","));
            if (separatedGuestNames.size() < 2) {
                return Lists.newArrayList(guestName);
            } else {
                //反转客人姓名
                String reverseGuestName = separatedGuestNames.get(1).trim() + "," + separatedGuestNames.get(0).trim();
                return Lists.newArrayList(guestName, reverseGuestName);
            }
        }

        /**
         * 比较客人姓名
         *
         * @param compRoomGuestName w-connect手动录入的客人姓名
         * @param reportGuestName   报告中的客人姓名
         * @return boolean true:一样 false:不一样
         * @author lichen
         * @date 2024/1/5
         */
        private boolean compareGuestName(String compRoomGuestName, String reportGuestName) {
            //判断两个客人姓名是否相等
            if (compRoomGuestName.equals(reportGuestName)) return true;

            List<String> separatedGuestNames = Lists.newArrayList(compRoomGuestName.split(","));
            if (separatedGuestNames.size() < 2) return false;

            //判断反转后的客人姓名是否 和 报告中的一致
            String reverseCompRoomGuestName = separatedGuestNames.get(1).trim() + "," + separatedGuestNames.get(0).trim();
            return reverseCompRoomGuestName.equals(reportGuestName);
        }

        /**
         * 根据房间号+入住日期关联w-connect数据
         *
         * @param complimentaryList 报告数据
         * @param propertyDTO       资产DTO
         * @return java.util.List<java.lang.Long>
         * @author lichen
         * @date 2024/1/4
         */
        private List<Long> doAssociatedByRoomNumber(List<CompRoomComplimentary> complimentaryList, PropertyDTO propertyDTO) {
            if (CollectionUtils.isEmpty(complimentaryList)) return Collections.emptyList();

            Set<String> roomNumberSet = complimentaryList.stream().map(CompRoomComplimentary::getRoomNumber).collect(Collectors.toSet());
            Set<Long> checkInDateSet = complimentaryList.stream().map(CompRoomComplimentary::getCheckInDate).collect(Collectors.toSet());

            //根据资产id，房间号，入住日期 查询w-connect维护的数据
            List<RqCompRoomForm> compRoomList = rqCompRoomFormService.list(new LambdaQueryWrapper<RqCompRoomForm>()
                    .eq(RqCompRoomForm::getPropertyId, propertyDTO.getId())
                    .in(RqCompRoomForm::getCheckInDate, checkInDateSet)
                    .apply(String.format("CONCAT( ',', room_number , ',' ) REGEXP ',(%s),'",
                            StringUtils.join(roomNumberSet, "|"))));

            if (CollectionUtils.isEmpty(compRoomList)) return Collections.emptyList();

            //已经关联的报告数据id
            List<Long> alreadyAssociatedReportIds = Lists.newArrayListWithCapacity(complimentaryList.size());
            //需要修改的报告数据集合
            List<CompRoomComplimentary> updateCompRoomComplimentaryList = Lists.newArrayListWithCapacity(complimentaryList.size());
            //需要修改的w-connect数据集合
            List<RqCompRoomForm> updateCompRoomList = Lists.newArrayListWithCapacity(compRoomList.size());

            for (RqCompRoomForm compRoom : compRoomList) {
                //获取w-connect维护的房间号集合
                List<String> roomNumberList = Lists.newArrayList(compRoom.getRoomNumber().split(","));

                //获取w-connect数据关联的报告id集合
                List<Long> relReportIds = Lists.newArrayList();
                if (StringUtils.isNotBlank(compRoom.getRelIds())) {
                    relReportIds = Arrays.stream(compRoom.getRelIds().split(","))
                            .map(Long::valueOf).collect(Collectors.toList());
                }

                //是否有关联
                boolean isHaveAssociated = false;

                for (CompRoomComplimentary compRoomComplimentary : complimentaryList) {
                    //判断是否已经关联w-connect数据
                    if (compRoomComplimentary.getRelId() > 0) continue;

                    //判断w-connect的数据是否和报告中的数据匹配
                    if (roomNumberList.contains(compRoomComplimentary.getRoomNumber()) &&
                            compRoom.getCheckInDate().equals(compRoomComplimentary.getCheckInDate())) {

                        isHaveAssociated = true;

                        //报告数据设置关联的w-connect数据id
                        compRoomComplimentary.setRelId(compRoom.getId());
                        updateCompRoomComplimentaryList.add(compRoomComplimentary);

                        //把报告数据id添加到集合中
                        relReportIds.add(compRoomComplimentary.getId());

                        alreadyAssociatedReportIds.add(compRoomComplimentary.getId());
                    }
                }

                if (isHaveAssociated) {
                    //w-connect的数据设置为已关联
                    compRoom.setIsRel(true);
                    //w-connect的数据设置关联的报告id
                    compRoom.setRelIds(StringUtils.join(relReportIds.stream().distinct().toList(), ","));
                    updateCompRoomList.add(compRoom);
                }
            }

            rqCompRoomFormService.updateBatchById(updateCompRoomList);
            updateBatchById(updateCompRoomComplimentaryList);

            return alreadyAssociatedReportIds;
        }

        /**
         * 根据资产id,入住日期,房间号查询数据库已经存在的免费房数据
         *
         * @param propertyId   资产id
         * @param checkInDates 入住日期集合
         * @param roomNumbers  房间号集合
         * @return java.util.List<cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary>
         * @author lichen
         * @date 2023/8/5
         */
        @Override
        public List<CompRoomComplimentary> listByPropertyIdAndCheckInDatesAndRoomNumbers(Integer propertyId, Collection<Long> checkInDates, Collection<String> roomNumbers) {
            return list(new LambdaQueryWrapper<CompRoomComplimentary>().eq(CompRoomComplimentary::getPropertyId, propertyId)
                    .in(CompRoomComplimentary::getCheckInDate, checkInDates)
                    .in(CompRoomComplimentary::getRoomNumber, roomNumbers));
        }

        /**
         * 查询关联的报告id集合（优先根据房间号查询）
         *
         * @param propertyId     资产ID
         * @param roomNumberList 房间号集合
         * @param guestName      客人姓名
         * @param checkInDate    入住日期
         * @return java.util.List<java.lang.Long>
         * @author lichen
         * @date 2024/1/4
         */
        @Override
        public List<Long> listAssociatedId(Integer propertyId, @Nullable List<String> roomNumberList, String guestName, Long checkInDate) {
            LambdaQueryWrapper<CompRoomComplimentary> queryWrapper = new LambdaQueryWrapper<CompRoomComplimentary>()
                    .eq(CompRoomComplimentary::getPropertyId, propertyId)
                    .eq(CompRoomComplimentary::getCheckInDate, checkInDate);
            if (CollectionUtils.isNotEmpty(roomNumberList)) {
                //房间号不为空时，优先使用房间号
                queryWrapper.in(CompRoomComplimentary::getRoomNumber, roomNumberList);
            } else {
                //房间号为空时，根据客人姓名查询
                queryWrapper.in(CompRoomComplimentary::getGuestName, getDifferentOrderGuestName(guestName));
            }
            queryWrapper.eq(CompRoomComplimentary::getRelId, 0);
            return list(queryWrapper).stream().map(CompRoomComplimentary::getId).toList();
        }

        /**
         * 校验数据是否存在
         *
         * @param checkIds CompRoomComplimentary数据id集合
         * @return java.util.List<cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary>
         * @author lichen
         * @date 2024/1/5
         */
        @Override
        public List<CompRoomComplimentary> checkExist(List<Long> checkIds) {
            List<CompRoomComplimentary> compRoomComplimentaries = listByIds(checkIds);
            List<Long> idList = compRoomComplimentaries.stream().map(CompRoomComplimentary::getId).toList();
            if (checkIds.stream().anyMatch(checkId -> !idList.contains(checkId))) {
                checkIds.removeAll(idList);
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND, BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The CompRoomComplimentary not found, ids:{0}", checkIds.toString()));
            }
            return compRoomComplimentaries;
        }

        /**
         * 获取在住的免费房数据
         *
         * @param propertyDTO 资产DTO
         * @return java.util.List<cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary>
         * @author lichen
         * @date 2024/1/5
         */
        @Override
        public List<CompRoomComplimentary> listInHouse(PropertyDTO propertyDTO) {
            long currentDate = DateTimeUtils.dateNowTimestamp(propertyDTO.getTimezone());
            return list(new LambdaQueryWrapper<CompRoomComplimentary>()
                    .eq(CompRoomComplimentary::getPropertyId, propertyDTO.getId())
                    .le(CompRoomComplimentary::getCheckInDate, currentDate)
                    .gt(CompRoomComplimentary::getCheckOutDate, currentDate));
        }

        /**
         * 分页查询
         *
         * @param propertyId 资产id
         * @param query      查询对象
         * @return cloud.wconnect.commonlib.pojo.dto.ListPagedDTO<cloud.wconnect.frontoffice.entity.comproom.CompRoomComplimentary>
         * @author lichen
         * @date 2025/12/29
         */
        @Override
        public ListPagedDTO<CompRoomComplimentary> pageList(int propertyId, CompRoomComplimentaryQuery query) {
            boolean isQueryCheckInDate = Objects.nonNull(query.getCheckInDateEnd()) && Objects.nonNull(query.getCheckInDateBegin());
            //查询报告数据
            Page<CompRoomComplimentary> page = page(new Page<>(query.getPage(), query.getSize()),
                    new LambdaQueryWrapper<CompRoomComplimentary>()
                            .eq(CompRoomComplimentary::getPropertyId, propertyId)
                            .between(isQueryCheckInDate, CompRoomComplimentary::getCheckInDate, query.getCheckInDateBegin(), query.getCheckInDateEnd())
                            .eq(StringUtils.isNotBlank(query.getRoomNumber()), CompRoomComplimentary::getRoomNumber, query.getRoomNumber())
                            .like(StringUtils.isNotBlank(query.getGuestName()), CompRoomComplimentary::getGuestName, query.getGuestName())
                            .eq(Objects.nonNull(query.getIsRel()) && !query.getIsRel(), CompRoomComplimentary::getRelId, 0)
                            .gt(Objects.nonNull(query.getIsRel()) && query.getIsRel(), CompRoomComplimentary::getRelId, 0)
                            .orderByDesc(CompRoomComplimentary::getId));
            return new ListPagedDTO<>(page.getTotal(), page.getRecords());
        }

    }