# ClcSiteDataServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.frontoffice.entity.clc.ClcArrivalDetailed;
    import cloud.wconnect.frontoffice.entity.clc.ClcCompareConfig;
    import cloud.wconnect.frontoffice.entity.clc.ClcGuestInh;
    import cloud.wconnect.frontoffice.entity.clc.ClcSiteData;
    import cloud.wconnect.frontoffice.mapper.clc.ClcSiteDataMapper;
    import cloud.wconnect.frontoffice.pojo.bo.clc.SiteDataBO;
    import cloud.wconnect.frontoffice.service.clc.IClcCompareConfigService;
    import cloud.wconnect.frontoffice.service.clc.IClcSiteDataService;
    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.service.impl.ServiceImpl;
    import jakarta.annotation.Nullable;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-07-24
     */
    @Service
    @DS("logbook")
    public class ClcSiteDataServiceImpl extends ServiceImpl<ClcSiteDataMapper, ClcSiteData> implements IClcSiteDataService {

        @Autowired
        private IClcCompareConfigService clcCompareConfigService;
        @Autowired
        private RedissonClient redissonClient;

        private static final String KEY_PREFIX = "clc_site_data:insert_lock";

        /**
         * 获取 clc 匹配记录
         *
         * @param clcArrivalDetailed opera arrival
         * @param clcSiteDataList    site data
         * @param checkDynamicConfig 检查动态配置规则
         * @return clc 匹配记录
         * @author yyt
         * @date 2023/7/24
         */
        @Override
        public ClcSiteData getMatchRecord(ClcArrivalDetailed clcArrivalDetailed, List<ClcSiteData> clcSiteDataList, boolean checkDynamicConfig) {
            /**
             * 匹配规则：
             * 1、优先使用 入住日期 + 房间号匹配，匹配不上，使用 入住日期 + 姓名匹配
             * 2、如果额外配置了比对规则（入住信息 房型与 clc 房价是否适配），则再次根据比对规则进行校验
             */
            if(CollectionUtils.isEmpty(clcSiteDataList)){
                return null;
            }

            Optional<ClcSiteData> optional = clcSiteDataList.stream()
                    .filter(item -> StringUtils.isNotEmpty(item.getRoomNumber()))
                    .filter(item -> item.getRoomNumber().equals(clcArrivalDetailed.getRoomNumber())).findAny();
            if (optional.isPresent()) {
                ClcSiteData siteData = optional.get();
                /**
                 * 是否检查动态配置规则，当前参数使用场景：
                 * 1、当导入报告时，需要检查是否符合动态配置规则
                 * 2、页面查看详情时，不需要检查是否符合动态配置规则，因为页面要根据返回信息做标红处理
                 */
                if (checkDynamicConfig) {
                    // 检查是否适配配置规则
                    if (!matchRoomTypeAndRate(clcArrivalDetailed, siteData)) {
                        return null;
                    }
                }
                return siteData;
            } else {
                // 房间号匹配失败后，再尝试使用客户姓名进行匹配
                String[] guestNameArr = StringUtils.split(clcArrivalDetailed.getGuestName(), ',');
                if (guestNameArr.length == 0) {
                    return null;
                }
                optional = clcSiteDataList.stream()
                        .filter(item -> StringUtils.isEmpty(item.getRoomNumber()))
                        .filter(item ->
                                Arrays.stream(guestNameArr).map(operaName -> operaName.trim().toLowerCase())
                                        .allMatch(operaLowerCaseName -> item.getCustomer().trim().toLowerCase()
                                                .contains(operaLowerCaseName)))
                        .findFirst();
                if (optional.isPresent()) {
                    ClcSiteData siteData = optional.get();
                    if (checkDynamicConfig) {
                        // 检查是否适配配置规则
                        if (!matchRoomTypeAndRate(clcArrivalDetailed, siteData)) {
                            return null;
                        }
                    }
                    return siteData;
                }
            }
            return null;
        }

        /**
         * 根据匹配规则（入住信息 房型与 clc 房价是否适配），校验 入住信息 与 clc 是否匹配
         *
         * @param clcArrivalDetailed 入住信息
         * @param siteData           clc
         * @return true/false
         * @author yyt
         * @date 2023/7/24
         */
        private boolean matchRoomTypeAndRate(ClcArrivalDetailed clcArrivalDetailed, ClcSiteData siteData) {
            List<ClcCompareConfig> compareConfigs = clcCompareConfigService.list(clcArrivalDetailed.getPropertyId());
            if (CollectionUtils.isEmpty(compareConfigs)) {
                return true;
            }
            if (compareConfigs.stream().noneMatch(item -> item.getRoomType().equals(clcArrivalDetailed.getRoomType())
                    && item.getRate().compareTo(new BigDecimal(siteData.getRate())) == 0)) {
                return false;
            }
            return true;
        }

        /**
         * 处理 site 数据
         *
         * @param property  资产
         * @param siteDatas site 数据
         * @param checkOut  退房
         * @author yyt
         * @date 2023/7/27
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleSiteData(PropertyDTO property, List<SiteDataBO> siteDatas, @Nullable Boolean checkOut) {
            long nowTime = DateTimeUtils.nowTimestamp();
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH);
            Integer propertyId = property.getId();
            ZoneId zoneId = ZoneId.of(property.getTimezone());
            InternationalizationNumberDTO number = property.getInternationalization().getNumber();
            int dataPrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            RLock lock = redissonClient.getLock(KEY_PREFIX);

            if (getLock(lock)) {
                try {

                    baseMapper.saveOrUpdateBatch(siteDatas.stream().map(item -> {
                        ClcSiteData siteData = new ClcSiteData();
                        siteData.setPropertyId(propertyId);
                        siteData.setCheckInDate(LocalDate.parse(item.getCheckInDate(), dateTimeFormatter).atStartOfDay(zoneId).toEpochSecond());
                        siteData.setCheckOutDate(LocalDate.parse(item.getCheckOutDate(), dateTimeFormatter).atStartOfDay(zoneId).toEpochSecond());
                        siteData.setRoomNumber(item.getRoomNumber());
                        siteData.setCustomer(item.getCustomer());
                        siteData.setFolioNumber(item.getFolioNumber());
                        siteData.setRate(Objects.isNull(item.getRate()) ? StringUtils.EMPTY : item.getRate().setScale(dataPrecision, roundingMode).toString());
                        siteData.setNights(item.getNights());
                        siteData.setCreatedTime(nowTime);
                        // 是否退房；0标识退房，1标识未退房
                        if (Objects.isNull(checkOut)) {
                            siteData.setClickCheckOut(!item.getCheckOut());
                        } else {
                            siteData.setClickCheckOut(!checkOut);
                        }
                        return siteData;
                    }).collect(Collectors.toList()));

                } finally {
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();  // 释放锁
                    }
                }
            }

        }

        /**
         * 获取 redis 锁
         *
         * @param lock  redis lock
         * @return redis 锁
         * @author yyt
         * @date 2025/11/8
         **/
        private boolean getLock(RLock lock) {
            int maxRetries = 3;
            for (int i = 0; i < maxRetries; i++) {
                try {
                    boolean acquired = lock.tryLock(10, -1, TimeUnit.SECONDS);
                    if (acquired) {
                        return true;
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Lock interrupted", e);
                }
            }
            return false;
        }

        /**
         * 获取匹配状态下、未点击退房的 clc 信息
         *
         * @param propertyId 资产 id
         * @return clc 信息
         * @author yyt
         * @date 2023/8/1
         */
        @Override
        public List<ClcSiteData> listMatchAndNotCheckOut(Integer propertyId) {
            return baseMapper.listMatchAndNotCheckOut(propertyId);
        }

        /**
         * 获取匹配状态下、已点击退房的 clc 信息
         *
         * @param checkInList 在店人员信息
         * @return clc 信息
         * @author yyt
         * @date 2023/8/1
         */
        @Override
        public List<ClcSiteData> listMatchAndCheckOut(List<ClcGuestInh> checkInList) {
            return baseMapper.listMatchAndCheckOut(checkInList);
        }

        /**
         * 处理未退房 site data
         *
         * @param propertyDTO 资产
         * @param siteDatas   site data
         * @author yyt
         * @date 2023/11/4
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleNotCheckOutSiteData(PropertyDTO propertyDTO, List<SiteDataBO> siteDatas) {
            // 先把 未退房的标记 2，然后再保存 site data
            update(new UpdateWrapper<ClcSiteData>().lambda().eq(ClcSiteData::getPropertyId, propertyDTO.getId())
                    .eq(ClcSiteData::getClickCheckOut, true).set(ClcSiteData::getClickCheckOut, 2));

            handleSiteData(propertyDTO, siteDatas, false);
        }

        /**
         * 查询指定资产、指定入住日期的 clc 数据
         *
         * @param propertyId  资产 id
         * @param arrivalDate 入住日期
         * @return clc 数据
         * @author yyt
         * @date 2024/11/19
         **/
        @Override
        public List<ClcSiteData> list(Integer propertyId, Long arrivalDate) {
            return list(new QueryWrapper<ClcSiteData>().lambda()
                    .eq(ClcSiteData::getPropertyId, propertyId)
                    .eq(ClcSiteData::getCheckInDate, arrivalDate));
        }

    }