# SvaActualScheduleRecordServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    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.YearMonthUtils;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaActualHour;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaActualScheduleRecord;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaScheduleHour;
    import cloud.wconnect.workperformance.entity.sva.employees.AttendanceEmployee;
    import cloud.wconnect.workperformance.entity.sva.setting.DepartmentCategory;
    import cloud.wconnect.workperformance.mapper.sva.avhours.SvaActualScheduleRecordMapper;
    import cloud.wconnect.workperformance.pojo.dto.sva.avhours.SvaActualHoursDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.avhours.SvaActualScheduleHoursDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.avhours.SvaScheduleHoursDTO;
    import cloud.wconnect.workperformance.pojo.query.sva.avhours.SvaActualScheduleHoursQuery;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaActualHourService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaActualScheduleRecordService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaScheduleHourService;
    import cloud.wconnect.workperformance.service.sva.employees.IAttendanceEmployeeService;
    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.update.LambdaUpdateWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.time.YearMonth;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * SVA Actual Schedule Record 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-04-22
     */
    @Service
    @DS("work_performance")
    public class SvaActualScheduleRecordServiceImpl extends ServiceImpl<SvaActualScheduleRecordMapper, SvaActualScheduleRecord> implements ISvaActualScheduleRecordService {
        @Lazy
        @Resource
        private ISvaActualHourService actualHourService;
        @Lazy
        @Resource
        private ISvaScheduleHourService scheduleHourService;
        @Resource
        private IAttendanceEmployeeService employeeService;
        @Lazy
        @Resource
        private IDepartmentCategoryService departmentCategoryService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private SqlSessionFactory sqlSessionFactory;

        /**
         * 检查 department category 是否被考勤数据占用
         *
         * @param propertyId 资产 id
         * @param categoryId 部门分类 id
         * @return true：被占用，false：未占用
         * @author zc
         * @date 2025/4/21
         */
        @Override
        public boolean checkOccupied(int propertyId, int categoryId) {
            return checkOccupied(propertyId, List.of(categoryId));
        }

        /**
         * 检查 department category 是否被考勤数据占用
         *
         * @param propertyId  资产 id
         * @param categoryIds 部门分类 id
         * @return true：被占用，false：未占用
         * @author zc
         * @date 2025/4/21
         */
        @Override
        public boolean checkOccupied(int propertyId, List<Integer> categoryIds) {
            boolean existsFromActual = actualHourService.exists(new LambdaUpdateWrapper<SvaActualHour>()
                    .in(SvaActualHour::getDepCategoryId, categoryIds)
                    .eq(SvaActualHour::getPropertyId, propertyId));

            boolean existsFromSchedule = scheduleHourService.exists(new LambdaUpdateWrapper<SvaScheduleHour>()
                    .in(SvaScheduleHour::getDepCategoryId, categoryIds)
                    .eq(SvaScheduleHour::getPropertyId, propertyId));
            return existsFromActual || existsFromSchedule;
        }

        /**
         * 检索考勤信息集合
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @return SvaActualScheduleHoursDTO 集合
         * @author zc
         * @date 2025/4/22
         */
        @Override
        public ListPagedDTO<SvaActualScheduleHoursDTO> listPaged(int propertyId, SvaActualScheduleHoursQuery query) {
            YearMonth targetYearMonth = YearMonthUtils.getYearMonth(query.getTargetDate().toString());
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(targetYearMonth.atDay(1),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(targetYearMonth.atEndOfMonth(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));

            query.setStartDate(startDate);
            query.setEndDate(endDate);
            Page<SvaActualScheduleRecord> page = new Page<>(query.getPage(), query.getSize());
            List<SvaActualScheduleRecord> recordList = baseMapper.listPaged(page, propertyId, query);
            if (CollectionUtils.isEmpty(recordList)) {
                return new ListPagedDTO<>();
            }

            // 获取员工名称
            List<String> positionIds = recordList.stream().map(SvaActualScheduleRecord::getPositionId).distinct().toList();
            Map<String, String> positionId2Name = employeeService.listByPositionIds(propertyId, positionIds, null).stream()
                    .collect(Collectors.toMap(AttendanceEmployee::getPositionId, AttendanceEmployee::getEmpName));

            // 获取 Actual Hour 数据
            List<SvaActualHour> actualHourList = actualHourService.listPositionIdAndPayDateInActualHour(propertyId, recordList);
            Map<String, List<SvaActualHour>> key2ActualHourList = actualHourList.stream()
                    .collect(Collectors.groupingBy(item -> item.getPositionId() + "&" + item.getPayDate()));

            // 获取 Schedule Hour 数据
            List<SvaScheduleHour> scheduleHourList = scheduleHourService.listPositionIdAndPayDateInScheduleHour(propertyId, recordList);
            Map<String, List<SvaScheduleHour>> key2ScheduleHourList = scheduleHourList.stream()
                    .collect(Collectors.groupingBy(item -> item.getPositionId() + "&" + item.getPayDate()));

            // 获取部门分类数据
            Set<Integer> depCategoryIds = Sets.newHashSet();
            depCategoryIds.addAll(actualHourList.stream().map(SvaActualHour::getDepCategoryId).toList());
            depCategoryIds.addAll(scheduleHourList.stream().map(SvaScheduleHour::getDepCategoryId).toList());
            Map<Integer, DepartmentCategory> depCategoryId2DepCategory = departmentCategoryService.listByIds(depCategoryIds).stream()
                    .collect(Collectors.toMap(DepartmentCategory::getId, Function.identity()));

            List<SvaActualScheduleHoursDTO> actualScheduleHoursDTOList = beanMapper.mapList(recordList, SvaActualScheduleHoursDTO.class,
                    (record, recordDTO) -> {
                        recordDTO.setEmployeeName(positionId2Name.get(record.getPositionId()));

                        String key = record.getPositionId() + "&" + record.getPayDate();
                        List<SvaActualHoursDTO> actualHoursList = beanMapper.mapList(key2ActualHourList.getOrDefault(key, Collections.emptyList()),
                                SvaActualHoursDTO.class, (actual, actualDTO) -> {
                                    DepartmentCategory departmentCategory = depCategoryId2DepCategory
                                            .getOrDefault(actual.getDepCategoryId(), new DepartmentCategory());
                                    actualDTO.setDepartment(departmentCategory.getName());
                                }
                        );
                        recordDTO.setActualHoursList(actualHoursList);
                        BigDecimal actualHours = actualHoursList.stream().map(SvaActualHoursDTO::getHours)
                                .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                        recordDTO.setActualHours(actualHours);
                        List<SvaScheduleHoursDTO> scheduleHoursList = beanMapper.mapList(key2ScheduleHourList.getOrDefault(key, Collections.emptyList()),
                                SvaScheduleHoursDTO.class, (schedule, scheduleDTO) -> {
                                    DepartmentCategory departmentCategory = depCategoryId2DepCategory
                                            .getOrDefault(schedule.getDepCategoryId(), new DepartmentCategory());
                                    scheduleDTO.setDepartment(departmentCategory.getName());
                                }
                        );
                        recordDTO.setScheduleHoursList(scheduleHoursList);
                        BigDecimal scheduleHours = scheduleHoursList.stream().map(SvaScheduleHoursDTO::getHours)
                                .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                        recordDTO.setScheduleHours(scheduleHours);

                        BigDecimal variance = Objects.isNull(actualHours) || Objects.isNull(scheduleHours) ?
                                null : actualHours.subtract(scheduleHours);
                        recordDTO.setVariance(variance);
                    }
            );
            return new ListPagedDTO<>(page.getTotal(), actualScheduleHoursDTOList);
        }

        /**
         * 更新 Actual Hour 记录
         *
         * @param propertyDTOList      资产集合
         * @param propertyId2DateRange 日期范围
         * @param actualHourList Actual Hour 数据
         * @author zc
         * @date 2025/4/22
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateActualHourRecord(List<PropertyDTO> propertyDTOList, Map<Integer, List<Long>> propertyId2DateRange, List<SvaActualHour> actualHourList) {
            if (CollectionUtils.isEmpty(propertyDTOList)) {
                return;
            }
            List<Integer> propertyIds = propertyDTOList.stream().map(PropertyDTO::getId).toList();
            Map<Integer, List<SvaActualHour>> propertyId2ActualHourList = actualHourList.stream()
                    .collect(Collectors.groupingBy(SvaActualHour::getPropertyId));
            List<SvaActualScheduleRecord> updateRecorList = Lists.newArrayList();
            for (int propertyId : propertyIds) {
                List<Long> dateRangeList = propertyId2DateRange.get(propertyId);
                if (CollectionUtils.isEmpty(dateRangeList)) {
                    continue;
                }
                // 设置 Actual Use = false
                update(new LambdaUpdateWrapper<SvaActualScheduleRecord>()
                        .set(SvaActualScheduleRecord::getActualUse, false)
                        .eq(SvaActualScheduleRecord::getPropertyId, propertyId)
                        .between(SvaActualScheduleRecord::getPayDate, dateRangeList.get(0), dateRangeList.get(1)));

                List<SvaActualHour> actualHours = propertyId2ActualHourList.getOrDefault(propertyId, Collections.emptyList());
                // 从新数据中插入 Actual Use = true 的记录
                Map<Long, List<String>> positionId2PayDateList = actualHours.stream()
                        .collect(Collectors.groupingBy(SvaActualHour::getPayDate,
                                Collectors.mapping(SvaActualHour::getPositionId, Collectors.toList())));
                positionId2PayDateList.forEach((payDate, positionIds) ->
                        positionIds.forEach(positionId -> {
                            SvaActualScheduleRecord record = new SvaActualScheduleRecord();
                            record.setPropertyId(propertyId);
                            record.setPayDate(payDate);
                            record.setPositionId(positionId);
                            record.setActualUse(true);
                            updateRecorList.add(record);
                        })
                );
            }
            if (CollectionUtils.isNotEmpty(updateRecorList)) {
                insertOrUpdateBatchActualRewrite(updateRecorList);
            }

            // 删除 Actual Use & Schedule Use 都为 false 的记录
            deleteNoUse(propertyIds);
        }

        /**
         * 更新 Schedule Hour 记录
         *
         * @param propertyDTOList      资产集合
         * @param propertyId2DateRange 日期范围
         * @param scheduleHourList     Schedule Hour 数据
         * @author zc
         * @date 2025/4/22
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateScheduleHourRecord(List<PropertyDTO> propertyDTOList, Map<Integer, List<Long>> propertyId2DateRange, List<SvaScheduleHour> scheduleHourList) {
            if (CollectionUtils.isEmpty(propertyDTOList)) {
                return;
            }
            List<Integer> propertyIds = propertyDTOList.stream().map(PropertyDTO::getId).toList();
            Map<Integer, List<SvaScheduleHour>> propertyId2ScheduleHourList = scheduleHourList.stream()
                    .collect(Collectors.groupingBy(SvaScheduleHour::getPropertyId));
            List<SvaActualScheduleRecord> updateRecorList = Lists.newArrayList();
            for (int propertyId : propertyIds) {
                List<Long> dateRangeList = propertyId2DateRange.get(propertyId);
                if (CollectionUtils.isEmpty(dateRangeList)) {
                    continue;
                }
                // 设置 Schedule Use = false
                update(new LambdaUpdateWrapper<SvaActualScheduleRecord>()
                        .set(SvaActualScheduleRecord::getScheduleUse, false)
                        .in(SvaActualScheduleRecord::getPropertyId, propertyId)
                        .between(SvaActualScheduleRecord::getPayDate, dateRangeList.get(0), dateRangeList.get(1)));

                List<SvaScheduleHour> scheduleHours = propertyId2ScheduleHourList.getOrDefault(propertyId, Collections.emptyList());

                // 从新数据中插入 Schedule Use = true 的记录
                Map<Long, List<String>> positionId2PayDateList = scheduleHours.stream()
                        .collect(Collectors.groupingBy(SvaScheduleHour::getPayDate,
                                Collectors.mapping(SvaScheduleHour::getPositionId, Collectors.toList())));
                positionId2PayDateList.forEach((payDate, positionIds) ->
                        positionIds.forEach(positionId -> {
                            SvaActualScheduleRecord record = new SvaActualScheduleRecord();
                            record.setPropertyId(propertyId);
                            record.setPayDate(payDate);
                            record.setPositionId(positionId);
                            record.setScheduleUse(true);
                            updateRecorList.add(record);
                        })
                );
            }
            if (CollectionUtils.isNotEmpty(updateRecorList)) {
                insertOrUpdateBatchScheduleRewrite(updateRecorList);
            }

            // 删除 Schedule Use & Schedule Use 都为 false 的记录
            deleteNoUse(propertyIds);
        }

        /**
         * 批量新增 Actual Record
         *
         * @param recordList Actual Recode List
         * @author zc
         * @date 2025/4/25
         */
        public void insertOrUpdateBatchActualRewrite(List<SvaActualScheduleRecord> recordList) {
            if (CollectionUtils.isEmpty(recordList)) {
                return;
            }

            try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
                SvaActualScheduleRecordMapper mapper = sqlSession.getMapper(SvaActualScheduleRecordMapper.class);
                for (int i = 0; i < recordList.size(); i++) {
                    SvaActualScheduleRecord record = recordList.get(i);
                    mapper.insertOrUpdateActualByUnique(record);
                    // 每 50 条提交一次
                    if ((i + 1) % 50 == 0) {
                        sqlSession.flushStatements();
                    }
                }
                sqlSession.flushStatements();
            }
        }

        /**
         * 批量新增或更新 Schedule Record
         *
         * @param recordList Schedule Recode List
         * @author zc
         * @date 2025/4/25
         */
        public void insertOrUpdateBatchScheduleRewrite(List<SvaActualScheduleRecord> recordList) {
            if (CollectionUtils.isEmpty(recordList)) {
                return;
            }

            try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
                SvaActualScheduleRecordMapper mapper = sqlSession.getMapper(SvaActualScheduleRecordMapper.class);
                for (int i = 0; i < recordList.size(); i++) {
                    SvaActualScheduleRecord record = recordList.get(i);
                    mapper.insertOrUpdateScheduleByUnique(record);
                    // 每 50 条提交一次
                    if ((i + 1) % 50 == 0) {
                        sqlSession.flushStatements();
                    }
                }
                sqlSession.flushStatements();
            }
        }

        /**
         * 根据日期范围获取 Actual Schedule Hour 记录
         *
         * @param propertyId 资产 ID
         * @param startDate  开始日期(yyyyMMdd)
         * @param endDate    结束日期(yyyyMMdd)
         * @return Actual Hour 记录
         * @author zc
         * @date 2025/4/22
         */
        @Override
        public List<SvaActualScheduleRecord> listByDate(int propertyId, long startDate, long endDate) {
            return list(new LambdaQueryWrapper<SvaActualScheduleRecord>()
                    .between(SvaActualScheduleRecord::getPayDate, startDate, endDate)
                    .eq(SvaActualScheduleRecord::getPropertyId, propertyId));
        }

        /**
         * 删除 Actual Use & Schedule Use 都为 false 的记录
         *
         * @param propertyIds 资产 ID 集合
         * @author zc
         * @date 2025/4/22
         */
        private void deleteNoUse(List<Integer> propertyIds) {
            if (CollectionUtils.isEmpty(propertyIds)) {
                return;
            }
            // 删除 Actual Use & Schedule Use 都为 false 的记录
            remove(new LambdaUpdateWrapper<SvaActualScheduleRecord>()
                    .in(SvaActualScheduleRecord::getPropertyId, propertyIds)
                    .eq(SvaActualScheduleRecord::getActualUse, false)
                    .eq(SvaActualScheduleRecord::getScheduleUse, false));
        }
    }