# DepCategoryRecommendHourServiceImpl.java

    package cloud.wconnect.workperformance.service.sva.setting.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.query.PageSizeQuery;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.sva.SvaErrorI18nConstant;
    import cloud.wconnect.workperformance.entity.sva.setting.DepCategoryRecommendHour;
    import cloud.wconnect.workperformance.entity.sva.setting.DepCodeToCategory;
    import cloud.wconnect.workperformance.entity.sva.setting.DepartmentCategory;
    import cloud.wconnect.workperformance.mapper.sva.setting.DepCategoryRecommendHourMapper;
    import cloud.wconnect.workperformance.pojo.dto.sva.recommendhours.RecommendHoursDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.recommendhours.RecommendHoursHistoryDTO;
    import cloud.wconnect.workperformance.service.sva.setting.IDepCategoryRecommendHourService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepCodeToCategoryService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepartmentCategoryService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    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.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Maps;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.time.ZoneId;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.IntStream;

    /**
     * <p>
     * 服务实现类：部门分类推荐时数
     * </p>
     *
     * @author zc
     * @since 2023-03-12
     */
    @Service
    @DS("work_performance")
    public class DepCategoryRecommendHourServiceImpl extends ServiceImpl<DepCategoryRecommendHourMapper, DepCategoryRecommendHour> implements IDepCategoryRecommendHourService {
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;
        @Lazy
        @Resource
        private IDepartmentCategoryService departmentCategoryService;
        @Lazy
        @Resource
        private IDepCodeToCategoryService depCodeToCategoryService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;

        /**
         * 新增 recommend hours 数据
         *
         * @param propertyId        property id
         * @param recommendHoursDTO recommend hours 数据
         * @param userId            操作人
         * @return recommend hours id
         * @author zc
         * @date 2023/3/12
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public int insert(int propertyId, RecommendHoursDTO recommendHoursDTO, int userId) {
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .eq(DepCategoryRecommendHour::getStartingDate, recommendHoursDTO.getStartingDate())
                    .select(DepCategoryRecommendHour::getId);
            if (count(queryWrapper) > 0) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_EXISTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The recommend hours startingDate has existed in current property; startingDate: " + recommendHoursDTO.getStartingDate());
            }

            // 根据基础版本，新增新版本对应的 部门分类，部门分类关联关系
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            int newVersion = Integer.parseInt(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(recommendHoursDTO.getStartingDate(), timezone), BaseConstant.DATE_FORMAT_YYYYMMDD));
            Map<String, Map<String, Object>> categoryId2RecommendHour = JsonUtils.convertStringByTypeReference(objectMapper,
                    recommendHoursDTO.getCategoryRecomHours(), new TypeReference<>() {
                    });
            DepartmentCategory category = departmentCategoryService.getById(categoryId2RecommendHour.keySet().stream().map(Integer::parseInt).toList().get(0));
            Map<Integer, Integer> newVersionCategoryIdByLastVersionCategoryId = departmentCategoryService.saveNewVersion(propertyId, newVersion, category.getVersion(), userId);

            Map<String, Map<String, Object>> newVersionCategoryId2RecommendHour = Maps.newHashMapWithExpectedSize(categoryId2RecommendHour.size());
            categoryId2RecommendHour.forEach((categoryId, recommendHour) -> {
                Integer newCategoryId = newVersionCategoryIdByLastVersionCategoryId.get(Integer.parseInt(categoryId));
                if (Objects.isNull(newCategoryId)) return;

                newVersionCategoryId2RecommendHour.put(newCategoryId.toString(), recommendHour);
            });

            DepCategoryRecommendHour recommendHour = new DepCategoryRecommendHour().setPropertyId(propertyId)
                    .setStartingDate(recommendHoursDTO.getStartingDate())
                    .setCategoryRecomHours(JsonUtils.convertObjToString(objectMapper, newVersionCategoryId2RecommendHour))
                    .setCreatedBy(userId)
                    .setCreatedTime(DateTimeUtils.nowTimestamp());
            save(recommendHour);

            return recommendHour.getId();
        }

        /**
         * 编辑 recommend hours 数据
         *
         * @param propertyDTO         资产
         * @param operatorId          操作人 id
         * @param originRecommendHour 原 recommend hours 数据
         * @param recommendHoursDTO   recommend hours 数据
         * @return recommend hours id
         * @author zc
         * @date 2023/3/12
         */
        @Override
        public int update(PropertyDTO propertyDTO, int operatorId, DepCategoryRecommendHour originRecommendHour, RecommendHoursDTO recommendHoursDTO) {
            Integer propertyId = propertyDTO.getId();
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .ne(DepCategoryRecommendHour::getId, recommendHoursDTO.getId())
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .eq(DepCategoryRecommendHour::getStartingDate, recommendHoursDTO.getStartingDate())
                    .select(DepCategoryRecommendHour::getId);
            if (count(queryWrapper) > 0) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_EXISTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The recommend hours startingDate has existed in current property; startingDate: " + recommendHoursDTO.getStartingDate());
            }

            DepCategoryRecommendHour recommendHour = beanMapper.map(recommendHoursDTO, DepCategoryRecommendHour.class);
            recommendHour.setId(recommendHoursDTO.getId());
            recommendHour.setLastUpdatedBy(operatorId);
            recommendHour.setLastUpdatedTime(DateTimeUtils.nowTimestamp());
            updateById(recommendHour);

            String timezone = propertyDTO.getTimezone();
            if (!originRecommendHour.getStartingDate().equals(recommendHoursDTO.getStartingDate())) {
                // 重置版本
                int originVersion = Integer.parseInt(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(originRecommendHour.getStartingDate(), timezone), BaseConstant.DATE_FORMAT_YYYYMMDD));
                int newVersion = Integer.parseInt(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(recommendHoursDTO.getStartingDate(), timezone), BaseConstant.DATE_FORMAT_YYYYMMDD));
                departmentCategoryService.update(new UpdateWrapper<DepartmentCategory>().lambda()
                        .set(DepartmentCategory::getVersion, newVersion)
                        .eq(DepartmentCategory::getPropertyId, propertyId)
                        .eq(DepartmentCategory::getVersion, originVersion));
                depCodeToCategoryService.update(new UpdateWrapper<DepCodeToCategory>().lambda()
                        .set(DepCodeToCategory::getVersion, newVersion)
                        .eq(DepCodeToCategory::getPropertyId, propertyId)
                        .eq(DepCodeToCategory::getVersion, originVersion));
            }

            return recommendHour.getId();
        }

        /**
         * 获取 recommend hours 历史版本列表
         *
         * @param propertyId property id
         * @param page       当前页码
         * @param size       每页条数
         * @return recommend hours 历史数据
         * @author zc
         * @date 2023/3/13
         */
        @Override
        public ListPagedDTO<RecommendHoursHistoryDTO> listHistories(int propertyId, int page, int size) {
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .orderByDesc(DepCategoryRecommendHour::getStartingDate)
                    .select(DepCategoryRecommendHour::getId, DepCategoryRecommendHour::getStartingDate);
            Page<DepCategoryRecommendHour> paged = page(new Page<>(page, size), queryWrapper);
            if (paged.getTotal() == 0L) {
                return new ListPagedDTO<>();
            }
            return new ListPagedDTO<>(paged.getTotal(), beanMapper.mapList(paged.getRecords(), RecommendHoursHistoryDTO.class));
        }

        /**
         * 获取当前应用的 recommend hours 数据
         *
         * @param propertyId property id
         * @param targetDate 目标日期
         * @return recommend hours
         * @author zc
         * @date 2023/3/12
         */
        @Override
        public RecommendHoursDTO getCurrent(int propertyId, long targetDate) {
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .le(DepCategoryRecommendHour::getStartingDate, targetDate)
                    .orderByDesc(DepCategoryRecommendHour::getStartingDate)
                    .last("limit 1");
            DepCategoryRecommendHour recommendHour = getOne(queryWrapper);
            if (Objects.isNull(recommendHour)) {
                return null;
            }

            return beanMapper.map(recommendHour, RecommendHoursDTO.class);
        }

        /**
         * 获取最新版本的 recommend hours 数据
         *
         * @param propertyId property id
         * @return recommend hours
         * @author zc
         * @date 2023/3/12
         */
        @Override
        public RecommendHoursDTO getLatest(int propertyId) {
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .orderByDesc(DepCategoryRecommendHour::getStartingDate)
                    .last("limit 1");
            DepCategoryRecommendHour recommendHour = getOne(queryWrapper);
            if (Objects.isNull(recommendHour)) {
                return null;
            }

            return beanMapper.map(recommendHour, RecommendHoursDTO.class);
        }

        /**
         * 获取有效的 recommend hours 数据
         *
         * @param propertyId property id
         * @param targetDate 目标日期
         * @param isGreater  0: 获取大于目标日期的数据；1: 获取小于目标日期的数据；2: 获取等于目标日期的数据
         * @return recommend hours
         * @author zc
         * @date 2023/3/17
         */
        @Override
        public RecommendHoursDTO getEffective(int propertyId, long targetDate, int isGreater) {
            LambdaQueryWrapper<DepCategoryRecommendHour> queryWrapper = new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId);
            if (RecommendHoursDTO.GREATER_THAN == isGreater) {
                queryWrapper.gt(DepCategoryRecommendHour::getStartingDate, targetDate)
                        .orderByAsc(DepCategoryRecommendHour::getStartingDate);
            } else if (RecommendHoursDTO.LESS_THAN == isGreater) {
                queryWrapper.lt(DepCategoryRecommendHour::getStartingDate, targetDate)
                        .orderByDesc(DepCategoryRecommendHour::getStartingDate);
            } else if (RecommendHoursDTO.EQUAL_TO == isGreater) {
                queryWrapper.eq(DepCategoryRecommendHour::getStartingDate, targetDate);
            }

            DepCategoryRecommendHour recommendHour = getOne(queryWrapper.last("limit 1"));
            if (Objects.isNull(recommendHour)) {
                return null;
            }

            return beanMapper.map(recommendHour, RecommendHoursDTO.class);
        }

        /**
         * 获取启用日期在选择日期范围内和选择日期之前的最新的一条数据
         *
         * @param propertyId 资产 id
         * @param startDate  开始时间（零点时刻时间戳）
         * @param endDate    结束时间（零点时刻时间戳）
         * @return DepCategoryRecommendHour 集合
         * @author zc
         * @date 2025/2/7
         */
        @Override
        public List<DepCategoryRecommendHour> listByDateRangeWithNewOne(int propertyId, long startDate, long endDate) {
            return baseMapper.listByDate(propertyId, startDate, endDate);
        }

        /**
         * 删除推荐工时
         *
         * @param recommendHour 推荐工时
         * @author yyt
         * @date 2025/3/31
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void removeRecommendHour(DepCategoryRecommendHour recommendHour) {
            Integer propertyId = recommendHour.getPropertyId();
            Integer version = departmentCategoryService.getVersion(propertyId, recommendHour.getStartingDate());
            List<DepartmentCategory> departmentCategories = departmentCategoryService.getByVersion(propertyId, version);
            if (CollectionUtils.isNotEmpty(departmentCategories)) {
                // 删除部门分类，部门分类关联关系
                List<Integer> categoryIds = departmentCategories.stream().map(DepartmentCategory::getId).toList();
                departmentCategoryService.removeBatchByIds(categoryIds);
                depCodeToCategoryService.remove(new QueryWrapper<DepCodeToCategory>().lambda()
                        .eq(DepCodeToCategory::getPropertyId, propertyId)
                        .in(DepCodeToCategory::getDepCategoryId, categoryIds));
            }
            // 删除推荐工时
            removeById(recommendHour.getId());
        }

        /**
         * 创建新版本推荐工时
         *
         * @param propertyId            资产 id
         * @param startingDate          开始日期
         * @param depCodeToCategoryList 部门分类与部门 Code 关联关系
         * @author yyt
         * @date 2025/3/31
         **/
        @Override
        public void createNewVersion(int propertyId, Long startingDate, List<DepCodeToCategory> depCodeToCategoryList) {
            long now = DateTimeUtils.nowTimestamp();
            DepCategoryRecommendHour depCategoryRecommendHour = new DepCategoryRecommendHour()
                    .setPropertyId(propertyId).setStartingDate(startingDate)
                    .setCreatedBy(BaseConstant.ZERO).setCreatedTime(now);

            // 初始7天工时 map
            Map<String, Object> recommendHours = Maps.newHashMapWithExpectedSize(7);
            IntStream.range(1, 8).forEach(i -> recommendHours.put(i + "", ""));

            Map<String, Map<String, Object>> categoryId2RecommendHour = Maps.newHashMapWithExpectedSize(depCodeToCategoryList.size());
            depCodeToCategoryList.forEach(depCodeToCategory -> categoryId2RecommendHour.put(depCodeToCategory.getDepCategoryId().toString(), recommendHours));
            depCategoryRecommendHour.setCategoryRecomHours(JsonUtils.convertObjToString(objectMapper, categoryId2RecommendHour));
            save(depCategoryRecommendHour);
        }

        /**
         * 新增推荐工时操作可供选择的推荐工时版本列表
         *
         * @param propertyDTO 资产
         * @param query       分页查询参数
         * @return 版本列表
         * @author yyt
         * @date 2025/4/30
         **/
        @Override
        public ListPagedDTO<RecommendHoursHistoryDTO> listNewOperationsAvailable(PropertyDTO propertyDTO, PageSizeQuery query) {
            // 返回当前正在使用的版本 + 未来日期的版本
            Integer propertyId = propertyDTO.getId();
            Long currentDate = DateTimeUtils.dateNowTimestamp(propertyDTO.getTimezone());
            RecommendHoursDTO current = getCurrent(propertyId, currentDate);
            if (Objects.nonNull(current)) {
                currentDate = current.getStartingDate();
            }

            Page<DepCategoryRecommendHour> page = page(new Page<>(query.getPage(), query.getSize()), new QueryWrapper<DepCategoryRecommendHour>().lambda()
                    .eq(DepCategoryRecommendHour::getPropertyId, propertyId)
                    .ge(DepCategoryRecommendHour::getStartingDate, currentDate)
                    .orderByDesc(DepCategoryRecommendHour::getStartingDate)
                    .select(DepCategoryRecommendHour::getId, DepCategoryRecommendHour::getStartingDate));

            return new ListPagedDTO<>(page.getTotal(), beanMapper.mapList(page.getRecords(), RecommendHoursHistoryDTO.class,
                    (dto -> dto.setEffective(Objects.nonNull(current) && dto.getStartingDate().equals(current.getStartingDate())))));
        }

        /**
         * 检查选择的版本必须是当前版本 或 未来版本
         *
         * @param propertyDTO        资产
         * @param categoryRecomHours 推荐工时
         * @author yyt
         * @date 2025/4/30
         **/
        @Override
        public void checkCategoryVersion(PropertyDTO propertyDTO, String categoryRecomHours) {
            Integer propertyId = propertyDTO.getId();
            String timezone = propertyDTO.getTimezone();

            Map<String, Map<String, Object>> categoryId2RecommendHour = JsonUtils.convertStringByTypeReference(objectMapper,
                    categoryRecomHours, new TypeReference<>() {
                    });

            // 检查 Category 存在
            List<Integer> ids = categoryId2RecommendHour.keySet().stream().map(Integer::parseInt).toList();
            List<DepartmentCategory> departmentCategoryList = departmentCategoryService.listByIds(ids);
            if (CollectionUtils.isEmpty(departmentCategoryList) || departmentCategoryList.size() != ids.size()) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The department category not found in current property; propertyId: " + propertyId);
            }

            // Category 所属版本必须一致
            List<Integer> versions = departmentCategoryList.stream().map(DepartmentCategory::getVersion).distinct().toList();
            if (versions.size() != 1) {
                throw new BadRequestException(SvaErrorI18nConstant.DEPARTMENT_VERSION_NOT_CONSISTENT,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "the department category version is not consistent; propertyId: " + propertyId);
            }
            Integer version = versions.get(0);
            if (Objects.equals(version, BaseConstant.ZERO)) return;

            RecommendHoursDTO current = getCurrent(propertyId, DateTimeUtils.dateNowTimestamp(timezone));
            if (Objects.isNull(current)) return;

            // Category 所属版本必须是当前版本 或 未来版本
            if (current.getStartingDate() > DateTimeUtils.strToLocalDate(version.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).atStartOfDay(ZoneId.of(timezone)).toEpochSecond()) {
                throw new BadRequestException(SvaErrorI18nConstant.DEPARTMENT_VERSION_MUST_CURRENT_OR_FUTURE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "the version of department category must be current version or future version; propertyId: " + propertyId);
            }
        }

    }