# OvertimeServiceImpl.java

    package cloud.wconnect.workperformance.service.mor.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.exception.ListenerException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.entity.mor.MorDepartment;
    import cloud.wconnect.workperformance.entity.mor.Overtime;
    import cloud.wconnect.workperformance.entity.mor.OvertimeSum;
    import cloud.wconnect.workperformance.mapper.mor.OvertimeMapper;
    import cloud.wconnect.workperformance.pojo.bo.mor.overtime.OvertimeDailyBO;
    import cloud.wconnect.workperformance.pojo.dto.mor.MorDepartmentDTO;
    import cloud.wconnect.workperformance.pojo.dto.mor.overtime.*;
    import cloud.wconnect.workperformance.pojo.query.mor.overtime.OvertimeQuery;
    import cloud.wconnect.workperformance.pojo.query.mor.overtime.OvertimeSumQuery;
    import cloud.wconnect.workperformance.service.mor.IMorDepartmentService;
    import cloud.wconnect.workperformance.service.mor.IOvertimeDataPermissionConfigService;
    import cloud.wconnect.workperformance.service.mor.IOvertimeService;
    import cloud.wconnect.workperformance.service.mor.IOvertimeSumService;
    import cloud.wconnect.workperformance.service.mor.overtime.IMorOvertimeParseService;
    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.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    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.ApplicationContext;
    import org.springframework.stereotype.Service;

    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.time.ZonedDateTime;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 加班信息 服务实现类
     * </p>
     *
     * @author zc
     * @since 2023-04-01
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class OvertimeServiceImpl extends ServiceImpl<OvertimeMapper, Overtime> implements IOvertimeService {
        @Resource
        private IMorDepartmentService morDepartmentService;
        @Resource
        private IOvertimeSumService overtimeSumService;
        @Resource
        private IOvertimeDataPermissionConfigService dataPermissionConfigService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private ApplicationContext applicationContext;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;

        /**
         * 获取 Overtime 列表数据
         *
         * @param tenantId      租户 id
         * @param propertyId    资产 id
         * @param userId        用户 id
         * @param overtimeQuery 查询条件
         * @return OvertimeTotalDTO
         * @author zc
         * @date 2023/4/1
         */
        @Override
        public OvertimeTotalDTO getOvertimeTotal(int tenantId, int propertyId, int userId, OvertimeQuery overtimeQuery) {
            overtimeQuery.setPropertyId(propertyId);
            List<Overtime> overtimeList = list(overtimeQuery);
            if (CollectionUtils.isEmpty(overtimeList)) {
                return new OvertimeTotalDTO();
            }

            // 查询 department desc
            List<String> departmentCodes = overtimeList.stream().map(Overtime::getWorkDepartmentCode).distinct().collect(Collectors.toList());
            Map<String, MorDepartment> code2Department = morDepartmentService.list(new QueryWrapper<MorDepartment>().lambda()
                            .eq(MorDepartment::getPropertyId, propertyId)
                            .in(CollectionUtils.isNotEmpty(departmentCodes), MorDepartment::getCode, departmentCodes))
                    .stream().collect(Collectors.toMap(MorDepartment::getCode, v -> v));

            // 校验当前登录用户是否拥有数据权限
            boolean hasDataPermissionAccess = dataPermissionConfigService.verifyDataPermissionAccess(tenantId, propertyId, userId);

            List<OvertimeDTO> overtimeDTOList = beanMapper.mapList(overtimeList, OvertimeDTO.class,
                    (item, dto) -> {
                        MorDepartmentDTO morDepartmentDTO = new MorDepartmentDTO();
                        morDepartmentDTO.setCode(item.getWorkDepartmentCode());

                        MorDepartment morDepartment = code2Department.get(item.getWorkDepartmentCode());
                        if (Objects.nonNull(morDepartment)) {
                            morDepartmentDTO.setId(morDepartment.getId());
                            morDepartmentDTO.setDesc(morDepartment.getDesc());
                            morDepartmentDTO.setDescByReport(morDepartment.getDescByReport());
                        }
                        dto.setWorkDepartment(morDepartmentDTO);
                        if (!hasDataPermissionAccess) {
                            dto.setPayRate(null);
                            dto.setAmount(null);
                        }
                    });

            BigDecimal hoursTotal = overtimeList.stream().map(Overtime::getHours).reduce(BigDecimal.ZERO, BigDecimal::add);

            BigDecimal dollarsTotal = hasDataPermissionAccess ? overtimeList.stream()
                    .map(Overtime::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add) : null;

            return OvertimeTotalDTO.builder()
                    .totalHours(hoursTotal)
                    .totalAmount(dollarsTotal)
                    .list(overtimeDTOList)
                    .build();
        }

        /**
         * 获取 Overtime 数据
         *
         * @param overtimeQuery 查询条件
         * @return OvertimeDTO 数组
         * @author zc
         * @date 2023/4/1
         */
        private List<Overtime> list(OvertimeQuery overtimeQuery) {
            LambdaQueryWrapper<Overtime> queryWrapper = new QueryWrapper<Overtime>().lambda()
                    .eq(Overtime::getPropertyId, overtimeQuery.getPropertyId())
                    .in(CollectionUtils.isNotEmpty(overtimeQuery.getWorkDepartmentCode()),
                            Overtime::getWorkDepartmentCode, overtimeQuery.getWorkDepartmentCode())
                    .like(StringUtils.isNoneBlank(overtimeQuery.getPositionId()),
                            Overtime::getPositionId, overtimeQuery.getPositionId())
                    .and(StringUtils.isNoneBlank(overtimeQuery.getName()), i ->
                            i.like(Overtime::getFirstName, overtimeQuery.getName())
                                    .or().like(Overtime::getLastName, overtimeQuery.getName()));

            if (overtimeQuery.getType() == OvertimeQuery.TYPE_YEAR) {
                PropertyDTO propertyDTO = propertyI18nRedisService.get(overtimeQuery.getPropertyId());
                ZonedDateTime fromMonth = DateTimeUtils.toZonedDateTime(overtimeQuery.getStartOfMonth(), propertyDTO.getTimezone())
                        .with(TemporalAdjusters.firstDayOfYear());

                queryWrapper.between(Overtime::getStartOfMonth, fromMonth.toEpochSecond(), fromMonth.plusMonths(11).toEpochSecond());
            } else {
                queryWrapper.eq(Overtime::getStartOfMonth, overtimeQuery.getStartOfMonth());
            }
            return list(queryWrapper);
        }

        /**
         * 获取 Overtime Details 年份，月份，部门对应的金额
         *
         * @param propertyDTOList 资产
         * @param query           查询条件
         * @return OvertimeSumForYearDTO 集合
         * @author zc
         * @date 2023/4/1
         */
        @Override
        public List<OvertimeSumForPropertyViewDTO> listSumInfoForPropertyYears(List<PropertyDTO> propertyDTOList, OvertimeSumQuery query) {
            query.setType(OvertimeSum.TYPE_OVERTIME);
            Map<Integer, List<OvertimeSum>> propertyId2OvertimeSums = overtimeSumService.list(query).stream()
                    .collect(Collectors.groupingBy(OvertimeSum::getPropertyId));

            Map<Integer, List<MorDepartment>> propertyId2Departments = morDepartmentService
                    .list(new QueryWrapper<MorDepartment>().lambda()
                            .in(MorDepartment::getPropertyId, query.getPropertyIds())).stream()
                    .collect(Collectors.groupingBy(MorDepartment::getPropertyId));

            return propertyDTOList.stream()
                    .filter(item -> query.getPropertyIds().contains(item.getId()))
                    .map(property -> {
                        List<OvertimeSumForYearDTO> sumForYearDTOList = propertyId2OvertimeSums.getOrDefault(property.getId(), Collections.emptyList()).stream()
                                .map(overtimeSum -> {
                                    OvertimeSumForYearDTO overtimeSumForYearDTO = JsonUtils.convertStringToObject(objectMapper, overtimeSum.getDetail(), OvertimeSumForYearDTO.class);
                                    // 设置 month 的时间戳字段为具体月份，用于前端处理
                                    overtimeSumForYearDTO.getMonthList().forEach(month ->
                                            month.setMonth((long) DateTimeUtils.toZonedDateTime(month.getMonth(), property.getTimezone()).getMonthValue()));
                                    return overtimeSumForYearDTO;
                                }).toList();

                        // 计算不同 property、相同年份的总金额
                        Map<Integer, BigDecimal> year2YearTotal = sumForYearDTOList.stream()
                                .collect(Collectors.groupingBy(OvertimeSumForYearDTO::getYear,
                                        Collectors.reducing(BigDecimal.ZERO, OvertimeSumForYearDTO::getYearTotal, BigDecimal::add)));

                        Map<String, MorDepartment> code2Department = propertyId2Departments.getOrDefault(property.getId(), Collections.emptyList()).stream()
                                .collect(Collectors.toMap(MorDepartment::getCode, v -> v));

                        Map<Integer, List<OvertimeSumForYearDTO>> year2SumForYear = sumForYearDTOList.stream()
                                .collect(Collectors.groupingBy(OvertimeSumForYearDTO::getYear));

                        List<Integer> years = CollectionUtils.isEmpty(query.getYears()) ?
                                Lists.newArrayList(year2SumForYear.keySet()) : query.getYears();

                        return OvertimeSumForPropertyViewDTO.builder().propertyId(property.getId())
                                .details(getSumForYearList(years, year2SumForYear, year2YearTotal, code2Department)).build();
                    }).toList();
        }

        /**
         * 获取对应年的金额统计数据
         *
         * @param years           年
         * @param year2SumForYear year 2 SumForYear
         * @param year2YearTotal  year 2 YearTotal
         * @param code2Department department
         * @return 对应年的金额统计数据
         * @author zc
         * @date 2023/4/1
         */
        private List<OvertimeSumForYearViewDTO> getSumForYearList(List<Integer> years,
                                                                  Map<Integer, List<OvertimeSumForYearDTO>> year2SumForYear,
                                                                  Map<Integer, BigDecimal> year2YearTotal,
                                                                  Map<String, MorDepartment> code2Department) {
            List<OvertimeSumForYearViewDTO> yearDTOList = Lists.newArrayList();
            years.forEach(year -> {
                // 年下面的月份进行分组，计算出相同月份的金额合计
                List<OvertimeSumForYearDTO> sumForYearList = year2SumForYear.getOrDefault(year, Collections.emptyList());
                Map<Long, BigDecimal> month2MonthTotal = sumForYearList.stream().flatMap(item -> item.getMonthList().stream())
                        .collect(Collectors.groupingBy(OvertimeSumForMonthDTO::getMonth,
                                Collectors.reducing(BigDecimal.ZERO, OvertimeSumForMonthDTO::getMonthTotal, BigDecimal::add)));

                // 相同的月份数据进行分组
                Map<Long, List<OvertimeSumForMonthDTO>> month2SumForMonth = sumForYearList.stream()
                        .flatMap(item -> item.getMonthList().stream()).collect(Collectors.groupingBy(OvertimeSumForMonthDTO::getMonth));

                List<OvertimeSumForMonthViewDTO> monthList = getSumForMonthList(month2SumForMonth, month2MonthTotal, code2Department);
                yearDTOList.add(OvertimeSumForYearViewDTO
                        .builder()
                        .year(year)
                        .yearTotal(year2YearTotal.get(year))
                        .monthList(monthList)
                        .build());
            });
            return yearDTOList;
        }

        /**
         * 获取对应月的金额统计数据
         *
         * @param month2SumForMonth month 2 SumForMonth
         * @param month2MonthTotal  month 2 MonthTotal
         * @param code2Department   department
         * @return 对应月的金额统计数据
         * @author zc
         * @date 2023/4/1
         */
        private List<OvertimeSumForMonthViewDTO> getSumForMonthList(Map<Long, List<OvertimeSumForMonthDTO>> month2SumForMonth,
                                                                    Map<Long, BigDecimal> month2MonthTotal,
                                                                    Map<String, MorDepartment> code2Department) {
            List<OvertimeSumForMonthViewDTO> monthList = Lists.newArrayList();
            for (long i = 1; i <= 12; i++) {
                // 月份下面的部门进行分组，计算出相同部门 code 的金额合计
                List<OvertimeSumForMonthDTO> sumForMonthList = month2SumForMonth.getOrDefault(i, Collections.emptyList());
                Map<String, BigDecimal> depCode2DepTotal = sumForMonthList.stream().flatMap(item -> item.getDepartmentList().stream())
                        .collect(Collectors.groupingBy(OvertimeSumForMonthDepartmentDTO::getDepartmentCode,
                                Collectors.reducing(BigDecimal.ZERO, OvertimeSumForMonthDepartmentDTO::getDepartmentTotal, BigDecimal::add)));

                List<OvertimeSumForMonthDepartmentViewDTO> departmentList = getSumForDepList(depCode2DepTotal, code2Department);
                monthList.add(OvertimeSumForMonthViewDTO
                        .builder()
                        .month(i)
                        .monthTotal(month2MonthTotal.get(i))
                        .departmentList(departmentList)
                        .build());
            }
            return monthList;
        }

        /**
         * 获取对应部门的金额统计数据
         *
         * @param depCode2DepTotal depCode 2 DepTotal
         * @param code2Department  department
         * @return 对应部门的金额统计数据
         * @author zc
         * @date 2023/4/1
         */
        private List<OvertimeSumForMonthDepartmentViewDTO> getSumForDepList(Map<String, BigDecimal> depCode2DepTotal,
                                                                            Map<String, MorDepartment> code2Department) {
            List<OvertimeSumForMonthDepartmentViewDTO> departmentList = Lists.newArrayList();
            depCode2DepTotal.forEach((departmentCode, departmentTotal) -> {
                MorDepartmentDTO morDepartmentDTO = new MorDepartmentDTO();
                morDepartmentDTO.setCode(departmentCode);

                MorDepartment morDepartment = code2Department.get(departmentCode);
                if (Objects.nonNull(morDepartment)) {
                    morDepartmentDTO.setId(morDepartment.getId());
                    morDepartmentDTO.setDesc(morDepartment.getDesc());
                    morDepartmentDTO.setDescByReport(morDepartment.getDescByReport());
                }
                departmentList.add(OvertimeSumForMonthDepartmentViewDTO
                        .builder()
                        .department(morDepartmentDTO)
                        .departmentTotal(depCode2DepTotal.get(departmentCode))
                        .build());
            });
            return departmentList;
        }

        /**
         * 解析报告
         *
         * @param propertyDTOList      资产集合
         * @param in                   文件流
         * @param source               数据源
         * @param propertyId2DateRange 数据日期范围
         * @return OvertimeListBO 集合
         * @author zc
         * @date 2024/8/17
         */
        @Override
        public List<OvertimeDailyBO> parseReportForOvertime(List<PropertyDTO> propertyDTOList, InputStream in,
                                                            String source, Map<Integer, List<Long>> propertyId2DateRange) {
            Collection<IMorOvertimeParseService> parserList = applicationContext.getBeansOfType(IMorOvertimeParseService.class).values();
            for (IMorOvertimeParseService overtime : parserList) {
                if (overtime.supports(source)) {
                    return overtime.parse(propertyDTOList, in, propertyId2DateRange);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 根据日期范围获取 Overtime 数据
         *
         * @param propertyId     资产 id
         * @param startTimestamp 开始日期时间戳
         * @param endTimestamp   结束日期时间戳
         * @return List<Overtime>
         * @author zc
         * @date 2025/3/17
         */
        @Override
        public List<Overtime> listByDateRange(int propertyId, long startTimestamp, long endTimestamp) {
            return list(new LambdaQueryWrapper<Overtime>()
                    .eq(Overtime::getPropertyId, propertyId)
                    .between(Overtime::getStartOfMonth, startTimestamp, endTimestamp));
        }

        /**
         * 根据日期范围删除 Overtime 数据
         *
         * @param propertyId     资产 id
         * @param startTimestamp 开始日期时间戳
         * @param endTimestamp   结束日期时间戳
         * @author zc
         * @date 2025/3/18
         */
        @Override
        public void deleteByDateRange(int propertyId, long startTimestamp, long endTimestamp) {
            remove(new LambdaQueryWrapper<Overtime>()
                    .eq(Overtime::getPropertyId, propertyId)
                    .between(Overtime::getStartOfMonth, startTimestamp, endTimestamp));
        }

    }