# HolidayServiceImpl.java

    package cloud.wconnect.rateshop.service.rateshop.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.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.UpdateLog;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.rateshop.cons.rateshop.RateShopCons;
    import cloud.wconnect.rateshop.cons.rateshop.RateShopErrorI18nConstant;
    import cloud.wconnect.rateshop.entity.rateshop.Holiday;
    import cloud.wconnect.rateshop.entity.rateshop.Setting;
    import cloud.wconnect.rateshop.mapper.rateshop.HolidayMapper;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.holiday.CollectDTO;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.holiday.HolidayDTO;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.holiday.HolidayExcelDTO;
    import cloud.wconnect.rateshop.pojo.vo.rateshop.holiday.CalendarHolidayVO;
    import cloud.wconnect.rateshop.pojo.vo.rateshop.holiday.HolidayVO;
    import cloud.wconnect.rateshop.service.rateshop.IHolidayService;
    import cloud.wconnect.rateshop.service.rateshop.ISettingService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.ImmutableMap;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.poi.ss.usermodel.Row;
    import org.apache.poi.ss.usermodel.Sheet;
    import org.apache.poi.ss.usermodel.Workbook;
    import org.apache.poi.ss.usermodel.WorkbookFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.IOException;
    import java.io.InputStream;
    import java.text.MessageFormat;
    import java.time.LocalDate;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 节假日 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2023-06-10
     */
    @DS("rate_shop")
    @Service
    public class HolidayServiceImpl extends ServiceImpl<HolidayMapper, Holiday> implements IHolidayService {

        @Autowired
        private ISettingService settingService;
        @Autowired
        private UpdateLogUtil updateLogUtil;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private HolidayMapper holidayMapper;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;

        /**
         * 查询节假日信息
         *
         * @param propertyDTO 资产DTO
         * @param fromDate    开始时间(yyyy-MM-dd)
         * @param toDate      结束时间(yyyy-MM-dd)
         * @return java.util.List<cloud.wconnect.rateshop.pojo.vo.holiday.CalendarHolidayVO>
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        public List<CalendarHolidayVO> list(PropertyDTO propertyDTO, String fromDate, String toDate) {
            //查询资产rateShop功能的配置信息
            Setting setting = settingService.getByPropertyId(propertyDTO.getId());
            if (Objects.isNull(setting)) {
                throw new BadRequestException(RateShopErrorI18nConstant.PROPERTY_NOT_CONFIG_RATE_SHOP_SETTING,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The property has no rateShop set, propertyId = " + propertyDTO.getId());
            }
            //查询自动创建的节假日
            List<Holiday> autoCreateHolidays = listAutoCreate(setting.getCountryRegionId(),
                    Integer.parseInt(fromDate.replace("-", "")),
                    Integer.parseInt(toDate.replace("-", "")));
            //查询手动创建的节假日
            List<Holiday> manualCreateHolidays = listManualCreate(propertyDTO.getId(),
                    Integer.parseInt(fromDate.replace("-", "")),
                    Integer.parseInt(toDate.replace("-", "")));
            //合并自动创建和手动创建的节假日
            List<Holiday> allHolidays = new ArrayList<>();
            allHolidays.addAll(autoCreateHolidays);
            allHolidays.addAll(manualCreateHolidays);
            if (CollectionUtils.isEmpty(allHolidays)) return Collections.emptyList();

            //时间转换为localDate
            LocalDate fromLocalDate = DateTimeUtils.strToLocalDate(fromDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);
            LocalDate toLocalDate = DateTimeUtils.strToLocalDate(toDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);

            List<CalendarHolidayVO> resultList = new ArrayList<>();

            //生成日历板
            for (LocalDate localDate = fromLocalDate; !localDate.isAfter(toLocalDate); localDate = localDate.plusDays(1)) {
                //把localDate转成Integer类型的时间（yyyyMMdd）
                int l = Integer.parseInt(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                List<Holiday> matchHolidays = allHolidays.stream()
                        .filter(holiday -> l >= holiday.getBeginDate() && l <= holiday.getEndDate())
                        .collect(Collectors.toList());
                if (CollectionUtils.isNotEmpty(matchHolidays)) {
                    String date = DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                    //List<Holiday> -> List<HolidayVO>
                    List<HolidayVO> holidayVOList = matchHolidays.stream().map(holiday -> {
                        //创建方式
                        int type = holiday.getPropertyId() != 0 ? HolidayVO.MANUAL_CREATE : HolidayVO.AUTO_CREATE;

                        //获取更新日志
                        List<UpdateLog> updateLogs = updateLogUtil.strToList(holiday.getUpdateLog());
                        //获取最后一次的更新时间
                        Long timestamp = updateLogs.stream()
                                .max(Comparator.comparingLong(UpdateLog::getTimestamp)).get().getTimestamp();

                        //格式化时间
                        LocalDate beginLocalDate = DateTimeUtils.strToLocalDate(holiday.getBeginDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                        LocalDate endLocalDate = DateTimeUtils.strToLocalDate(holiday.getEndDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);

                        return HolidayVO.builder()
                                .id(holiday.getId())
                                .title(holiday.getTitle())
                                .beginDate(DateTimeUtils.localDateToStr(beginLocalDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                                .endDate(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                                .description(holiday.getDescription())
                                .type(type)
                                .updateTime(timestamp)
                                .build();
                    }).collect(Collectors.toList());
                    resultList.add(new CalendarHolidayVO(date, holidayVOList));
                }
            }
            return resultList;
        }

        /**
         * 查询自动创建的节假日信息
         *
         * @param countryRegionId 国家id
         * @param fromDate        开始时间(yyyyMMdd)
         * @param toDate          结束时间(yyyyMMdd)
         * @return java.util.List<cloud.wconnect.rateshop.entity.rateshop.Holiday>
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        public List<Holiday> listAutoCreate(Integer countryRegionId, int fromDate, int toDate) {
            return list(new LambdaQueryWrapper<Holiday>()
                    .eq(Holiday::getPropertyId, 0)
                    .eq(Holiday::getCountry, countryRegionId)
                    .and(queryWrapper ->
                            queryWrapper
                                    .between(Holiday::getBeginDate, fromDate, toDate)
                                    .or()
                                    .between(Holiday::getEndDate, fromDate, toDate)
                    ));
        }

        /**
         * 查询手动创建的节假日信息
         *
         * @param propertyId 资产id
         * @param fromDate   开始时间(yyyyMMdd)
         * @param toDate     结束时间(yyyyMMdd)
         * @return java.util.List<cloud.wconnect.rateshop.entity.rateshop.Holiday>
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        public List<Holiday> listManualCreate(Integer propertyId, int fromDate, int toDate) {
            return list(new LambdaQueryWrapper<Holiday>()
                    .eq(Holiday::getPropertyId, propertyId)
                    .eq(Holiday::getCountry, 0)
                    .and(queryWrapper ->
                            queryWrapper
                                    .between(Holiday::getBeginDate, fromDate, toDate)
                                    .or()
                                    .between(Holiday::getEndDate, fromDate, toDate)
                    ));
        }

        /**
         * 新增或修改节假日信息
         *
         * @param propertyDTO 资产DTO
         * @param holidayDTO  节假日对象
         * @return java.util.Map<java.lang.Integer, java.lang.Integer>
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        public Map<String, Integer> saveOrUpdate(PropertyDTO propertyDTO, HolidayDTO holidayDTO) {
            //把yyyy-MM-dd的日期转化成时间戳
            long beginTimestamp = DateTimeUtils.dateStrToEpochSecond(holidayDTO.getBeginDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD, propertyDTO.getTimezone());
            long endTimestamp = DateTimeUtils.dateStrToEpochSecond(holidayDTO.getEndDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD, propertyDTO.getTimezone());
            //开始时间不能大于结束时间
            if (beginTimestamp > endTimestamp) {
                throw new BadRequestException(CommonErrorI18nConstant.START_DATE_CANNOT_GREATER_THAN_END_DATE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The endDate must be longer than the beginDate");
            }
            //构建holiday对象
            Holiday holiday = Holiday.builder()
                    .id(holidayDTO.getId())
                    .propertyId(propertyDTO.getId())
                    .title(holidayDTO.getTitle())
                    .country(0)
                    .beginDate(Integer.parseInt(holidayDTO.getBeginDate().replace("-", "")))
                    .endDate(Integer.parseInt(holidayDTO.getEndDate().replace("-", "")))
                    .description(holidayDTO.getDescription())
                    .updateLog(holidayDTO.getUpdateLog())
                    .build();
            //新增或修改
            saveOrUpdate(holiday);
            return ImmutableMap.of("id", holiday.getId());
        }

        /**
         * 校验节假日是否存在且是否手动创建
         *
         * @param id 节假日主键id
         * @return cloud.wconnect.rateshop.entity.rateshop.Holiday
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        public Holiday checkExistAndIsManualCreate(Integer id) {
            Holiday holiday = getById(id);
            if (Objects.isNull(holiday)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The holiday not found, holidayId = " + id);
            }
            if (holiday.getPropertyId() == 0) {
                throw new BadRequestException(RateShopErrorI18nConstant.AUTO_IMPORT_HOLIDAY_CANNOT_OPERATE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "You can operate only manually created holiday data, holidayId = " + id);
            }
            return holiday;
        }

        /**
         * 采集节假日信息
         *
         * @param collectDTO 采集的数据
         * @return void
         * @author lichen
         * @date 2023/6/19
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void collect(CollectDTO collectDTO) {
            //更新记录日志
            UpdateLog updateLog = new UpdateLog(0, collectDTO.getQueryDate());
            List<UpdateLog> updateLogs = Arrays.asList(updateLog);
            String updateLogStr = JsonUtils.convertObjListToString(objectMapper, updateLogs);

            //List<HolidayDTO> -> List<Holiday>
            List<Holiday> holidays = beanMapper.mapList(collectDTO.getHoliday(), Holiday.class, holiday -> {
                holiday.setUpdateLog(updateLogStr);
                holiday.setPropertyId(BaseConstant.ZERO);
                holiday.setCountry(collectDTO.getCountry());
                holiday.setDescription("");
            });

            if (CollectionUtils.isEmpty(holidays)) return;

            //从采集数据中获取第一条数据，用于计算当前年份的第一天和最后一天
            Integer beginDate = holidays.get(0).getBeginDate();
            LocalDate localDate = DateTimeUtils.strToLocalDate(beginDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            //计算当前采集年份的第一天
            String firstDay = DateTimeUtils.localDateToStr(localDate.withDayOfYear(1), BaseConstant.DATE_FORMAT_YYYYMMDD);
            //计算当前采集的年份最后一天
            int lengthOfYear = localDate.lengthOfYear();
            String endDay = DateTimeUtils.localDateToStr(localDate.withDayOfYear(lengthOfYear), BaseConstant.DATE_FORMAT_YYYYMMDD);

            //删除当年的数据
            remove(new LambdaQueryWrapper<Holiday>()
                    .eq(Holiday::getCountry, collectDTO.getCountry())
                    .eq(Holiday::getPropertyId, 0)
                    .between(Holiday::getBeginDate, firstDay, endDay));

            //批量新增
            saveBatch(holidays);
        }

        /**
         * 导入假期
         *
         * @param propertyDTO 资产DTO
         * @param file        文件
         * @param loginUserId 登录用户id
         * @return void
         * @author lichen
         * @date 2023/12/20
         */
        @Override
        public void importExcel(PropertyDTO propertyDTO, MultipartFile file, int loginUserId) {
            //解析excel
            List<HolidayExcelDTO> holidayExcelDTOS = parseExcel(file);
            if (CollectionUtils.isEmpty(holidayExcelDTOS)) return;

            List<Holiday> holidays = beanMapper.mapList(holidayExcelDTOS, Holiday.class, (dto, entity) -> {
                entity.setPropertyId(propertyDTO.getId());
                entity.setCountry(0);
                entity.setUpdateLog(updateLogUtil.get(loginUserId));
                entity.setBeginDate(Integer.valueOf(dto.getStartDateStr().replace("-", "")));
                entity.setEndDate(Integer.valueOf(dto.getEndDateStr().replace("-", "")));
            });

            //根据title + beginDate去重
            List<Holiday> distinctHolidayList = holidays.stream()
                    .collect(
                            Collectors.collectingAndThen(
                                    Collectors.toCollection(() -> new TreeSet<>(
                                            Comparator.comparing(Holiday::titleAndBeginDateKey))), ArrayList::new
                            )
                    );

            List<Holiday> saveBatchList = Lists.newArrayList(distinctHolidayList);

            //根据title 和 beginDate 查询手动创建的节假日
            List<Holiday> alreadyExistHolidays = holidayMapper.selectManualByTitleAndBeginDate(propertyDTO.getId(), distinctHolidayList);
            if (CollectionUtils.isNotEmpty(alreadyExistHolidays)) {
                Set<String> alreadyKeys = alreadyExistHolidays.stream().map(Holiday::titleAndBeginDateKey).collect(Collectors.toSet());

                //过滤掉已经存在的节假日
                saveBatchList = distinctHolidayList.stream().filter(item -> {
                    String key = item.titleAndBeginDateKey();
                    return !alreadyKeys.contains(key);
                }).toList();
            }

            //批量新增
            saveBatch(saveBatchList);
        }

        /**
         * 解析Excel
         *
         * @param file 文件
         * @return java.util.List<cloud.wconnect.rateshop.pojo.dto.rateshop.holiday.HolidayExcelDTO>
         * @author lichen
         * @date 2023/12/20
         */
        private List<HolidayExcelDTO> parseExcel(MultipartFile file) {
            List<HolidayExcelDTO> holidayExcelDTOS = Lists.newArrayList();
            try (InputStream inputStream = file.getInputStream()) {
                Workbook workbook = WorkbookFactory.create(inputStream);
                Sheet sheet = workbook.getSheetAt(0);
                //获取最大行数
                int lastRowNum = sheet.getLastRowNum();
                //只包含标题
                if (lastRowNum < 1) return Collections.emptyList();

                //标题行
                Row titleRow = sheet.getRow(0);
                //标题集合
                ArrayList<String> titles = Lists.newArrayList();
                for (int i = 0; i < 4; i++) {
                    titles.add(CellUtils.convertCellStr(titleRow.getCell(i)));
                }

                for (int i = 1; i <= lastRowNum; i++) {
                    Row row = sheet.getRow(i);

                    HolidayExcelDTO holidayExcelDTO = new HolidayExcelDTO();

                    //标题
                    String title = CellUtils.convertCellStr(row.getCell(0));
                    if (StringUtils.isBlank(title)) {
                        //获取国际化的参数
                        Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(0));
                        throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY, language2ParamList,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The title cannot be null");
                    } else {
                        if (title.length() > 255) {
                            //获取国际化的参数
                            Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(0), 255);
                            throw new BadRequestException(CommonErrorI18nConstant.OVERLENGTH, language2ParamList,
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                    "The title cannot be longer than 255, title =" + title);
                        }
                    }
                    holidayExcelDTO.setTitle(title);

                    //开始日期
                    String startDate = CellUtils.convertCellStr(row.getCell(1));
                    LocalDate startDateOfLocalDate;
                    if (StringUtils.isBlank(startDate)) {
                        //获取国际化的参数
                        Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(1));
                        throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY, language2ParamList,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The startDate cannot be null");
                    } else {
                        try {
                            startDateOfLocalDate = DateTimeUtils.strToLocalDate(startDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                        } catch (Exception e) {
                            //获取国际化的参数
                            Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(1),
                                    BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                            throw new BadRequestException(CommonErrorI18nConstant.FORMAT_INCORRECT, language2ParamList,
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The startDate format is incorrect, startDate = " + startDate);
                        }
                    }
                    holidayExcelDTO.setStartDateStr(startDate);

                    //结束日期
                    String endDate = CellUtils.convertCellStr(row.getCell(2));
                    LocalDate endDateLocalDate;
                    if (StringUtils.isBlank(endDate)) {
                        //获取国际化的参数
                        Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(2));
                        throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY, language2ParamList,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The endDate cannot be null");
                    } else {
                        try {
                            endDateLocalDate = DateTimeUtils.strToLocalDate(endDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                        } catch (Exception e) {
                            //获取国际化的参数
                            Map<String, List<Object>> language2ParamList = i18nMessageUtils.getErrorParamMapNotNeedTranslate(titles.get(2),
                                    BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                            throw new BadRequestException(CommonErrorI18nConstant.FORMAT_INCORRECT, language2ParamList,
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The endDate format is incorrect, endDate = " + endDate);
                        }
                    }
                    holidayExcelDTO.setEndDateStr(endDate);

                    //校验开始时间不能大于结束时间
                    if (startDateOfLocalDate.isAfter(endDateLocalDate)) {
                        throw new BadRequestException(CommonErrorI18nConstant.START_DATE_CANNOT_GREATER_THAN_END_DATE,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                MessageFormat.format("Start Date Cannot Be Later Than End Dat, startDate:{0}, endDate:{1}", startDate, endDate));
                    }

                    String desc = CellUtils.convertCellStr(row.getCell(3));
                    holidayExcelDTO.setDescription(desc);

                    holidayExcelDTOS.add(holidayExcelDTO);
                }

                return holidayExcelDTOS;
            } catch (IOException ioException) {
                throw new InternalServerErrorException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.SYSTEM_ERROR.getCode(), "parse event excel error");
            }
        }
    }