package cn.iocoder.lyzsys.module.tjt.service.report;

import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.lyzsys.module.tjt.controller.admin.report.vo.*;
import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRolePersonRespVO;
import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitRespVO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.employee.EmployeeDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.employeeyearcostbudget.EmployeeYearCostBudgetDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.employeeyearleaderoutput.EmployeeYearLeaderOutputDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.office.OfficeDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningguidedetail.ProjectPlanningGuideDetailDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.projectroleperson.ProjectRolePersonDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplitperson.SpecialtyRoleSplitPersonDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.yearkvalue.YearKValueDO;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.employee.EmployeeMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.employeeyearcostbudget.EmployeeYearCostBudgetMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.employeeyearleaderoutput.EmployeeYearLeaderOutputMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.office.OfficeMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planningquarter.ProjectPlanningQuarterMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.projectroleperson.ProjectRolePersonMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.specialtyrolesplitperson.SpecialtyRoleSplitPersonMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.yearkvalue.YearKValueMapper;
import cn.iocoder.lyzsys.module.tjt.enums.OutputSplitBizConstants;
import cn.iocoder.lyzsys.module.tjt.enums.ProjectPlanningBizTypeConstants;
import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
import cn.iocoder.lyzsys.module.tjt.service.planningguidedetail.ProjectPlanningGuideDetailService;
import cn.iocoder.lyzsys.module.tjt.service.report.builder.*;
import cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit.SpecialtyRoleSplitService;
import lombok.Data;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.*;

@Service
@Validated
public class ProjectOutputReportServiceImpl implements ProjectOutputReportService {

    private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
    private static final BigDecimal DEFAULT_K_VALUE = new BigDecimal("0.4000");
    private static final BigDecimal FULL_RATIO = new BigDecimal("1.0000");

    private static final String SORT_OUTPUT_DESC = "output_desc";
    private static final String SORT_OUTPUT_ASC = "output_asc";
    private static final String SORT_NAME_ASC = "name_asc";
    private static final String SORT_ANNUAL_TOTAL_DESC = "annual_total_desc";
    private static final String SORT_ANNUAL_TOTAL_ASC = "annual_total_asc";

    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;
    @Resource
    private ProjectRolePersonMapper projectRolePersonMapper;
    @Resource
    private SpecialtyRoleSplitPersonMapper specialtyRoleSplitPersonMapper;
    @Resource
    private EmployeeMapper employeeMapper;
    @Resource
    private OfficeMapper officeMapper;
    @Resource
    private EmployeeYearCostBudgetMapper employeeYearCostBudgetMapper;
    @Resource
    private EmployeeYearLeaderOutputMapper employeeYearLeaderOutputMapper;
    @Resource
    private YearKValueMapper yearKValueMapper;
    @Resource
    private ProjectOutputSplitService projectOutputSplitService;
    @Resource
    private SpecialtyRoleSplitService specialtyRoleSplitService;
    @Resource
    private ProjectPlanningGuideDetailService projectPlanningGuideDetailService;
    @Resource
    private ProjectBudgetExcelBuilder projectBudgetExcelBuilder;
    @Resource
    private ProjectQuarterOutputExcelBuilder projectQuarterOutputExcelBuilder;
    @Resource
    private ProjectLeadQuarterOutputExcelBuilder projectLeadQuarterOutputExcelBuilder;
    @Resource
    private ProjectOverviewOutputExcelBuilder projectOverviewOutputExcelBuilder;
    @Resource
    private EmployeeOutputSummaryExcelBuilder employeeOutputSummaryExcelBuilder;
    @Resource
    private SpecialtyPersonOutputExcelBuilder specialtyPersonOutputExcelBuilder;

    @Override
    public void exportProjectBudgetExcel(HttpServletResponse response, ProjectBudgetExportReqVO reqVO)
            throws IOException {
        ProjectDO project = validateProjectExists(reqVO.getProjectId());
        Integer reportYear = resolveExportYear(reqVO.getYear());
        List<ProjectPlanningDO> planningList = sortPlanningList(projectPlanningMapper.selectListByProjectId(project.getId()));
        List<ProjectRolePersonDO> rolePersons = projectRolePersonMapper.selectListByProjectId(project.getId());
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        Map<Long, List<ProjectPlanningGuideDetailDO>> guideDetailMap = projectPlanningGuideDetailService
                .getProjectPlanningGuideDetailMapByPlanningIds(planningList.stream()
                        .map(ProjectPlanningDO::getId)
                        .collect(Collectors.toList()));

        ProjectBudgetExcelBuilder.ExportData data = new ProjectBudgetExcelBuilder.ExportData();
        data.setProjectCode("");
        data.setProjectName(project.getProjectName());
        data.setProjectManagerName(joinProjectRoleNames(rolePersons, OutputSplitBizConstants.ROLE_PROJECT_MANAGER));
        data.setEngineeringPrincipalName(joinProjectRoleNames(rolePersons, OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL));
        data.setTotalConstructionArea(amount(project.getTotalConstructionArea()));
        data.setContractUnitPrice(firstNonZero(planningList.stream()
                .map(ProjectPlanningDO::getContractUnitPrice)
                .collect(Collectors.toList())));
        data.setInternalGuidanceUnitPrice(firstNonZero(planningList.stream()
                .map(ProjectPlanningDO::getInternalGuidanceUnitPrice)
                .collect(Collectors.toList())));
        data.setRemark(joinPlanningRemarks(planningList));

        BigDecimal totalAssessmentArea = ZERO_AMOUNT;
        BigDecimal totalAssessmentOutputValue = ZERO_AMOUNT;
        for (ProjectPlanningDO planning : planningList) {
            totalAssessmentArea = totalAssessmentArea.add(amount(planning.getAssessmentArea())).setScale(2, RoundingMode.HALF_UP);
            totalAssessmentOutputValue = totalAssessmentOutputValue.add(amount(planning.getAssessmentOutputValue()))
                    .setScale(2, RoundingMode.HALF_UP);
        }
        data.setBudgetRows(buildProjectBudgetRows(planningList, guideDetailMap));
        data.setTotalAssessmentArea(totalAssessmentArea);
        data.setTotalAssessmentOutputValue(totalAssessmentOutputValue);
        data.setTotalAssessmentOutputValueWan(amountToWan(totalAssessmentOutputValue));
        data.setQuarterBudgetRows(buildQuarterBudgetRows(planningList, quarterMap, reportYear));
        data.setYearRangeText(buildProjectBudgetYearRangeText(planningList, quarterMap));

        projectBudgetExcelBuilder.writeWorkbook(response,
                buildFileName(project.getProjectName(), reportYear, "项目考核产值预算表"),
                projectBudgetExcelBuilder.build(data));
    }

    @Override
    public void exportProjectQuarterOutputExcel(HttpServletResponse response, ProjectQuarterOutputExportReqVO reqVO)
            throws IOException {
        ProjectPlanningDO anchorPlanning = validatePlanningExists(reqVO.getPlanningId());
        ProjectDO project = validateProjectExists(anchorPlanning.getProjectId());
        Integer reportYear = resolveExportYear(reqVO.getYear());
        List<ProjectPlanningDO> planningList = sortPlanningList(projectPlanningMapper.selectListByProjectId(project.getId()));
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getExistingOutputSplitMap(planningList);

        ProjectQuarterOutputExcelBuilder.ExportData data = new ProjectQuarterOutputExcelBuilder.ExportData();
        data.setProjectCode("");
        data.setProjectName(project.getProjectName());
        data.setYear(reportYear);
        data.setCenterSignerLabel("设计中心相关负责人（签名）：");
        data.setProjectSignerLabel("项目经理/工程负责人（签名）：");
        data.setRows(buildProjectQuarterOutputRows(planningList, quarterMap, outputSplitMap, reportYear));

        projectQuarterOutputExcelBuilder.writeWorkbook(response,
                buildFileName(project.getProjectName(), reportYear, "专业间项目考核产值年度季度计取表"),
                projectQuarterOutputExcelBuilder.build(data));
    }

    @Override
    public void exportProjectLeadQuarterOutputExcel(HttpServletResponse response,
                                                    ProjectLeadQuarterOutputExportReqVO reqVO)
            throws IOException {
        ProjectPlanningDO anchorPlanning = validatePlanningExists(reqVO.getPlanningId());
        ProjectDO project = validateProjectExists(anchorPlanning.getProjectId());
        Integer reportYear = resolveExportYear(reqVO.getYear());
        List<ProjectPlanningDO> planningList = projectPlanningMapper.selectListByProjectId(project.getId()).stream()
                .sorted(Comparator
                        .comparingInt((ProjectPlanningDO item) -> projectLeadOutputTypeOrder(item.getOwnershipType()))
                        .thenComparing(ProjectPlanningDO::getId))
                .collect(Collectors.toList());
        List<ProjectRolePersonDO> projectRolePersons = projectRolePersonMapper.selectListByProjectId(project.getId());
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getOutputSplitMap(planningList);
        Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap = getRoleSplitMap(planningList);

        ProjectLeadQuarterOutputExcelBuilder.ExportData data = new ProjectLeadQuarterOutputExcelBuilder.ExportData();
        data.setProjectName(project.getProjectName());
        data.setYear(reportYear);
        data.setProjectManagerNames(joinProjectRoleNames(projectRolePersons,
                OutputSplitBizConstants.ROLE_PROJECT_MANAGER, "/"));
        data.setEngineeringPrincipalNames(joinProjectRoleNames(projectRolePersons,
                OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, "/"));
        data.setCenterSignerLabel("设计中心相关负责人（签名）：");
        data.setProjectSignerLabel("项目经理/工程负责人（签名）：");

        List<ProjectLeadQuarterOutputExcelBuilder.LeadQuarterRow> rows = new ArrayList<>();
        for (ProjectPlanningDO planning : planningList) {
            ProjectOutputSplitDO outputSplit = resolveQuarterOutputSplit(planning, outputSplitMap);
            BigDecimal projectLeadRatio = ratio(outputSplit.getProjectLeadRatio());
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            List<SpecialtyRoleSplitRespVO> roleList = roleSplitMap.getOrDefault(planning.getId(), Collections.emptyList());
            SpecialtyRoleSplitRespVO projectManagerRow = findRoleRow(roleList,
                    OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD, OutputSplitBizConstants.ROLE_PROJECT_MANAGER);
            SpecialtyRoleSplitRespVO engineeringPrincipalRow = findRoleRow(roleList,
                    OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD, OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL);

            BigDecimal projectManagerRatio = projectManagerRow == null ? ZERO_RATIO : ratio(projectManagerRow.getRoleRatio());
            BigDecimal engineeringPrincipalRatio = engineeringPrincipalRow == null
                    ? ZERO_RATIO : ratio(engineeringPrincipalRow.getRoleRatio());

            ProjectLeadQuarterOutputExcelBuilder.LeadQuarterRow row =
                    new ProjectLeadQuarterOutputExcelBuilder.LeadQuarterRow();
            row.setSerialNo(rows.size() + 1);
            row.setOutputType(resolveBudgetCategoryLabel(planning.getOwnershipType()));
            row.setDesignContent(defaultString(planning.getPlanningContent()));
            row.setProjectManagerNames(joinRoleSplitPersonNames(projectManagerRow, "/"));
            row.setProjectManagerRatio(projectManagerRatio);
            row.setEngineeringPrincipalNames(joinRoleSplitPersonNames(engineeringPrincipalRow, "/"));
            row.setEngineeringPrincipalRatio(engineeringPrincipalRatio);
            row.setProjectManagerQuarterOneAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterOneAmount(),
                    projectLeadRatio, projectManagerRatio)));
            row.setProjectManagerQuarterTwoAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterTwoAmount(),
                    projectLeadRatio, projectManagerRatio)));
            row.setProjectManagerQuarterThreeAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterThreeAmount(),
                    projectLeadRatio, projectManagerRatio)));
            row.setProjectManagerQuarterFourAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterFourAmount(),
                    projectLeadRatio, projectManagerRatio)));
            row.setProjectManagerYearTotalAmountWan(amountToWan(multiplyAmount(quarterSummary.getYearTotalAmount(),
                    projectLeadRatio, projectManagerRatio)));
            row.setEngineeringPrincipalQuarterOneAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterOneAmount(),
                    projectLeadRatio, engineeringPrincipalRatio)));
            row.setEngineeringPrincipalQuarterTwoAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterTwoAmount(),
                    projectLeadRatio, engineeringPrincipalRatio)));
            row.setEngineeringPrincipalQuarterThreeAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterThreeAmount(),
                    projectLeadRatio, engineeringPrincipalRatio)));
            row.setEngineeringPrincipalQuarterFourAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterFourAmount(),
                    projectLeadRatio, engineeringPrincipalRatio)));
            row.setEngineeringPrincipalYearTotalAmountWan(amountToWan(multiplyAmount(quarterSummary.getYearTotalAmount(),
                    projectLeadRatio, engineeringPrincipalRatio)));
            rows.add(row);
        }
        data.setRows(rows);

        projectLeadQuarterOutputExcelBuilder.writeWorkbook(response,
                buildFileName(project.getProjectName(), reportYear, "项目经理工程负责人工作量及考核产值表"),
                projectLeadQuarterOutputExcelBuilder.build(data));
    }

    @Override
    public void exportSpecialtyPersonOutputExcel(HttpServletResponse response,
                                                 SpecialtyPersonOutputExportReqVO reqVO) throws IOException {
        SpecialtyPersonOutputBuildResult result = buildSpecialtyPersonOutputData(reqVO);
        SpecialtyPersonOutputExcelBuilder.ExportData data = result.getData();

        specialtyPersonOutputExcelBuilder.writeWorkbook(response,
                buildFileName(result.getProject().getProjectName(), result.getReportYear(),
                        data.getSpecialtyName() + "专业内人员年度季度计取表"),
                specialtyPersonOutputExcelBuilder.build(data));
    }

    @Override
    public SpecialtyPersonOutputPreviewRespVO getSpecialtyPersonOutputPreview(SpecialtyPersonOutputPreviewReqVO reqVO) {
        ProjectPlanningDO anchorPlanning = validatePlanningExists(reqVO.getPlanningId());
        ProjectDO project = validateProjectExists(anchorPlanning.getProjectId());
        Integer reportYear = resolveExportYear(reqVO.getYear());
        List<ProjectPlanningDO> planningList = Collections.singletonList(anchorPlanning);
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        List<ProjectPlanningDO> relevantPlanningList = getSpecialtyPersonRelevantPlanningList(anchorPlanning);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getOutputSplitMap(relevantPlanningList);
        Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap = getRoleSplitMap(relevantPlanningList);
        List<SpecialtyRoleSplitRespVO> anchorRoleList =
                specialtyRoleSplitService.getSpecialtyRoleSplitListByPlanningId(anchorPlanning.getId());

        SpecialtyPersonOutputPreviewRespVO respVO = new SpecialtyPersonOutputPreviewRespVO();
        respVO.setPlanningId(anchorPlanning.getId());
        respVO.setYear(reportYear);
        respVO.setProjectName(project.getProjectName());
        respVO.setGroups(buildSpecialtyPersonPreviewGroups(anchorRoleList, relevantPlanningList, quarterMap,
                outputSplitMap, roleSplitMap, reportYear));
        return respVO;
    }

    private List<SpecialtyPersonOutputPreviewRespVO.GroupRow> buildSpecialtyPersonPreviewGroups(
            List<SpecialtyRoleSplitRespVO> anchorRoleList,
            List<ProjectPlanningDO> relevantPlanningList,
            Map<Long, List<ProjectPlanningQuarterDO>> quarterMap,
            Map<Long, ProjectOutputSplitDO> outputSplitMap,
            Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap,
            Integer reportYear) {
        if (anchorRoleList == null || anchorRoleList.isEmpty()) {
            return Collections.emptyList();
        }
        Map<String, List<SpecialtyRoleSplitRespVO>> groupMap = anchorRoleList.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.groupingBy(SpecialtyRoleSplitRespVO::getSpecialtyCode,
                        LinkedHashMap::new, Collectors.toList()));
        List<SpecialtyPersonOutputPreviewRespVO.GroupRow> groups = new ArrayList<>();
        for (Map.Entry<String, List<SpecialtyRoleSplitRespVO>> entry : groupMap.entrySet()) {
            String specialtyCode = entry.getKey();
            if (!isSpecialtyPersonExportable(specialtyCode)) {
                continue;
            }
            List<SpecialtyRoleSplitRespVO> roleRows = entry.getValue().stream()
                    .sorted(Comparator.comparing(SpecialtyRoleSplitRespVO::getSortNo,
                            Comparator.nullsLast(Integer::compareTo)))
                    .collect(Collectors.toList());
            SpecialtyQuarterPreviewAggregate aggregate = buildSpecialtyQuarterPreviewAggregate(relevantPlanningList,
                    quarterMap, outputSplitMap, roleSplitMap, specialtyCode, reportYear);

            SpecialtyPersonOutputPreviewRespVO.GroupRow group = new SpecialtyPersonOutputPreviewRespVO.GroupRow();
            group.setSpecialtyCode(specialtyCode);
            group.setSpecialtyName(resolveSpecialtyName(roleRows, specialtyCode));
            group.setExportable(Boolean.TRUE);
            group.setSpecialtyAmount(amount(roleRows.isEmpty() ? null : roleRows.get(0).getSpecialtyAmount()));
            group.setRoleTotal(sumRoleRatio(roleRows));
            group.setPersonCount(countPreviewPersons(roleRows));
            group.setOverRatio(group.getRoleTotal().compareTo(FULL_RATIO) > 0);
            group.setSpecialtyQuarterOneAmountWan(aggregate.getSpecialtyQuarterOneAmountWan());
            group.setSpecialtyQuarterTwoAmountWan(aggregate.getSpecialtyQuarterTwoAmountWan());
            group.setSpecialtyQuarterThreeAmountWan(aggregate.getSpecialtyQuarterThreeAmountWan());
            group.setSpecialtyQuarterFourAmountWan(aggregate.getSpecialtyQuarterFourAmountWan());
            group.setSpecialtyYearTotalAmountWan(aggregate.getSpecialtyYearTotalAmountWan());
            group.setRows(buildSpecialtyPreviewRoleRows(roleRows, aggregate));
            groups.add(group);
        }
        return groups;
    }

    private SpecialtyQuarterPreviewAggregate buildSpecialtyQuarterPreviewAggregate(
            List<ProjectPlanningDO> relevantPlanningList,
            Map<Long, List<ProjectPlanningQuarterDO>> quarterMap,
            Map<Long, ProjectOutputSplitDO> outputSplitMap,
            Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap,
            String specialtyCode,
            Integer reportYear) {
        SpecialtyQuarterPreviewAggregate aggregate = new SpecialtyQuarterPreviewAggregate();
        if (relevantPlanningList == null || relevantPlanningList.isEmpty()) {
            return aggregate;
        }
        for (ProjectPlanningDO planning : relevantPlanningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            ProjectOutputSplitDO outputSplit = outputSplitMap.get(planning.getId());
            BigDecimal specialtyRatio = getSpecialtyRatio(outputSplit, specialtyCode);
            BigDecimal officeRatio = outputSplit == null ? ZERO_RATIO : ratio(outputSplit.getOfficeRatio());
            BigDecimal specialtyBaseRatio = multiplyRatio(officeRatio, specialtyRatio);

            aggregate.setSpecialtyQuarterOneAmountWan(addAmountValue(aggregate.getSpecialtyQuarterOneAmountWan(),
                    amountToWan(multiplyAmount(quarterSummary.getQuarterOneAmount(), specialtyBaseRatio))));
            aggregate.setSpecialtyQuarterTwoAmountWan(addAmountValue(aggregate.getSpecialtyQuarterTwoAmountWan(),
                    amountToWan(multiplyAmount(quarterSummary.getQuarterTwoAmount(), specialtyBaseRatio))));
            aggregate.setSpecialtyQuarterThreeAmountWan(addAmountValue(aggregate.getSpecialtyQuarterThreeAmountWan(),
                    amountToWan(multiplyAmount(quarterSummary.getQuarterThreeAmount(), specialtyBaseRatio))));
            aggregate.setSpecialtyQuarterFourAmountWan(addAmountValue(aggregate.getSpecialtyQuarterFourAmountWan(),
                    amountToWan(multiplyAmount(quarterSummary.getQuarterFourAmount(), specialtyBaseRatio))));
            aggregate.setSpecialtyYearTotalAmountWan(addAmountValue(aggregate.getSpecialtyYearTotalAmountWan(),
                    amountToWan(multiplyAmount(quarterSummary.getYearTotalAmount(), specialtyBaseRatio))));

            List<SpecialtyRoleSplitRespVO> specialtyRoleRows = (roleSplitMap == null ? Collections.<SpecialtyRoleSplitRespVO>emptyList()
                    : roleSplitMap.getOrDefault(planning.getId(), Collections.emptyList()))
                    .stream()
                    .filter(item -> Objects.equals(item.getSpecialtyCode(), specialtyCode))
                    .collect(Collectors.toList());
            for (SpecialtyRoleSplitRespVO roleRow : specialtyRoleRows) {
                if (roleRow.getPersons() == null || roleRow.getPersons().isEmpty()) {
                    continue;
                }
                BigDecimal roleRatio = ratio(roleRow.getRoleRatio());
                for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
                    if (!isValidSpecialtyRolePerson(person)) {
                        continue;
                    }
                    BigDecimal personRatio = ratio(person.getPersonRatio());
                    BigDecimal amountRatio = multiplyRatio(specialtyBaseRatio, roleRatio, personRatio);
                    String rolePersonKey = buildSpecialtyRolePersonModuleKey(roleRow.getRoleCode(),
                            person.getEmployeeId(), person.getEmployeeName());
                    SpecialtyPersonOutputExcelBuilder.PersonAmountValue value = aggregate.getRolePersonAmountMap()
                            .computeIfAbsent(rolePersonKey, key -> new SpecialtyPersonOutputExcelBuilder.PersonAmountValue());
                    accumulateSpecialtyPersonAmount(value, quarterSummary, amountRatio);
                }
            }
        }
        return aggregate;
    }

    private List<SpecialtyPersonOutputPreviewRespVO.RoleRow> buildSpecialtyPreviewRoleRows(
            List<SpecialtyRoleSplitRespVO> roleRows,
            SpecialtyQuarterPreviewAggregate aggregate) {
        List<SpecialtyPersonOutputPreviewRespVO.RoleRow> result = new ArrayList<>();
        for (SpecialtyRoleSplitRespVO roleRow : roleRows) {
            SpecialtyPersonOutputPreviewRespVO.RoleRow row = new SpecialtyPersonOutputPreviewRespVO.RoleRow();
            row.setSpecialtyCode(roleRow.getSpecialtyCode());
            row.setSpecialtyName(roleRow.getSpecialtyName());
            row.setRoleCode(roleRow.getRoleCode());
            row.setRoleName(roleRow.getRoleName());
            row.setRoleRatio(ratio(roleRow.getRoleRatio()));
            row.setRoleAmount(amount(roleRow.getRoleAmount()));
            row.setPersonTotalRatio(sumPersonRatio(roleRow.getPersons()));
            row.setPersons(buildSpecialtyPreviewPersons(roleRow, aggregate));
            result.add(row);
        }
        return result;
    }

    private List<SpecialtyPersonOutputPreviewRespVO.PersonRow> buildSpecialtyPreviewPersons(
            SpecialtyRoleSplitRespVO roleRow,
            SpecialtyQuarterPreviewAggregate aggregate) {
        if (roleRow.getPersons() == null || roleRow.getPersons().isEmpty()) {
            return Collections.emptyList();
        }
        List<SpecialtyPersonOutputPreviewRespVO.PersonRow> result = new ArrayList<>();
        BigDecimal roleRatio = ratio(roleRow.getRoleRatio());
        for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
            if (!isValidSpecialtyRolePerson(person)) {
                continue;
            }
            BigDecimal personRatio = ratio(person.getPersonRatio());
            String personKey = buildSpecialtyPersonModuleKey(person.getEmployeeId(), person.getEmployeeName());
            String rolePersonKey = buildSpecialtyRolePersonModuleKey(roleRow.getRoleCode(),
                    person.getEmployeeId(), person.getEmployeeName());
            SpecialtyPersonOutputExcelBuilder.PersonAmountValue value =
                    aggregate.getRolePersonAmountMap().get(rolePersonKey);
            SpecialtyPersonOutputPreviewRespVO.PersonRow row = new SpecialtyPersonOutputPreviewRespVO.PersonRow();
            row.setPersonKey(personKey);
            row.setEmployeeId(person.getEmployeeId());
            row.setEmployeeName(defaultString(person.getEmployeeName()));
            row.setPersonRatio(personRatio);
            row.setPersonAmount(amount(person.getPersonAmount()));
            row.setRoleNames(defaultString(roleRow.getRoleName()));
            row.setAdjustedPersonRatio(multiplyRatio(roleRatio, personRatio));
            row.setQuarterOneRatio(value == null ? ZERO_RATIO : ratio(value.getQuarterOneRatio()));
            row.setQuarterOneAmountWan(value == null ? ZERO_AMOUNT : amount(value.getQuarterOneAmountWan()));
            row.setQuarterTwoRatio(value == null ? ZERO_RATIO : ratio(value.getQuarterTwoRatio()));
            row.setQuarterTwoAmountWan(value == null ? ZERO_AMOUNT : amount(value.getQuarterTwoAmountWan()));
            row.setQuarterThreeRatio(value == null ? ZERO_RATIO : ratio(value.getQuarterThreeRatio()));
            row.setQuarterThreeAmountWan(value == null ? ZERO_AMOUNT : amount(value.getQuarterThreeAmountWan()));
            row.setQuarterFourRatio(value == null ? ZERO_RATIO : ratio(value.getQuarterFourRatio()));
            row.setQuarterFourAmountWan(value == null ? ZERO_AMOUNT : amount(value.getQuarterFourAmountWan()));
            row.setYearTotalRatio(value == null ? ZERO_RATIO : ratio(value.getYearTotalRatio()));
            row.setYearTotalAmountWan(value == null ? ZERO_AMOUNT : amount(value.getYearTotalAmountWan()));
            result.add(row);
        }
        return result;
    }

    private boolean isSpecialtyPersonExportable(String specialtyCode) {
        return OutputSplitBizConstants.isValidSpecialtyCode(specialtyCode)
                && !OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode);
    }

    private String resolveSpecialtyName(List<SpecialtyRoleSplitRespVO> rows, String specialtyCode) {
        if (rows != null) {
            for (SpecialtyRoleSplitRespVO row : rows) {
                if (row != null && !defaultString(row.getSpecialtyName()).isEmpty()) {
                    return defaultString(row.getSpecialtyName());
                }
            }
        }
        return OutputSplitBizConstants.getSpecialtyName(specialtyCode);
    }

    private BigDecimal sumRoleRatio(List<SpecialtyRoleSplitRespVO> roleRows) {
        BigDecimal total = ZERO_RATIO;
        if (roleRows == null) {
            return total;
        }
        for (SpecialtyRoleSplitRespVO row : roleRows) {
            total = addRatio(total, row.getRoleRatio());
        }
        return total;
    }

    private BigDecimal sumPersonRatio(List<SpecialtyRolePersonRespVO> persons) {
        BigDecimal total = ZERO_RATIO;
        if (persons == null) {
            return total;
        }
        for (SpecialtyRolePersonRespVO person : persons) {
            if (isValidSpecialtyRolePerson(person)) {
                total = addRatio(total, person.getPersonRatio());
            }
        }
        return total;
    }

    private int countPreviewPersons(List<SpecialtyRoleSplitRespVO> roleRows) {
        if (roleRows == null) {
            return 0;
        }
        int total = 0;
        for (SpecialtyRoleSplitRespVO row : roleRows) {
            if (row.getPersons() == null) {
                continue;
            }
            for (SpecialtyRolePersonRespVO person : row.getPersons()) {
                if (isValidSpecialtyRolePerson(person)) {
                    total++;
                }
            }
        }
        return total;
    }

    private SpecialtyPersonOutputBuildResult buildSpecialtyPersonOutputData(SpecialtyPersonOutputExportReqVO reqVO) {
        validateSpecialtyCode(reqVO.getSpecialtyCode());
        ProjectPlanningDO anchorPlanning = validatePlanningExists(reqVO.getPlanningId());
        ProjectDO project = validateProjectExists(anchorPlanning.getProjectId());
        Integer reportYear = resolveExportYear(reqVO.getYear());
        List<ProjectPlanningDO> planningList = Collections.singletonList(anchorPlanning);
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        List<ProjectPlanningDO> relevantPlanningList = getSpecialtyPersonRelevantPlanningList(anchorPlanning);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getOutputSplitMap(relevantPlanningList);
        Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap = getRoleSplitMap(relevantPlanningList);
        List<ProjectRolePersonDO> projectRolePersons = projectRolePersonMapper.selectListByProjectId(project.getId());
        List<SpecialtyRoleSplitRespVO> anchorRoleList = roleSplitMap.get(anchorPlanning.getId());
        if (anchorRoleList == null) {
            anchorRoleList = specialtyRoleSplitService.getSpecialtyRoleSplitListByPlanningId(anchorPlanning.getId());
        }
        SpecialtyRoleSplitRespVO projectManagerRow = findRoleRow(anchorRoleList,
                OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD, OutputSplitBizConstants.ROLE_PROJECT_MANAGER);
        SpecialtyRoleSplitRespVO engineeringPrincipalRow = findRoleRow(anchorRoleList,
                OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD, OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL);
        String projectManagerNames = joinRoleSplitPersonNames(projectManagerRow, "/");
        if (projectManagerNames.isEmpty()) {
            projectManagerNames = joinProjectRoleNames(projectRolePersons,
                    OutputSplitBizConstants.ROLE_PROJECT_MANAGER, "/");
        }
        String engineeringPrincipalNames = joinRoleSplitPersonNames(engineeringPrincipalRow, "/");
        if (engineeringPrincipalNames.isEmpty()) {
            engineeringPrincipalNames = joinProjectRoleNames(projectRolePersons,
                    OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, "/");
        }

        SpecialtyPersonOutputExcelBuilder.ExportData data = new SpecialtyPersonOutputExcelBuilder.ExportData();
        data.setProjectCode("");
        data.setProjectName(project.getProjectName());
        data.setSpecialtyName(OutputSplitBizConstants.getSpecialtyName(reqVO.getSpecialtyCode()));
        data.setProjectManagerNames(projectManagerNames);
        data.setEngineeringPrincipalNames(engineeringPrincipalNames);
        data.setProjectManagerRatio(projectManagerRow == null ? ZERO_RATIO : ratio(projectManagerRow.getRoleRatio()));
        data.setEngineeringPrincipalRatio(engineeringPrincipalRow == null
                ? ZERO_RATIO : ratio(engineeringPrincipalRow.getRoleRatio()));
        data.setYear(reportYear);
        data.setCenterSignerLabel("设计中心相关负责人（签名）：");
        data.setProjectSignerLabel("项目经理 / 工程负责人（签名）：");
        data.setRolePersonModules(buildSpecialtyRolePersonModules(relevantPlanningList, roleSplitMap, reqVO.getSpecialtyCode()));
        data.setPersonAmountModules(buildSpecialtyPersonAmountModules(relevantPlanningList, roleSplitMap, reqVO.getSpecialtyCode()));
        data.setRows(buildSpecialtyPlanningRows(relevantPlanningList, quarterMap, outputSplitMap,
                roleSplitMap, project.getProjectName(), reqVO.getSpecialtyCode(), reportYear));

        SpecialtyPersonOutputBuildResult result = new SpecialtyPersonOutputBuildResult();
        result.setAnchorPlanning(anchorPlanning);
        result.setProject(project);
        result.setReportYear(reportYear);
        result.setData(data);
        return result;
    }

    private List<ProjectPlanningDO> getSpecialtyPersonRelevantPlanningList(ProjectPlanningDO anchorPlanning) {
        if (anchorPlanning == null) {
            return Collections.emptyList();
        }
        return Collections.singletonList(anchorPlanning);
    }

    @Override
    public ProjectOverviewPreviewRespVO getProjectOverviewPreview(ProjectOverviewExportReqVO reqVO) {
        return buildProjectOverviewPreviewResp(buildProjectOverviewExportData(reqVO));
    }

    @Override
    public void exportProjectOverviewExcel(HttpServletResponse response, ProjectOverviewExportReqVO reqVO)
            throws IOException {
        ProjectOverviewOutputExcelBuilder.ExportData data = buildProjectOverviewExportData(reqVO);
        projectOverviewOutputExcelBuilder.writeWorkbook(response,
                buildFileName(data.getOfficeName(), data.getYear(), "项目总览表"),
                projectOverviewOutputExcelBuilder.build(data));
    }

    private ProjectOverviewOutputExcelBuilder.ExportData buildProjectOverviewExportData(ProjectOverviewExportReqVO reqVO) {
        Integer reportYear = resolveExportYear(reqVO.getYear());
        OfficeDO office = validateOfficeExists(reqVO.getOfficeId());
        List<EmployeeDO> employeeList = employeeMapper.selectList(new LambdaQueryWrapperX<EmployeeDO>()
                .eq(EmployeeDO::getOfficeId, reqVO.getOfficeId())
                .eq(EmployeeDO::getEnabledFlag, Boolean.TRUE)
                .orderByAsc(EmployeeDO::getSortNo)
                .orderByAsc(EmployeeDO::getId));
        if (employeeList.isEmpty()) {
            throw invalidParamException("当前专业所下暂无员工，无法导出");
        }

        Set<Long> employeeIds = employeeList.stream()
                .map(EmployeeDO::getId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        List<ProjectPlanningDO> planningList = getProjectOverviewPlanningList(reportYear, employeeIds);
        Map<Long, ProjectDO> projectMap = getProjectMap(planningList);
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(planningList);
        Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap = getRoleSplitMap(planningList);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getExistingOutputSplitMap(planningList);

        Map<Long, ProjectOverviewAccumulator> accumulatorMap = new LinkedHashMap<>();
        for (ProjectPlanningDO planning : planningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            if (quarterSummary.getYearTotalAmount().compareTo(ZERO_AMOUNT) <= 0) {
                continue;
            }
            ProjectOutputSplitDO outputSplit = outputSplitMap.get(planning.getId());
            if (outputSplit == null) {
                continue;
            }
            PlanningOfficeContribution contribution = buildPlanningOfficeContribution(
                    planning, outputSplit, quarterSummary, roleSplitMap.get(planning.getId()), employeeIds);
            if (contribution.getCurrentSettlementAmount().compareTo(ZERO_AMOUNT) <= 0) {
                continue;
            }
            ProjectDO project = projectMap.get(planning.getProjectId());
            ProjectOverviewAccumulator accumulator = accumulatorMap.computeIfAbsent(planning.getProjectId(), projectId -> {
                ProjectOverviewAccumulator item = new ProjectOverviewAccumulator();
                item.setProjectId(projectId);
                item.setProjectName(project == null ? "" : defaultString(project.getProjectName()));
                return item;
            });
            appendProjectOverviewContribution(accumulator, planning, project, contribution);
        }

        List<ProjectOverviewOutputExcelBuilder.ProjectRow> rows = buildProjectOverviewRows(
                new ArrayList<>(accumulatorMap.values()), employeeList, reqVO.getSortType());
        ProjectOverviewOutputExcelBuilder.ExportData data = new ProjectOverviewOutputExcelBuilder.ExportData();
        data.setYear(reportYear);
        data.setOfficeName(office.getOfficeName());
        data.setEmployeeColumns(buildProjectOverviewEmployeeColumns(employeeList));
        data.setRows(rows);
        data.setTotalRow(buildProjectOverviewTotalRow(new ArrayList<>(accumulatorMap.values()), employeeList));

        return data;
    }

    @Override
    public EmployeeOutputSummaryPreviewRespVO getEmployeeOutputSummaryPreview(EmployeeOutputSummaryExportReqVO reqVO) {
        return buildEmployeeOutputSummaryPreviewResp(buildEmployeeOutputSummaryExportData(reqVO));
    }

    @Override
    public void exportEmployeeOutputSummaryExcel(HttpServletResponse response,
                                                 EmployeeOutputSummaryExportReqVO reqVO) throws IOException {
        EmployeeOutputSummaryExcelBuilder.ExportData data = buildEmployeeOutputSummaryExportData(reqVO);
        employeeOutputSummaryExcelBuilder.writeWorkbook(response,
                buildFileName("员工个人考核产值", reqVO.getYear(), "汇总表"),
                employeeOutputSummaryExcelBuilder.build(data));
    }

    private EmployeeOutputSummaryExcelBuilder.ExportData buildEmployeeOutputSummaryExportData(
            EmployeeOutputSummaryExportReqVO reqVO) {
        YearKValueDO yearKValue = yearKValueMapper.selectEnabledByKYear(reqVO.getYear());
        BigDecimal kValue = yearKValue == null ? DEFAULT_K_VALUE : ratio(yearKValue.getKValue());
        List<EmployeeDO> employeeList = employeeMapper.selectList(new LambdaQueryWrapperX<EmployeeDO>()
                .eqIfPresent(EmployeeDO::getId, reqVO.getEmployeeId())
                .eqIfPresent(EmployeeDO::getOfficeId, reqVO.getOfficeId())
                .eqIfPresent(EmployeeDO::getEmployeeStatus, reqVO.getEmployeeStatus())
                .eq(EmployeeDO::getEnabledFlag, Boolean.TRUE)
                .orderByAsc(EmployeeDO::getSortNo)
                .orderByAsc(EmployeeDO::getId));
        if (employeeList.isEmpty()) {
            EmployeeOutputSummaryExcelBuilder.ExportData data = new EmployeeOutputSummaryExcelBuilder.ExportData();
            data.setYear(reqVO.getYear());
            data.setKValue(kValue);
            data.setRows(Collections.emptyList());
            return data;
        }

        Set<Long> employeeIds = employeeList.stream().map(EmployeeDO::getId).collect(Collectors.toCollection(LinkedHashSet::new));

        List<ProjectPlanningDO> relevantPlanningList = getEmployeeOutputSummaryPlanningList(reqVO.getYear(), employeeIds);
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = getQuarterMap(relevantPlanningList);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = getOutputSplitMap(relevantPlanningList);
        Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap = getRoleSplitMap(relevantPlanningList);
        Map<Long, EmployeeAccumulator> accumulatorMap = new LinkedHashMap<>();
        employeeIds.forEach(id -> accumulatorMap.put(id, new EmployeeAccumulator()));

        for (ProjectPlanningDO planning : relevantPlanningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reqVO.getYear());
            ProjectOutputSplitDO outputSplit = outputSplitMap.get(planning.getId());
            if (outputSplit == null) {
                continue;
            }
            List<SpecialtyRoleSplitRespVO> roleList = roleSplitMap.getOrDefault(planning.getId(), Collections.emptyList());
            for (SpecialtyRoleSplitRespVO roleRow : roleList) {
                BigDecimal categoryRatio = getCategoryRatio(outputSplit, roleRow.getSpecialtyCode());
                BigDecimal roleRatio = ratio(roleRow.getRoleRatio());
                List<SpecialtyRolePersonRespVO> persons = roleRow.getPersons() == null
                        ? Collections.emptyList() : roleRow.getPersons();
                for (SpecialtyRolePersonRespVO person : persons) {
                    if (person.getEmployeeId() == null || !employeeIds.contains(person.getEmployeeId())) {
                        continue;
                    }
                    BigDecimal totalRatio = multiplyRatio(categoryRatio, roleRatio, person.getPersonRatio());
                    EmployeeAccumulator accumulator = accumulatorMap.get(person.getEmployeeId());
                    accumulator.addQuarterAmount(1, multiplyAmount(quarterSummary.getQuarterOneAmount(), totalRatio));
                    accumulator.addQuarterAmount(2, multiplyAmount(quarterSummary.getQuarterTwoAmount(), totalRatio));
                    accumulator.addQuarterAmount(3, multiplyAmount(quarterSummary.getQuarterThreeAmount(), totalRatio));
                    accumulator.addQuarterAmount(4, multiplyAmount(quarterSummary.getQuarterFourAmount(), totalRatio));
                }
            }
        }

        Map<Long, EmployeeYearCostBudgetDO> costBudgetMap = employeeYearCostBudgetMapper.selectList(
                        new LambdaQueryWrapperX<EmployeeYearCostBudgetDO>()
                                .eq(EmployeeYearCostBudgetDO::getBudgetYear, reqVO.getYear())
                                .eq(EmployeeYearCostBudgetDO::getEnabledFlag, Boolean.TRUE)
                                .inIfPresent(EmployeeYearCostBudgetDO::getEmployeeId, employeeIds))
                .stream()
                .collect(Collectors.toMap(EmployeeYearCostBudgetDO::getEmployeeId, item -> item, (a, b) -> a));
        Map<Long, EmployeeYearLeaderOutputDO> leaderOutputMap = employeeYearLeaderOutputMapper
                .selectEnabledListByOutputYear(reqVO.getYear(), employeeIds)
                .stream()
                .collect(Collectors.toMap(EmployeeYearLeaderOutputDO::getEmployeeId, item -> item, (a, b) -> a));

        List<EmployeeOutputSummaryExcelRespVO> rows = new ArrayList<>();
        for (EmployeeDO employee : employeeList) {
            EmployeeAccumulator accumulator = accumulatorMap.get(employee.getId());
            if (accumulator == null) {
                accumulator = new EmployeeAccumulator();
            }
            EmployeeYearCostBudgetDO costBudget = costBudgetMap.get(employee.getId());
            EmployeeYearLeaderOutputDO leaderOutput = leaderOutputMap.get(employee.getId());
            BigDecimal expectedCostAmount = costBudget == null ? ZERO_AMOUNT : amount(costBudget.getExpectedCostAmount());
            BigDecimal officeLeaderOrBimAmount = leaderOutput == null ? ZERO_AMOUNT : amount(leaderOutput.getLeaderOutputAmount());
            BigDecimal annualTotalAmount = accumulator.getAnnualTotalAmount();
            BigDecimal totalAssessmentOutputAmount = annualTotalAmount.add(officeLeaderOrBimAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal basicAssessmentOutputAmount = kValue.compareTo(BigDecimal.ZERO) == 0
                    ? ZERO_AMOUNT
                    : expectedCostAmount.divide(kValue, 2, RoundingMode.HALF_UP);
            BigDecimal remainingOutputAmount = totalAssessmentOutputAmount.subtract(basicAssessmentOutputAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal estimatedYearEndPerformanceAmount = remainingOutputAmount.multiply(kValue)
                    .setScale(2, RoundingMode.HALF_UP);

            EmployeeOutputSummaryExcelRespVO row = new EmployeeOutputSummaryExcelRespVO();
            row.setEmployeeName(employee.getEmployeeName());
            row.setQuarterOneAmount(amountToWan(accumulator.getQuarterOneAmount()));
            row.setQuarterTwoAmount(amountToWan(accumulator.getQuarterTwoAmount()));
            row.setQuarterThreeAmount(amountToWan(accumulator.getQuarterThreeAmount()));
            row.setQuarterFourAmount(amountToWan(accumulator.getQuarterFourAmount()));
            row.setAnnualTotalAmount(amountToWan(annualTotalAmount));
            row.setOfficeLeaderOrBimAmount(amountToWan(officeLeaderOrBimAmount));
            row.setTotalAssessmentOutputAmount(amountToWan(totalAssessmentOutputAmount));
            row.setExpectedCostAmount(amountToWan(expectedCostAmount));
            row.setBasicAssessmentOutputAmount(amountToWan(basicAssessmentOutputAmount));
            row.setRemainingOutputAmount(amountToWan(remainingOutputAmount));
            row.setEstimatedYearEndPerformanceAmount(amountToWan(estimatedYearEndPerformanceAmount));
            rows.add(row);
        }

        sortEmployeeRows(rows, reqVO.getSortType());
        for (int i = 0; i < rows.size(); i++) {
            rows.get(i).setSerialNo(i + 1);
        }

        EmployeeOutputSummaryExcelBuilder.ExportData data = new EmployeeOutputSummaryExcelBuilder.ExportData();
        data.setYear(reqVO.getYear());
        data.setKValue(kValue);
        data.setRows(rows);
        return data;
    }

    private ProjectOverviewPreviewRespVO buildProjectOverviewPreviewResp(
            ProjectOverviewOutputExcelBuilder.ExportData data) {
        ProjectOverviewPreviewRespVO respVO = new ProjectOverviewPreviewRespVO();
        respVO.setYear(data.getYear());
        respVO.setOfficeName(data.getOfficeName());
        respVO.setEmployeeColumns(data.getEmployeeColumns() == null
                ? Collections.emptyList()
                : data.getEmployeeColumns().stream()
                .map(this::toProjectOverviewPreviewEmployeeColumn)
                .collect(Collectors.toList()));
        respVO.setRows(data.getRows() == null
                ? Collections.emptyList()
                : data.getRows().stream()
                .map(row -> toProjectOverviewPreviewRow(row, false))
                .collect(Collectors.toList()));
        respVO.setTotalRow(toProjectOverviewPreviewRow(data.getTotalRow(), true));
        return respVO;
    }

    private ProjectOverviewPreviewRespVO.EmployeeColumn toProjectOverviewPreviewEmployeeColumn(
            ProjectOverviewOutputExcelBuilder.EmployeeColumn column) {
        ProjectOverviewPreviewRespVO.EmployeeColumn previewColumn =
                new ProjectOverviewPreviewRespVO.EmployeeColumn();
        if (column == null) {
            return previewColumn;
        }
        previewColumn.setEmployeeId(column.getEmployeeId());
        previewColumn.setEmployeeName(column.getEmployeeName());
        return previewColumn;
    }

    private ProjectOverviewPreviewRespVO.ProjectRow toProjectOverviewPreviewRow(
            ProjectOverviewOutputExcelBuilder.ProjectRow row, boolean totalRow) {
        ProjectOverviewPreviewRespVO.ProjectRow previewRow = new ProjectOverviewPreviewRespVO.ProjectRow();
        previewRow.setTotalRow(totalRow);
        if (row == null) {
            return previewRow;
        }
        previewRow.setSerialNo(row.getSerialNo());
        previewRow.setProjectName(totalRow ? "合计" : row.getProjectName());
        previewRow.setProgressText(row.getProgressText());
        previewRow.setWorkStage(row.getWorkStage());
        previewRow.setTotalOutputAmountWan(amountToWan(row.getTotalOutputAmount()));
        previewRow.setHistoricalIssuedRatio(row.getHistoricalIssuedRatio());
        previewRow.setCurrentSettlementRatio(row.getCurrentSettlementRatio());
        previewRow.setCurrentSettlementAmountWan(amountToWan(row.getCurrentSettlementAmount()));
        previewRow.setPendingRatio(row.getPendingRatio());
        Map<Long, ProjectOverviewPreviewRespVO.EmployeeAmountValue> employeeAmountMap = new LinkedHashMap<>();
        if (row.getEmployeeAmountMap() != null) {
            row.getEmployeeAmountMap().forEach((employeeId, value) ->
                    employeeAmountMap.put(employeeId, toProjectOverviewPreviewEmployeeAmount(value)));
        }
        previewRow.setEmployeeAmountMap(employeeAmountMap);
        return previewRow;
    }

    private ProjectOverviewPreviewRespVO.EmployeeAmountValue toProjectOverviewPreviewEmployeeAmount(
            ProjectOverviewOutputExcelBuilder.EmployeeAmountValue value) {
        ProjectOverviewPreviewRespVO.EmployeeAmountValue previewValue =
                new ProjectOverviewPreviewRespVO.EmployeeAmountValue();
        if (value == null) {
            return previewValue;
        }
        previewValue.setQuarterOneAmountWan(amountToWan(value.getQuarterOneAmount()));
        previewValue.setQuarterTwoAmountWan(amountToWan(value.getQuarterTwoAmount()));
        previewValue.setQuarterThreeAmountWan(amountToWan(value.getQuarterThreeAmount()));
        previewValue.setQuarterFourAmountWan(amountToWan(value.getQuarterFourAmount()));
        previewValue.setAnnualTotalAmountWan(amountToWan(value.getAnnualTotalAmount()));
        return previewValue;
    }

    private EmployeeOutputSummaryPreviewRespVO buildEmployeeOutputSummaryPreviewResp(
            EmployeeOutputSummaryExcelBuilder.ExportData data) {
        EmployeeOutputSummaryPreviewRespVO respVO = new EmployeeOutputSummaryPreviewRespVO();
        respVO.setYear(data.getYear());
        respVO.setKValue(data.getKValue());
        List<EmployeeOutputSummaryExcelRespVO> rows = data.getRows() == null
                ? Collections.emptyList() : data.getRows();
        respVO.setRows(rows);
        respVO.setTotalRow(buildEmployeeOutputSummaryTotalRow(rows));
        return respVO;
    }

    private EmployeeOutputSummaryExcelRespVO buildEmployeeOutputSummaryTotalRow(
            List<EmployeeOutputSummaryExcelRespVO> rows) {
        EmployeeOutputSummaryExcelRespVO totalRow = new EmployeeOutputSummaryExcelRespVO();
        totalRow.setEmployeeName("合计");
        totalRow.setQuarterOneAmount(ZERO_AMOUNT);
        totalRow.setQuarterTwoAmount(ZERO_AMOUNT);
        totalRow.setQuarterThreeAmount(ZERO_AMOUNT);
        totalRow.setQuarterFourAmount(ZERO_AMOUNT);
        totalRow.setAnnualTotalAmount(ZERO_AMOUNT);
        totalRow.setOfficeLeaderOrBimAmount(ZERO_AMOUNT);
        totalRow.setTotalAssessmentOutputAmount(ZERO_AMOUNT);
        totalRow.setExpectedCostAmount(ZERO_AMOUNT);
        totalRow.setBasicAssessmentOutputAmount(ZERO_AMOUNT);
        totalRow.setRemainingOutputAmount(ZERO_AMOUNT);
        totalRow.setEstimatedYearEndPerformanceAmount(ZERO_AMOUNT);
        if (rows == null) {
            return totalRow;
        }
        for (EmployeeOutputSummaryExcelRespVO row : rows) {
            totalRow.setQuarterOneAmount(addAmountValue(totalRow.getQuarterOneAmount(), row.getQuarterOneAmount()));
            totalRow.setQuarterTwoAmount(addAmountValue(totalRow.getQuarterTwoAmount(), row.getQuarterTwoAmount()));
            totalRow.setQuarterThreeAmount(addAmountValue(totalRow.getQuarterThreeAmount(), row.getQuarterThreeAmount()));
            totalRow.setQuarterFourAmount(addAmountValue(totalRow.getQuarterFourAmount(), row.getQuarterFourAmount()));
            totalRow.setAnnualTotalAmount(addAmountValue(totalRow.getAnnualTotalAmount(), row.getAnnualTotalAmount()));
            totalRow.setOfficeLeaderOrBimAmount(addAmountValue(
                    totalRow.getOfficeLeaderOrBimAmount(), row.getOfficeLeaderOrBimAmount()));
            totalRow.setTotalAssessmentOutputAmount(addAmountValue(
                    totalRow.getTotalAssessmentOutputAmount(), row.getTotalAssessmentOutputAmount()));
            totalRow.setExpectedCostAmount(addAmountValue(totalRow.getExpectedCostAmount(), row.getExpectedCostAmount()));
            totalRow.setBasicAssessmentOutputAmount(addAmountValue(
                    totalRow.getBasicAssessmentOutputAmount(), row.getBasicAssessmentOutputAmount()));
            totalRow.setRemainingOutputAmount(addAmountValue(
                    totalRow.getRemainingOutputAmount(), row.getRemainingOutputAmount()));
            totalRow.setEstimatedYearEndPerformanceAmount(addAmountValue(
                    totalRow.getEstimatedYearEndPerformanceAmount(), row.getEstimatedYearEndPerformanceAmount()));
        }
        return totalRow;
    }

    private ProjectPlanningDO validatePlanningExists(Long planningId) {
        ProjectPlanningDO planning = projectPlanningMapper.selectById(planningId);
        if (planning == null) {
            throw exception(PROJECT_PLANNING_NOT_EXISTS);
        }
        return planning;
    }

    private ProjectDO validateProjectExists(Long projectId) {
        ProjectDO project = projectMapper.selectById(projectId);
        if (project == null) {
            throw exception(PROJECT_NOT_EXISTS);
        }
        return project;
    }

    private OfficeDO validateOfficeExists(Long officeId) {
        OfficeDO office = officeMapper.selectById(officeId);
        if (office == null) {
            throw exception(OFFICE_NOT_EXISTS);
        }
        return office;
    }

    private void validateSpecialtyCode(String specialtyCode) {
        if (!OutputSplitBizConstants.isValidSpecialtyCode(specialtyCode)
                || OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            throw exception(SPECIALTY_ROLE_SPLIT_SPECIALTY_INVALID);
        }
    }

    private List<ProjectPlanningDO> getProjectPlanningScope(ProjectPlanningDO anchorPlanning, boolean majorOnly) {
        List<ProjectPlanningDO> projectPlanningList = projectPlanningMapper.selectListByProjectId(anchorPlanning.getProjectId());
        Integer targetYear = resolveReportYear(anchorPlanning);
        return projectPlanningList.stream()
                .filter(item -> Objects.equals(resolveReportYear(item), targetYear))
                .filter(item -> !majorOnly || ProjectPlanningBizTypeConstants.isMajor(item.getOwnershipType()))
                .collect(Collectors.toList());
    }

    private List<ProjectPlanningDO> getAssignablePlanningList() {
        return projectPlanningMapper.selectList(new LambdaQueryWrapperX<ProjectPlanningDO>()
                .in(ProjectPlanningDO::getOwnershipType, Arrays.asList(
                        ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_MAJOR,
                        ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_COMPREHENSIVE,
                        ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SPECIAL_SUBCONTRACT,
                        ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SOURCE_COOP_SUBCONTRACT
                )));
    }

    private List<ProjectPlanningDO> getProjectOverviewPlanningList(Integer reportYear, Set<Long> employeeIds) {
        if (reportYear == null || employeeIds == null || employeeIds.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> candidatePlanningIds = specialtyRoleSplitPersonMapper.selectListByEmployeeIds(employeeIds).stream()
                .map(SpecialtyRoleSplitPersonDO::getPlanningId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Set<Long> projectLeadProjectIds = projectRolePersonMapper.selectListByEmployeeIds(employeeIds).stream()
                .filter(item -> Objects.equals(item.getRoleCode(), OutputSplitBizConstants.ROLE_PROJECT_MANAGER)
                        || Objects.equals(item.getRoleCode(), OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL))
                .map(ProjectRolePersonDO::getProjectId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (!projectLeadProjectIds.isEmpty()) {
            projectPlanningMapper.selectList(new LambdaQueryWrapperX<ProjectPlanningDO>()
                            .in(ProjectPlanningDO::getProjectId, projectLeadProjectIds)
                            .in(ProjectPlanningDO::getOwnershipType, Arrays.asList(
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_MAJOR,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_COMPREHENSIVE,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SPECIAL_SUBCONTRACT,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SOURCE_COOP_SUBCONTRACT
                            )))
                    .stream()
                    .map(ProjectPlanningDO::getId)
                    .filter(Objects::nonNull)
                    .forEach(candidatePlanningIds::add);
        }

        Set<Long> matchedPlanningIds = getYearAmountPlanningIds(reportYear, candidatePlanningIds);
        if (matchedPlanningIds.isEmpty()) {
            return Collections.emptyList();
        }
        return sortPlanningList(projectPlanningMapper.selectBatchIds(matchedPlanningIds).stream()
                .filter(item -> item != null && ProjectPlanningBizTypeConstants.isValidOwnershipType(item.getOwnershipType()))
                .collect(Collectors.toList()));
    }

    private List<ProjectPlanningDO> getEmployeeOutputSummaryPlanningList(Integer reportYear, Set<Long> employeeIds) {
        if (reportYear == null || employeeIds == null || employeeIds.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> yearPlanningIds = getYearAmountPlanningIds(reportYear);
        if (yearPlanningIds.isEmpty()) {
            return Collections.emptyList();
        }

        Set<Long> matchedPlanningIds = specialtyRoleSplitPersonMapper
                .selectListByPlanningIdsAndEmployeeIds(yearPlanningIds, employeeIds)
                .stream()
                .map(SpecialtyRoleSplitPersonDO::getPlanningId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Set<Long> projectIds = projectRolePersonMapper.selectListByEmployeeIds(employeeIds)
                .stream()
                .map(ProjectRolePersonDO::getProjectId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (!projectIds.isEmpty()) {
            projectPlanningMapper.selectList(new LambdaQueryWrapperX<ProjectPlanningDO>()
                            .in(ProjectPlanningDO::getId, yearPlanningIds)
                            .in(ProjectPlanningDO::getProjectId, projectIds)
                            .in(ProjectPlanningDO::getOwnershipType, Arrays.asList(
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_MAJOR,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_COMPREHENSIVE,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SPECIAL_SUBCONTRACT,
                                    ProjectPlanningBizTypeConstants.OWNERSHIP_TYPE_SOURCE_COOP_SUBCONTRACT
                            )))
                    .stream()
                    .map(ProjectPlanningDO::getId)
                    .filter(Objects::nonNull)
                    .forEach(matchedPlanningIds::add);
        }
        if (matchedPlanningIds.isEmpty()) {
            return Collections.emptyList();
        }
        return sortPlanningList(projectPlanningMapper.selectBatchIds(matchedPlanningIds).stream()
                .filter(item -> item != null && ProjectPlanningBizTypeConstants.isValidOwnershipType(item.getOwnershipType()))
                .collect(Collectors.toList()));
    }

    private Set<Long> getYearAmountPlanningIds(Integer reportYear) {
        if (reportYear == null) {
            return Collections.emptySet();
        }
        return projectPlanningQuarterMapper.selectList(new LambdaQueryWrapperX<ProjectPlanningQuarterDO>()
                        .eq(ProjectPlanningQuarterDO::getDistributionYear, reportYear)
                        .gt(ProjectPlanningQuarterDO::getDistributionAmount, BigDecimal.ZERO))
                .stream()
                .map(ProjectPlanningQuarterDO::getPlanningId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    private Set<Long> getYearAmountPlanningIds(Integer reportYear, Collection<Long> planningIds) {
        if (reportYear == null || planningIds == null || planningIds.isEmpty()) {
            return Collections.emptySet();
        }
        Set<Long> result = new LinkedHashSet<>();
        for (List<Long> partition : partitionIds(planningIds, 1000)) {
            projectPlanningQuarterMapper.selectList(new LambdaQueryWrapperX<ProjectPlanningQuarterDO>()
                            .in(ProjectPlanningQuarterDO::getPlanningId, partition)
                            .eq(ProjectPlanningQuarterDO::getDistributionYear, reportYear)
                            .gt(ProjectPlanningQuarterDO::getDistributionAmount, BigDecimal.ZERO))
                    .stream()
                    .map(ProjectPlanningQuarterDO::getPlanningId)
                    .filter(Objects::nonNull)
                    .forEach(result::add);
        }
        return result;
    }

    private List<List<Long>> partitionIds(Collection<Long> ids, int partitionSize) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> normalizedIds = ids.stream()
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (normalizedIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<List<Long>> result = new ArrayList<>();
        for (int start = 0; start < normalizedIds.size(); start += partitionSize) {
            result.add(normalizedIds.subList(start, Math.min(start + partitionSize, normalizedIds.size())));
        }
        return result;
    }

    private List<ProjectQuarterOutputExcelBuilder.QuarterRow> buildProjectQuarterOutputRows(
            List<ProjectPlanningDO> planningList, Map<Long, List<ProjectPlanningQuarterDO>> quarterMap,
            Map<Long, ProjectOutputSplitDO> outputSplitMap, Integer reportYear) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyList();
        }

        List<ProjectQuarterOutputExcelBuilder.QuarterRow> rows = new ArrayList<>();
        BigDecimal totalAssessmentOutputValue = ZERO_AMOUNT;
        BigDecimal totalQuarterOneAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterTwoAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterThreeAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterFourAmount = ZERO_AMOUNT;
        BigDecimal totalYearAmount = ZERO_AMOUNT;
        BigDecimal totalProjectLeadAssessment = ZERO_AMOUNT;
        BigDecimal totalProjectLeadQuarterOneAmount = ZERO_AMOUNT;
        BigDecimal totalProjectLeadQuarterTwoAmount = ZERO_AMOUNT;
        BigDecimal totalProjectLeadQuarterThreeAmount = ZERO_AMOUNT;
        BigDecimal totalProjectLeadQuarterFourAmount = ZERO_AMOUNT;
        BigDecimal totalProjectLeadYearAmount = ZERO_AMOUNT;
        BigDecimal totalOfficeAssessment = ZERO_AMOUNT;
        BigDecimal totalOfficeQuarterOneAmount = ZERO_AMOUNT;
        BigDecimal totalOfficeQuarterTwoAmount = ZERO_AMOUNT;
        BigDecimal totalOfficeQuarterThreeAmount = ZERO_AMOUNT;
        BigDecimal totalOfficeQuarterFourAmount = ZERO_AMOUNT;
        BigDecimal totalOfficeYearAmount = ZERO_AMOUNT;
        BigDecimal totalArchAssessment = ZERO_AMOUNT;
        BigDecimal totalDecorAssessment = ZERO_AMOUNT;
        BigDecimal totalStructAssessment = ZERO_AMOUNT;
        BigDecimal totalWaterAssessment = ZERO_AMOUNT;
        BigDecimal totalHvacAssessment = ZERO_AMOUNT;
        BigDecimal totalElecAssessment = ZERO_AMOUNT;
        BigDecimal totalDigitalAssessment = ZERO_AMOUNT;
        int serialNo = 1;

        for (ProjectPlanningDO planning : planningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            ProjectOutputSplitDO outputSplit = resolveQuarterOutputSplit(planning, outputSplitMap);
            BigDecimal assessmentOutputValue = amount(planning.getAssessmentOutputValue());
            BigDecimal quarterOneAmount = amount(quarterSummary.getQuarterOneAmount());
            BigDecimal quarterTwoAmount = amount(quarterSummary.getQuarterTwoAmount());
            BigDecimal quarterThreeAmount = amount(quarterSummary.getQuarterThreeAmount());
            BigDecimal quarterFourAmount = amount(quarterSummary.getQuarterFourAmount());
            BigDecimal yearTotalAmount = amount(quarterSummary.getYearTotalAmount());

            BigDecimal projectLeadRatio = ratio(outputSplit.getProjectLeadRatio());
            BigDecimal officeRatio = ratio(outputSplit.getOfficeRatio());
            BigDecimal projectLeadAssessment = multiplyAmount(assessmentOutputValue, projectLeadRatio);
            BigDecimal projectLeadQuarterOneAmount = multiplyAmount(quarterOneAmount, projectLeadRatio);
            BigDecimal projectLeadQuarterTwoAmount = multiplyAmount(quarterTwoAmount, projectLeadRatio);
            BigDecimal projectLeadQuarterThreeAmount = multiplyAmount(quarterThreeAmount, projectLeadRatio);
            BigDecimal projectLeadQuarterFourAmount = multiplyAmount(quarterFourAmount, projectLeadRatio);
            BigDecimal projectLeadYearAmount = multiplyAmount(yearTotalAmount, projectLeadRatio);

            BigDecimal officeAssessment = multiplyAmount(assessmentOutputValue, officeRatio);
            BigDecimal officeQuarterOneAmount = multiplyAmount(quarterOneAmount, officeRatio);
            BigDecimal officeQuarterTwoAmount = multiplyAmount(quarterTwoAmount, officeRatio);
            BigDecimal officeQuarterThreeAmount = multiplyAmount(quarterThreeAmount, officeRatio);
            BigDecimal officeQuarterFourAmount = multiplyAmount(quarterFourAmount, officeRatio);
            BigDecimal officeYearAmount = multiplyAmount(yearTotalAmount, officeRatio);

            BigDecimal archRatio = ratio(outputSplit.getArchRatio());
            BigDecimal decorRatio = ratio(outputSplit.getDecorRatio());
            BigDecimal structRatio = ratio(outputSplit.getStructRatio());
            BigDecimal waterRatio = ratio(outputSplit.getWaterRatio());
            BigDecimal hvacRatio = ratio(outputSplit.getHvacRatio());
            BigDecimal elecRatio = ratio(outputSplit.getElecRatio());
            BigDecimal digitalRatio = ratio(outputSplit.getDigitalRatio());
            BigDecimal archAssessment = multiplyAmount(officeAssessment, archRatio);
            BigDecimal decorAssessment = multiplyAmount(officeAssessment, decorRatio);
            BigDecimal structAssessment = multiplyAmount(officeAssessment, structRatio);
            BigDecimal waterAssessment = multiplyAmount(officeAssessment, waterRatio);
            BigDecimal hvacAssessment = multiplyAmount(officeAssessment, hvacRatio);
            BigDecimal elecAssessment = multiplyAmount(officeAssessment, elecRatio);
            BigDecimal digitalAssessment = multiplyAmount(officeAssessment, digitalRatio);

            ProjectQuarterOutputExcelBuilder.QuarterRow row = new ProjectQuarterOutputExcelBuilder.QuarterRow();
            row.setSerialNo(serialNo++);
            row.setOutputType(resolveBudgetCategoryLabel(planning.getOwnershipType()));
            row.setDesignContent(defaultString(planning.getPlanningContent()));
            row.setQuarterOneAmountWan(amountToWan(quarterOneAmount));
            row.setQuarterTwoAmountWan(amountToWan(quarterTwoAmount));
            row.setQuarterThreeAmountWan(amountToWan(quarterThreeAmount));
            row.setQuarterFourAmountWan(amountToWan(quarterFourAmount));
            row.setYearTotalAmountWan(amountToWan(yearTotalAmount));
            row.setProjectLeadRatio(projectLeadRatio);
            row.setProjectLeadAssessmentOutputWan(amountToWan(projectLeadAssessment));
            row.setProjectLeadQuarterOneAmountWan(amountToWan(projectLeadQuarterOneAmount));
            row.setProjectLeadQuarterTwoAmountWan(amountToWan(projectLeadQuarterTwoAmount));
            row.setProjectLeadQuarterThreeAmountWan(amountToWan(projectLeadQuarterThreeAmount));
            row.setProjectLeadQuarterFourAmountWan(amountToWan(projectLeadQuarterFourAmount));
            row.setProjectLeadYearTotalAmountWan(amountToWan(projectLeadYearAmount));
            row.setOfficeRatio(officeRatio);
            row.setOfficeAssessmentOutputWan(amountToWan(officeAssessment));
            row.setOfficeQuarterOneAmountWan(amountToWan(officeQuarterOneAmount));
            row.setOfficeQuarterTwoAmountWan(amountToWan(officeQuarterTwoAmount));
            row.setOfficeQuarterThreeAmountWan(amountToWan(officeQuarterThreeAmount));
            row.setOfficeQuarterFourAmountWan(amountToWan(officeQuarterFourAmount));
            row.setOfficeYearTotalAmountWan(amountToWan(officeYearAmount));
            row.setArchRatio(archRatio);
            row.setDecorRatio(decorRatio);
            row.setStructRatio(structRatio);
            row.setWaterRatio(waterRatio);
            row.setHvacRatio(hvacRatio);
            row.setElecRatio(elecRatio);
            row.setDigitalRatio(digitalRatio);
            row.setArchAssessmentOutputWan(amountToWan(archAssessment));
            row.setDecorAssessmentOutputWan(amountToWan(decorAssessment));
            row.setStructAssessmentOutputWan(amountToWan(structAssessment));
            row.setWaterAssessmentOutputWan(amountToWan(waterAssessment));
            row.setHvacAssessmentOutputWan(amountToWan(hvacAssessment));
            row.setElecAssessmentOutputWan(amountToWan(elecAssessment));
            row.setDigitalAssessmentOutputWan(amountToWan(digitalAssessment));
            rows.add(row);

            totalAssessmentOutputValue = totalAssessmentOutputValue.add(assessmentOutputValue).setScale(2, RoundingMode.HALF_UP);
            totalQuarterOneAmount = totalQuarterOneAmount.add(quarterOneAmount).setScale(2, RoundingMode.HALF_UP);
            totalQuarterTwoAmount = totalQuarterTwoAmount.add(quarterTwoAmount).setScale(2, RoundingMode.HALF_UP);
            totalQuarterThreeAmount = totalQuarterThreeAmount.add(quarterThreeAmount).setScale(2, RoundingMode.HALF_UP);
            totalQuarterFourAmount = totalQuarterFourAmount.add(quarterFourAmount).setScale(2, RoundingMode.HALF_UP);
            totalYearAmount = totalYearAmount.add(yearTotalAmount).setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadAssessment = totalProjectLeadAssessment.add(projectLeadAssessment).setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadQuarterOneAmount = totalProjectLeadQuarterOneAmount.add(projectLeadQuarterOneAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadQuarterTwoAmount = totalProjectLeadQuarterTwoAmount.add(projectLeadQuarterTwoAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadQuarterThreeAmount = totalProjectLeadQuarterThreeAmount.add(projectLeadQuarterThreeAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadQuarterFourAmount = totalProjectLeadQuarterFourAmount.add(projectLeadQuarterFourAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalProjectLeadYearAmount = totalProjectLeadYearAmount.add(projectLeadYearAmount).setScale(2, RoundingMode.HALF_UP);
            totalOfficeAssessment = totalOfficeAssessment.add(officeAssessment).setScale(2, RoundingMode.HALF_UP);
            totalOfficeQuarterOneAmount = totalOfficeQuarterOneAmount.add(officeQuarterOneAmount).setScale(2, RoundingMode.HALF_UP);
            totalOfficeQuarterTwoAmount = totalOfficeQuarterTwoAmount.add(officeQuarterTwoAmount).setScale(2, RoundingMode.HALF_UP);
            totalOfficeQuarterThreeAmount = totalOfficeQuarterThreeAmount.add(officeQuarterThreeAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalOfficeQuarterFourAmount = totalOfficeQuarterFourAmount.add(officeQuarterFourAmount)
                    .setScale(2, RoundingMode.HALF_UP);
            totalOfficeYearAmount = totalOfficeYearAmount.add(officeYearAmount).setScale(2, RoundingMode.HALF_UP);
            totalArchAssessment = totalArchAssessment.add(archAssessment).setScale(2, RoundingMode.HALF_UP);
            totalDecorAssessment = totalDecorAssessment.add(decorAssessment).setScale(2, RoundingMode.HALF_UP);
            totalStructAssessment = totalStructAssessment.add(structAssessment).setScale(2, RoundingMode.HALF_UP);
            totalWaterAssessment = totalWaterAssessment.add(waterAssessment).setScale(2, RoundingMode.HALF_UP);
            totalHvacAssessment = totalHvacAssessment.add(hvacAssessment).setScale(2, RoundingMode.HALF_UP);
            totalElecAssessment = totalElecAssessment.add(elecAssessment).setScale(2, RoundingMode.HALF_UP);
            totalDigitalAssessment = totalDigitalAssessment.add(digitalAssessment).setScale(2, RoundingMode.HALF_UP);
        }

        ProjectQuarterOutputExcelBuilder.QuarterRow totalRow = new ProjectQuarterOutputExcelBuilder.QuarterRow();
        totalRow.setTotalRow(true);
        totalRow.setQuarterOneAmountWan(amountToWan(totalQuarterOneAmount));
        totalRow.setQuarterTwoAmountWan(amountToWan(totalQuarterTwoAmount));
        totalRow.setQuarterThreeAmountWan(amountToWan(totalQuarterThreeAmount));
        totalRow.setQuarterFourAmountWan(amountToWan(totalQuarterFourAmount));
        totalRow.setYearTotalAmountWan(amountToWan(totalYearAmount));
        totalRow.setProjectLeadRatio(divideAmountRatio(totalProjectLeadAssessment, totalAssessmentOutputValue));
        totalRow.setProjectLeadAssessmentOutputWan(amountToWan(totalProjectLeadAssessment));
        totalRow.setProjectLeadQuarterOneAmountWan(amountToWan(totalProjectLeadQuarterOneAmount));
        totalRow.setProjectLeadQuarterTwoAmountWan(amountToWan(totalProjectLeadQuarterTwoAmount));
        totalRow.setProjectLeadQuarterThreeAmountWan(amountToWan(totalProjectLeadQuarterThreeAmount));
        totalRow.setProjectLeadQuarterFourAmountWan(amountToWan(totalProjectLeadQuarterFourAmount));
        totalRow.setProjectLeadYearTotalAmountWan(amountToWan(totalProjectLeadYearAmount));
        totalRow.setOfficeRatio(divideAmountRatio(totalOfficeAssessment, totalAssessmentOutputValue));
        totalRow.setOfficeAssessmentOutputWan(amountToWan(totalOfficeAssessment));
        totalRow.setOfficeQuarterOneAmountWan(amountToWan(totalOfficeQuarterOneAmount));
        totalRow.setOfficeQuarterTwoAmountWan(amountToWan(totalOfficeQuarterTwoAmount));
        totalRow.setOfficeQuarterThreeAmountWan(amountToWan(totalOfficeQuarterThreeAmount));
        totalRow.setOfficeQuarterFourAmountWan(amountToWan(totalOfficeQuarterFourAmount));
        totalRow.setOfficeYearTotalAmountWan(amountToWan(totalOfficeYearAmount));
        totalRow.setArchAssessmentOutputWan(amountToWan(totalArchAssessment));
        totalRow.setDecorAssessmentOutputWan(amountToWan(totalDecorAssessment));
        totalRow.setStructAssessmentOutputWan(amountToWan(totalStructAssessment));
        totalRow.setWaterAssessmentOutputWan(amountToWan(totalWaterAssessment));
        totalRow.setHvacAssessmentOutputWan(amountToWan(totalHvacAssessment));
        totalRow.setElecAssessmentOutputWan(amountToWan(totalElecAssessment));
        totalRow.setDigitalAssessmentOutputWan(amountToWan(totalDigitalAssessment));
        rows.add(totalRow);
        return rows;
    }

    private List<ProjectBudgetExcelBuilder.BudgetRow> buildProjectBudgetRows(
            List<ProjectPlanningDO> planningList,
            Map<Long, List<ProjectPlanningGuideDetailDO>> guideDetailMap) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyList();
        }
        List<ProjectBudgetExcelBuilder.BudgetRow> rows = new ArrayList<>();
        planningList.stream()
                .sorted(Comparator.comparing(ProjectPlanningDO::getId))
                .forEach(planning -> {
                    if (!ProjectPlanningBizTypeConstants.isMajorGuidanceScene(
                            planning.getOwnershipType(), planning.getCalculationMethod())) {
                        return;
                    }
                    List<ProjectPlanningGuideDetailDO> detailList = guideDetailMap == null
                            ? Collections.emptyList()
                            : guideDetailMap.getOrDefault(planning.getId(), Collections.emptyList());
                    if (detailList.isEmpty()) {
                        return;
                    }
                    appendBudgetPlanningRows(rows, planning, sortBudgetGuideDetailList(detailList));
                });
        return rows;
    }

    private void appendBudgetPlanningRows(List<ProjectBudgetExcelBuilder.BudgetRow> rows,
                                          ProjectPlanningDO planning,
                                          List<ProjectPlanningGuideDetailDO> planningGroup) {
        if (planningGroup == null || planningGroup.isEmpty()) {
            return;
        }
        String planningContent = resolveBudgetPlanningContent(planning);
        List<ProjectPlanningGuideDetailDO> groundPlanningList = filterBudgetPartPlanningList(
                planningGroup, ProjectPlanningBizTypeConstants.DESIGN_PART_REAL_ESTATE);
        List<ProjectPlanningGuideDetailDO> undergroundPlanningList = filterBudgetPartPlanningList(
                planningGroup, ProjectPlanningBizTypeConstants.DESIGN_PART_UNDERGROUND);
        List<ProjectPlanningGuideDetailDO> otherPlanningList = filterBudgetPartPlanningList(
                planningGroup, ProjectPlanningBizTypeConstants.DESIGN_PART_OTHER);

        appendBudgetPartRows(rows, planning, planningContent,
                ProjectPlanningBizTypeConstants.DESIGN_PART_REAL_ESTATE, groundPlanningList);
        appendBudgetPartRows(rows, planning, planningContent,
                ProjectPlanningBizTypeConstants.DESIGN_PART_UNDERGROUND, undergroundPlanningList);
        appendBudgetPartRows(rows, planning, planningContent,
                ProjectPlanningBizTypeConstants.DESIGN_PART_OTHER, otherPlanningList);
        rows.add(buildBudgetSummaryRow(ProjectBudgetExcelBuilder.BudgetRowType.PLANNING_TOTAL,
                planning, planningContent, "总计", planningGroup));
    }

    private void appendBudgetPartRows(List<ProjectBudgetExcelBuilder.BudgetRow> rows,
                                      ProjectPlanningDO planning,
                                      String planningContent,
                                      String designPart,
                                      List<ProjectPlanningGuideDetailDO> partPlanningList) {
        if (partPlanningList == null || partPlanningList.isEmpty()) {
            return;
        }
        for (ProjectPlanningGuideDetailDO detail : partPlanningList) {
            rows.add(buildBudgetDetailRow(planning, planningContent, designPart, detail));
        }
        if (partPlanningList.size() > 2) {
            rows.add(buildBudgetSummaryRow(ProjectBudgetExcelBuilder.BudgetRowType.PART_SUBTOTAL,
                    planning, planningContent, designPart + "总计", partPlanningList));
        }
    }

    private List<ProjectPlanningGuideDetailDO> filterBudgetPartPlanningList(
            List<ProjectPlanningGuideDetailDO> planningGroup, String designPart) {
        return planningGroup.stream()
                .filter(item -> Objects.equals(item.getDesignPart(), designPart))
                .sorted(Comparator
                        .comparingInt((ProjectPlanningGuideDetailDO item) ->
                                item.getSortNo() == null ? Integer.MAX_VALUE : item.getSortNo())
                        .thenComparing(ProjectPlanningGuideDetailDO::getId))
                .collect(Collectors.toList());
    }

    private String resolveBudgetPlanningContent(ProjectPlanningDO planning) {
        return defaultString(planning == null ? null : planning.getPlanningContent());
    }

    private ProjectBudgetExcelBuilder.BudgetRow buildBudgetDetailRow(ProjectPlanningDO planning,
                                                                     String planningContent,
                                                                     String designPart,
                                                                     ProjectPlanningGuideDetailDO detail) {
        ProjectBudgetExcelBuilder.BudgetRow row = initBudgetRow(
                ProjectBudgetExcelBuilder.BudgetRowType.DETAIL, planning, planningContent, designPart);
        row.setDisplayBuildingType(defaultString(detail.getBuildingType()));
        row.setInternalGuidanceUnitPrice(detail.getInternalGuidanceUnitPrice());
        row.setDesignArea(detail.getDesignArea());
        row.setBuildingOrUnitCount(detail.getBuildingOrUnitCount());
        row.setDrawingSetFactor(detail.getDrawingSetFactor());
        row.setScaleFactor(detail.getScaleFactor());
        row.setModificationFactor(detail.getModificationFactor());
        row.setComplexityFactor(detail.getComplexityFactor());
        row.setSubtotalArea(detail.getAssessmentArea());
        row.setCurrentDesignStageRatio(detail.getDesignRatio());
        row.setAssessmentOutputValueWan(detail.getAssessmentOutputValue() == null
                ? null : amountToWan(detail.getAssessmentOutputValue()));
        return row;
    }

    private ProjectBudgetExcelBuilder.BudgetRow buildBudgetEmptyRow(String designPart) {
        return initBudgetRow(ProjectBudgetExcelBuilder.BudgetRowType.PART_EMPTY, null, "", designPart);
    }

    private ProjectBudgetExcelBuilder.BudgetRow buildBudgetSummaryRow(ProjectBudgetExcelBuilder.BudgetRowType rowType,
                                                                      ProjectPlanningDO planning,
                                                                      String planningContent,
                                                                      String displayDesignPart,
                                                                      List<ProjectPlanningGuideDetailDO> detailList) {
        ProjectBudgetExcelBuilder.BudgetRow row = initBudgetRow(rowType, planning, planningContent, displayDesignPart);
        row.setDesignArea(sumGuideDetailDesignArea(detailList));
        row.setSubtotalArea(sumGuideDetailAssessmentArea(detailList));
        row.setAssessmentOutputValueWan(amountToWan(sumGuideDetailAssessmentOutputValue(detailList)));
        return row;
    }

    private ProjectBudgetExcelBuilder.BudgetRow initBudgetRow(ProjectBudgetExcelBuilder.BudgetRowType rowType,
                                                              ProjectPlanningDO planning,
                                                              String planningContent,
                                                              String displayDesignPart) {
        ProjectBudgetExcelBuilder.BudgetRow row = new ProjectBudgetExcelBuilder.BudgetRow();
        row.setRowType(rowType);
        row.setPlanningId(planning == null ? null : planning.getId());
        row.setPlanningContent(planningContent);
        row.setDisplayDesignPart(displayDesignPart);
        return row;
    }

    private BigDecimal sumPlanningArea(List<ProjectPlanningDO> planningList) {
        BigDecimal total = ZERO_AMOUNT;
        if (planningList == null || planningList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningDO planning : planningList) {
            total = total.add(amount(planning.getPlanningArea())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private BigDecimal sumAssessmentArea(List<ProjectPlanningDO> planningList) {
        BigDecimal total = ZERO_AMOUNT;
        if (planningList == null || planningList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningDO planning : planningList) {
            total = total.add(amount(planning.getAssessmentArea())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private BigDecimal sumAssessmentOutputValue(List<ProjectPlanningDO> planningList) {
        BigDecimal total = ZERO_AMOUNT;
        if (planningList == null || planningList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningDO planning : planningList) {
            total = total.add(amount(planning.getAssessmentOutputValue())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private List<ProjectPlanningGuideDetailDO> sortBudgetGuideDetailList(List<ProjectPlanningGuideDetailDO> detailList) {
        return detailList.stream()
                .sorted(Comparator
                        .comparingInt((ProjectPlanningGuideDetailDO item) -> designPartOrder(item.getDesignPart()))
                        .thenComparingInt(item -> item.getSortNo() == null ? Integer.MAX_VALUE : item.getSortNo())
                        .thenComparing(ProjectPlanningGuideDetailDO::getId))
                .collect(Collectors.toList());
    }

    private BigDecimal sumGuideDetailDesignArea(List<ProjectPlanningGuideDetailDO> detailList) {
        BigDecimal total = ZERO_AMOUNT;
        if (detailList == null || detailList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningGuideDetailDO detail : detailList) {
            total = total.add(amount(detail.getDesignArea())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private BigDecimal sumGuideDetailAssessmentArea(List<ProjectPlanningGuideDetailDO> detailList) {
        BigDecimal total = ZERO_AMOUNT;
        if (detailList == null || detailList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningGuideDetailDO detail : detailList) {
            total = total.add(amount(detail.getAssessmentArea())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private BigDecimal sumGuideDetailAssessmentOutputValue(List<ProjectPlanningGuideDetailDO> detailList) {
        BigDecimal total = ZERO_AMOUNT;
        if (detailList == null || detailList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningGuideDetailDO detail : detailList) {
            total = total.add(amount(detail.getAssessmentOutputValue())).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private List<ProjectBudgetExcelBuilder.QuarterBudgetRow> buildQuarterBudgetRows(
            List<ProjectPlanningDO> planningList, Map<Long, List<ProjectPlanningQuarterDO>> quarterMap,
            Integer reportYear) {
        List<ProjectBudgetExcelBuilder.QuarterBudgetRow> rows = new ArrayList<>();
        BigDecimal totalQuarterOneAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterTwoAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterThreeAmount = ZERO_AMOUNT;
        BigDecimal totalQuarterFourAmount = ZERO_AMOUNT;
        BigDecimal totalYearAmount = ZERO_AMOUNT;
        int serialNo = 1;

        for (ProjectPlanningDO planning : planningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            ProjectBudgetExcelBuilder.QuarterBudgetRow row = new ProjectBudgetExcelBuilder.QuarterBudgetRow();
            row.setSerialNo(serialNo++);
            row.setOutputType(resolveBudgetCategoryLabel(planning.getOwnershipType()));
            row.setDesignContent(defaultString(planning.getPlanningContent()));
            row.setBudgetYear(reportYear);
            row.setTotalDesignArea(amount(planning.getPlanningArea()));
            row.setTotalAssessmentArea(amount(planning.getAssessmentArea()));
            row.setTotalAssessmentOutputWan(resolveTotalAssessmentOutputWan(planning));
            row.setDesignStageRatio(ratio(planning.getCurrentDesignStageRatio()));
            row.setDesignStageOutputWan(amountToWan(planning.getAssessmentOutputValue()));
            row.setHistoricalIssuedRatio(quarterSummary.getHistoricalIssuedRatio());
            row.setQuarterOneRatio(quarterSummary.getQuarterOneRatio());
            row.setQuarterTwoRatio(quarterSummary.getQuarterTwoRatio());
            row.setQuarterThreeRatio(quarterSummary.getQuarterThreeRatio());
            row.setQuarterFourRatio(quarterSummary.getQuarterFourRatio());
            row.setCurrentYearRatio(quarterSummary.getCurrentYearRatio());
            row.setPendingRatio(quarterSummary.getPendingRatio());
            row.setQuarterOneAmountWan(amountToWan(quarterSummary.getQuarterOneAmount()));
            row.setQuarterTwoAmountWan(amountToWan(quarterSummary.getQuarterTwoAmount()));
            row.setQuarterThreeAmountWan(amountToWan(quarterSummary.getQuarterThreeAmount()));
            row.setQuarterFourAmountWan(amountToWan(quarterSummary.getQuarterFourAmount()));
            row.setYearTotalAmountWan(amountToWan(quarterSummary.getYearTotalAmount()));
            row.setRatioRemark(defaultString(planning.getProgressRemark()));
            rows.add(row);

            totalQuarterOneAmount = totalQuarterOneAmount.add(quarterSummary.getQuarterOneAmount()).setScale(2, RoundingMode.HALF_UP);
            totalQuarterTwoAmount = totalQuarterTwoAmount.add(quarterSummary.getQuarterTwoAmount()).setScale(2, RoundingMode.HALF_UP);
            totalQuarterThreeAmount = totalQuarterThreeAmount.add(quarterSummary.getQuarterThreeAmount()).setScale(2, RoundingMode.HALF_UP);
            totalQuarterFourAmount = totalQuarterFourAmount.add(quarterSummary.getQuarterFourAmount()).setScale(2, RoundingMode.HALF_UP);
            totalYearAmount = totalYearAmount.add(quarterSummary.getYearTotalAmount()).setScale(2, RoundingMode.HALF_UP);
        }

        if (!planningList.isEmpty()) {
            ProjectBudgetExcelBuilder.QuarterBudgetRow totalRow = new ProjectBudgetExcelBuilder.QuarterBudgetRow();
            totalRow.setTotalRow(true);
            totalRow.setTotalDesignArea(sumPlanningArea(planningList));
            totalRow.setTotalAssessmentArea(sumAssessmentArea(planningList));
            totalRow.setTotalAssessmentOutputWan(sumQuarterBudgetTotalAssessmentOutputWan(planningList));
            totalRow.setDesignStageOutputWan(amountToWan(sumAssessmentOutputValue(planningList)));
            totalRow.setQuarterOneAmountWan(amountToWan(totalQuarterOneAmount));
            totalRow.setQuarterTwoAmountWan(amountToWan(totalQuarterTwoAmount));
            totalRow.setQuarterThreeAmountWan(amountToWan(totalQuarterThreeAmount));
            totalRow.setQuarterFourAmountWan(amountToWan(totalQuarterFourAmount));
            totalRow.setYearTotalAmountWan(amountToWan(totalYearAmount));
            rows.add(totalRow);
        }
        return rows;
    }

    private BigDecimal sumQuarterBudgetTotalAssessmentOutputWan(List<ProjectPlanningDO> planningList) {
        BigDecimal total = ZERO_AMOUNT;
        if (planningList == null || planningList.isEmpty()) {
            return total;
        }
        for (ProjectPlanningDO planning : planningList) {
            total = total.add(resolveTotalAssessmentOutputWan(planning)).setScale(2, RoundingMode.HALF_UP);
        }
        return total;
    }

    private BigDecimal resolveTotalAssessmentOutputWan(ProjectPlanningDO planning) {
        if (planning == null) {
            return ZERO_AMOUNT;
        }
        BigDecimal stageRatio = ratio(planning.getCurrentDesignStageRatio());
        if (stageRatio.compareTo(BigDecimal.ZERO) <= 0) {
            return amountToWan(planning.getAssessmentOutputValue());
        }
        return amount(planning.getAssessmentOutputValue())
                .divide(stageRatio, 2, RoundingMode.HALF_UP)
                .divide(new BigDecimal("10000"), 2, RoundingMode.HALF_UP);
    }

    private List<ProjectPlanningDO> sortPlanningList(List<ProjectPlanningDO> planningList) {
        return planningList.stream()
                .sorted(Comparator.comparing(ProjectPlanningDO::getId))
                .collect(Collectors.toList());
    }

    private ProjectOutputSplitDO resolveQuarterOutputSplit(ProjectPlanningDO planning,
                                                           Map<Long, ProjectOutputSplitDO> outputSplitMap) {
        if (planning == null) {
            return buildQuarterOutputDefaultSplit(null);
        }
        ProjectOutputSplitDO outputSplit = outputSplitMap == null ? null : outputSplitMap.get(planning.getId());
        if (outputSplit != null) {
            return outputSplit;
        }
        return buildQuarterOutputDefaultSplit(planning);
    }

    private ProjectOutputSplitDO buildQuarterOutputDefaultSplit(ProjectPlanningDO planning) {
        ProjectOutputSplitDO outputSplit = new ProjectOutputSplitDO();
        if (planning != null) {
            outputSplit.setProjectId(planning.getProjectId());
            outputSplit.setPlanningId(planning.getId());
            outputSplit.setYear(resolveReportYear(planning));
        }
        outputSplit.setProjectLeadRatio(ZERO_RATIO);
        outputSplit.setOfficeRatio(FULL_RATIO);
        outputSplit.setArchRatio(FULL_RATIO);
        outputSplit.setDecorRatio(ZERO_RATIO);
        outputSplit.setStructRatio(ZERO_RATIO);
        outputSplit.setWaterRatio(ZERO_RATIO);
        outputSplit.setElecRatio(ZERO_RATIO);
        outputSplit.setHvacRatio(ZERO_RATIO);
        outputSplit.setDigitalRatio(ZERO_RATIO);
        return outputSplit;
    }

    private int designPartOrder(String designPart) {
        if (Objects.equals(designPart, ProjectPlanningBizTypeConstants.DESIGN_PART_REAL_ESTATE)) {
            return 1;
        }
        if (Objects.equals(designPart, ProjectPlanningBizTypeConstants.DESIGN_PART_UNDERGROUND)) {
            return 2;
        }
        if (Objects.equals(designPart, ProjectPlanningBizTypeConstants.DESIGN_PART_OTHER)) {
            return 3;
        }
        return 9;
    }

    private Integer resolveReportYear(ProjectPlanningDO planning) {
        return planning.getPlanningStartYear() == null ? LocalDate.now().getYear() : planning.getPlanningStartYear();
    }

    private Integer resolveExportYear(Integer year) {
        return year == null ? LocalDate.now().getYear() : year;
    }

    private List<Integer> resolveBudgetYears(ProjectPlanningDO planning, List<ProjectPlanningQuarterDO> quarterList) {
        Set<Integer> yearSet = new LinkedHashSet<>();
        if (planning != null && planning.getPlanningStartYear() != null) {
            yearSet.add(planning.getPlanningStartYear());
        }
        if (quarterList != null) {
            quarterList.stream()
                    .map(ProjectPlanningQuarterDO::getDistributionYear)
                    .filter(Objects::nonNull)
                    .sorted()
                    .forEach(yearSet::add);
        }
        if (yearSet.isEmpty()) {
            yearSet.add(LocalDate.now().getYear());
        }
        return new ArrayList<>(yearSet);
    }

    private Map<Long, List<ProjectPlanningQuarterDO>> getQuarterMap(Collection<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Long> planningIds = planningList.stream().map(ProjectPlanningDO::getId).collect(Collectors.toList());
        return projectPlanningQuarterMapper.selectListByPlanningIds(planningIds).stream()
                .collect(Collectors.groupingBy(ProjectPlanningQuarterDO::getPlanningId, LinkedHashMap::new, Collectors.toList()));
    }

    private Map<Long, ProjectOutputSplitDO> getOutputSplitMap(Collection<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProjectOutputSplitDO> map = new LinkedHashMap<>();
        for (ProjectPlanningDO planning : planningList) {
            map.put(planning.getId(), projectOutputSplitService.getOrCreateProjectOutputSplit(planning.getId()));
        }
        return map;
    }

    private Map<Long, ProjectOutputSplitDO> getExistingOutputSplitMap(Collection<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Long> planningIds = planningList.stream().map(ProjectPlanningDO::getId).collect(Collectors.toList());
        return projectOutputSplitService.getProjectOutputSplitMap(planningIds);
    }

    private Map<Long, List<SpecialtyRoleSplitRespVO>> getRoleSplitMap(Collection<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Long> planningIds = planningList.stream()
                .map(ProjectPlanningDO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        return specialtyRoleSplitService.getSpecialtyRoleSplitListMapByPlanningIds(planningIds);
    }

    private Map<Long, ProjectDO> getProjectMap(Collection<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> projectIds = planningList.stream().map(ProjectPlanningDO::getProjectId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        return projectMapper.selectBatchIds(projectIds).stream()
                .collect(Collectors.toMap(ProjectDO::getId, item -> item, (a, b) -> a));
    }

    private Map<Long, OfficeDO> getOfficeMap(Collection<EmployeeDO> employeeList) {
        if (employeeList == null || employeeList.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> officeIds = employeeList.stream()
                .map(EmployeeDO::getOfficeId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (officeIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return officeMapper.selectBatchIds(officeIds).stream()
                .collect(Collectors.toMap(OfficeDO::getId, item -> item, (a, b) -> a));
    }

    private QuarterSummary buildQuarterSummary(ProjectPlanningDO planning, List<ProjectPlanningQuarterDO> quarterList, Integer reportYear) {
        QuarterSummary summary = new QuarterSummary();
        summary.setHistoricalIssuedRatio(ZERO_RATIO);
        summary.setQuarterOneRatio(ZERO_RATIO);
        summary.setQuarterTwoRatio(ZERO_RATIO);
        summary.setQuarterThreeRatio(ZERO_RATIO);
        summary.setQuarterFourRatio(ZERO_RATIO);
        summary.setCurrentYearRatio(ZERO_RATIO);
        summary.setPendingRatio(defaultDistributionRatio(planning));
        summary.setQuarterOneAmount(ZERO_AMOUNT);
        summary.setQuarterTwoAmount(ZERO_AMOUNT);
        summary.setQuarterThreeAmount(ZERO_AMOUNT);
        summary.setQuarterFourAmount(ZERO_AMOUNT);
        summary.setYearTotalAmount(ZERO_AMOUNT);
        if (quarterList == null || quarterList.isEmpty()) {
            return summary;
        }

        BigDecimal allocatedRatio = ZERO_RATIO;
        for (ProjectPlanningQuarterDO quarter : quarterList) {
            BigDecimal distributionRatio = ratio(quarter.getDistributionRatio());
            allocatedRatio = allocatedRatio.add(distributionRatio).setScale(4, RoundingMode.HALF_UP);
            if (quarter.getDistributionYear() != null && quarter.getDistributionYear() < reportYear) {
                summary.setHistoricalIssuedRatio(summary.getHistoricalIssuedRatio().add(distributionRatio)
                        .setScale(4, RoundingMode.HALF_UP));
                continue;
            }
            if (!Objects.equals(quarter.getDistributionYear(), reportYear)) {
                continue;
            }
            BigDecimal distributionAmount = amount(quarter.getDistributionAmount());
            summary.setCurrentYearRatio(summary.getCurrentYearRatio().add(distributionRatio)
                    .setScale(4, RoundingMode.HALF_UP));
            if (Objects.equals(quarter.getQuarterNo(), 1)) {
                summary.setQuarterOneRatio(summary.getQuarterOneRatio().add(distributionRatio)
                        .setScale(4, RoundingMode.HALF_UP));
                summary.setQuarterOneAmount(summary.getQuarterOneAmount().add(distributionAmount).setScale(2, RoundingMode.HALF_UP));
            } else if (Objects.equals(quarter.getQuarterNo(), 2)) {
                summary.setQuarterTwoRatio(summary.getQuarterTwoRatio().add(distributionRatio)
                        .setScale(4, RoundingMode.HALF_UP));
                summary.setQuarterTwoAmount(summary.getQuarterTwoAmount().add(distributionAmount).setScale(2, RoundingMode.HALF_UP));
            } else if (Objects.equals(quarter.getQuarterNo(), 3)) {
                summary.setQuarterThreeRatio(summary.getQuarterThreeRatio().add(distributionRatio)
                        .setScale(4, RoundingMode.HALF_UP));
                summary.setQuarterThreeAmount(summary.getQuarterThreeAmount().add(distributionAmount).setScale(2, RoundingMode.HALF_UP));
            } else if (Objects.equals(quarter.getQuarterNo(), 4)) {
                summary.setQuarterFourRatio(summary.getQuarterFourRatio().add(distributionRatio)
                        .setScale(4, RoundingMode.HALF_UP));
                summary.setQuarterFourAmount(summary.getQuarterFourAmount().add(distributionAmount).setScale(2, RoundingMode.HALF_UP));
            }
            summary.setYearTotalAmount(summary.getYearTotalAmount().add(distributionAmount).setScale(2, RoundingMode.HALF_UP));
        }

        BigDecimal pendingRatio = defaultDistributionRatio(planning).subtract(allocatedRatio)
                .setScale(4, RoundingMode.HALF_UP);
        summary.setPendingRatio(pendingRatio.compareTo(BigDecimal.ZERO) < 0 ? ZERO_RATIO : pendingRatio);
        return summary;
    }

    private BigDecimal getCategoryRatio(ProjectOutputSplitDO outputSplit, String specialtyCode) {
        if (OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            return ratio(outputSplit.getProjectLeadRatio());
        }
        return multiplyRatio(outputSplit.getOfficeRatio(), getSpecialtyRatio(outputSplit, specialtyCode));
    }

    private BigDecimal getSpecialtyRatio(ProjectOutputSplitDO outputSplit, String specialtyCode) {
        if (outputSplit == null || specialtyCode == null) {
            return ZERO_RATIO;
        }
        switch (specialtyCode) {
            case OutputSplitBizConstants.SPECIALTY_ARCH:
                return ratio(outputSplit.getArchRatio());
            case OutputSplitBizConstants.SPECIALTY_DECOR:
                return ratio(outputSplit.getDecorRatio());
            case OutputSplitBizConstants.SPECIALTY_STRUCT:
                return ratio(outputSplit.getStructRatio());
            case OutputSplitBizConstants.SPECIALTY_WATER:
                return ratio(outputSplit.getWaterRatio());
            case OutputSplitBizConstants.SPECIALTY_ELEC:
                return ratio(outputSplit.getElecRatio());
            case OutputSplitBizConstants.SPECIALTY_HVAC:
                return ratio(outputSplit.getHvacRatio());
            case OutputSplitBizConstants.SPECIALTY_DIGITAL:
                return ratio(outputSplit.getDigitalRatio());
            default:
                return ZERO_RATIO;
        }
    }

    private String buildSpecialtySummary(ProjectOutputSplitDO outputSplit, BigDecimal baseAmount) {
        List<String> summaryList = new ArrayList<>();
        summaryList.add("建筑 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getArchRatio())).toPlainString());
        summaryList.add("装饰 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getDecorRatio())).toPlainString());
        summaryList.add("结构 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getStructRatio())).toPlainString());
        summaryList.add("给排水 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getWaterRatio())).toPlainString());
        summaryList.add("电气 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getElecRatio())).toPlainString());
        summaryList.add("暖通 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getHvacRatio())).toPlainString());
        summaryList.add("数字化设计 " + multiplyAmount(baseAmount,
                multiplyRatio(outputSplit.getOfficeRatio(), outputSplit.getDigitalRatio())).toPlainString());
        return String.join("；", summaryList);
    }

    private String resolveBudgetCategoryLabel(String ownershipType) {
        if (ProjectPlanningBizTypeConstants.isMajor(ownershipType)) {
            return "六大专业考核产值";
        }
        if (ProjectPlanningBizTypeConstants.isSpecialSubcontract(ownershipType)) {
            return "专项分包产值";
        }
        if (ProjectPlanningBizTypeConstants.isSourceCoopSubcontract(ownershipType)) {
            return "源头合作分包产值";
        }
        if (ProjectPlanningBizTypeConstants.isComprehensive(ownershipType)) {
            return "内部协作产值";
        }
        return "其他产值";
    }

    private int projectLeadOutputTypeOrder(String ownershipType) {
        String outputType = resolveBudgetCategoryLabel(ownershipType);
        if (Objects.equals(outputType, "六大专业考核产值")) {
            return 1;
        }
        if (Objects.equals(outputType, "专项分包产值")) {
            return 2;
        }
        if (Objects.equals(outputType, "源头合作分包产值")) {
            return 3;
        }
        if (Objects.equals(outputType, "内部协作产值")) {
            return 4;
        }
        return 5;
    }

    private String buildPlanningDisplayName(ProjectPlanningDO planning) {
        if (planning == null) {
            return "";
        }
        String planningContent = defaultString(planning.getPlanningContent());
        String ownershipType = defaultString(planning.getOwnershipType());
        if (planningContent.isEmpty()) {
            return ownershipType;
        }
        if (ownershipType.isEmpty()) {
            return planningContent;
        }
        return planningContent + "（" + ownershipType + "）";
    }

    private String joinPlanningRemarks(List<ProjectPlanningDO> planningList) {
        if (planningList == null || planningList.isEmpty()) {
            return "";
        }
        return planningList.stream()
                .map(ProjectPlanningDO::getProgressRemark)
                .map(this::defaultString)
                .filter(item -> !item.isEmpty())
                .distinct()
                .collect(Collectors.joining("；"));
    }

    private String buildProjectBudgetYearRangeText(List<ProjectPlanningDO> planningList,
                                                   Map<Long, List<ProjectPlanningQuarterDO>> quarterMap) {
        if (planningList == null || planningList.isEmpty()) {
            return String.valueOf(LocalDate.now().getYear());
        }
        Set<Integer> yearSet = new LinkedHashSet<>();
        for (ProjectPlanningDO planning : planningList) {
            yearSet.addAll(resolveBudgetYears(planning, quarterMap.get(planning.getId())));
        }
        List<Integer> years = yearSet.stream().sorted().collect(Collectors.toList());
        if (years.isEmpty()) {
            return String.valueOf(LocalDate.now().getYear());
        }
        if (years.size() == 1) {
            return String.valueOf(years.get(0));
        }
        return years.get(0) + "-" + years.get(years.size() - 1);
    }

    private String joinProjectRoleNames(List<ProjectRolePersonDO> rolePersons, String roleCode) {
        return joinProjectRoleNames(rolePersons, roleCode, "、");
    }

    private String joinProjectRoleNames(List<ProjectRolePersonDO> rolePersons, String roleCode, String separator) {
        if (rolePersons == null || rolePersons.isEmpty()) {
            return "";
        }
        return rolePersons.stream()
                .filter(item -> Objects.equals(item.getRoleCode(), roleCode))
                .map(ProjectRolePersonDO::getEmployeeName)
                .filter(Objects::nonNull)
                .collect(Collectors.joining(separator));
    }

    private SpecialtyRoleSplitRespVO findRoleRow(List<SpecialtyRoleSplitRespVO> roleList,
                                                 String specialtyCode, String roleCode) {
        if (roleList == null || roleList.isEmpty()) {
            return null;
        }
        return roleList.stream()
                .filter(item -> Objects.equals(item.getSpecialtyCode(), specialtyCode)
                        && Objects.equals(item.getRoleCode(), roleCode))
                .findFirst()
                .orElse(null);
    }

    private String joinRoleSplitPersonNames(SpecialtyRoleSplitRespVO roleRow) {
        return joinRoleSplitPersonNames(roleRow, "、");
    }

    private String joinRoleSplitPersonNames(SpecialtyRoleSplitRespVO roleRow, String separator) {
        if (roleRow == null || roleRow.getPersons() == null || roleRow.getPersons().isEmpty()) {
            return "";
        }
        return roleRow.getPersons().stream()
                .map(SpecialtyRolePersonRespVO::getEmployeeName)
                .filter(Objects::nonNull)
                .collect(Collectors.joining(separator));
    }

    private List<SpecialtyPersonOutputExcelBuilder.RolePersonModule> buildSpecialtyRolePersonModules(
            List<ProjectPlanningDO> planningList, Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap, String specialtyCode) {
        List<SpecialtyPersonOutputExcelBuilder.RolePersonModule> result = new ArrayList<>();
        for (String roleCode : specialtyPersonRoleOrder()) {
            Map<String, SpecialtyPersonOutputExcelBuilder.RolePersonModule> roleModuleMap = new LinkedHashMap<>();
            for (ProjectPlanningDO planning : planningList) {
                SpecialtyRoleSplitRespVO roleRow = findRoleRow(roleSplitMap.get(planning.getId()), specialtyCode, roleCode);
                if (roleRow == null || roleRow.getPersons() == null) {
                    continue;
                }
                for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
                    if (!isValidSpecialtyRolePerson(person)) {
                        continue;
                    }
                    String key = buildSpecialtyRolePersonModuleKey(roleCode, person.getEmployeeId(), person.getEmployeeName());
                    roleModuleMap.computeIfAbsent(key, item -> {
                        SpecialtyPersonOutputExcelBuilder.RolePersonModule module =
                                new SpecialtyPersonOutputExcelBuilder.RolePersonModule();
                        module.setKey(item);
                        module.setRoleCode(roleCode);
                        module.setRoleName(defaultString(roleRow.getRoleName()));
                        module.setEmployeeName(defaultString(person.getEmployeeName()));
                        module.setPlaceholder(Boolean.FALSE);
                        return module;
                    });
                }
            }
            result.addAll(roleModuleMap.values());
        }
        return result;
    }

    private List<SpecialtyPersonOutputExcelBuilder.PersonAmountModule> buildSpecialtyPersonAmountModules(
            List<ProjectPlanningDO> planningList, Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap, String specialtyCode) {
        Map<String, SpecialtyPersonOutputExcelBuilder.PersonAmountModule> moduleMap = new LinkedHashMap<>();
        for (ProjectPlanningDO planning : planningList) {
            for (String roleCode : specialtyPersonRoleOrder()) {
                SpecialtyRoleSplitRespVO roleRow = findRoleRow(roleSplitMap.get(planning.getId()), specialtyCode, roleCode);
                if (roleRow == null || roleRow.getPersons() == null) {
                    continue;
                }
                for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
                    if (!isValidSpecialtyRolePerson(person)) {
                        continue;
                    }
                    String key = buildSpecialtyPersonModuleKey(person.getEmployeeId(), person.getEmployeeName());
                    moduleMap.computeIfAbsent(key, item -> {
                        SpecialtyPersonOutputExcelBuilder.PersonAmountModule module =
                                new SpecialtyPersonOutputExcelBuilder.PersonAmountModule();
                        module.setKey(item);
                        module.setEmployeeName(defaultString(person.getEmployeeName()));
                        module.setPlaceholder(Boolean.FALSE);
                        return module;
                    });
                }
            }
        }
        return new ArrayList<>(moduleMap.values());
    }

    private List<SpecialtyPersonOutputExcelBuilder.SpecialtyPlanningRow> buildSpecialtyPlanningRows(
            List<ProjectPlanningDO> planningList,
            Map<Long, List<ProjectPlanningQuarterDO>> quarterMap,
            Map<Long, ProjectOutputSplitDO> outputSplitMap,
            Map<Long, List<SpecialtyRoleSplitRespVO>> roleSplitMap,
            String projectName,
            String specialtyCode,
            Integer reportYear) {
        List<SpecialtyPersonOutputExcelBuilder.SpecialtyPlanningRow> rows = new ArrayList<>();
        for (ProjectPlanningDO planning : planningList) {
            QuarterSummary quarterSummary = buildQuarterSummary(planning, quarterMap.get(planning.getId()), reportYear);
            ProjectOutputSplitDO outputSplit = outputSplitMap.get(planning.getId());
            BigDecimal specialtyRatio = getSpecialtyRatio(outputSplit, specialtyCode);
            BigDecimal officeRatio = outputSplit == null ? ZERO_RATIO : ratio(outputSplit.getOfficeRatio());
            BigDecimal specialtyBaseRatio = multiplyRatio(officeRatio, specialtyRatio);

            SpecialtyPersonOutputExcelBuilder.SpecialtyPlanningRow row =
                    new SpecialtyPersonOutputExcelBuilder.SpecialtyPlanningRow();
            row.setSerialNo(rows.size() + 1);
            row.setProjectName(projectName);
            row.setOutputType(resolveBudgetCategoryLabel(planning.getOwnershipType()));
            row.setDesignContent(defaultString(planning.getPlanningContent()));
            row.setSpecialtyRatio(specialtyRatio);
            row.setSpecialtyQuarterOneAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterOneAmount(), specialtyBaseRatio)));
            row.setSpecialtyQuarterTwoAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterTwoAmount(), specialtyBaseRatio)));
            row.setSpecialtyQuarterThreeAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterThreeAmount(), specialtyBaseRatio)));
            row.setSpecialtyQuarterFourAmountWan(amountToWan(multiplyAmount(quarterSummary.getQuarterFourAmount(), specialtyBaseRatio)));
            row.setSpecialtyYearTotalAmountWan(amountToWan(multiplyAmount(quarterSummary.getYearTotalAmount(), specialtyBaseRatio)));
            row.setAdjustedPersonRatio(ZERO_RATIO);
            row.setRemark(defaultString(planning.getProgressRemark()));

            List<SpecialtyRoleSplitRespVO> specialtyRoleList = roleSplitMap.getOrDefault(planning.getId(), Collections.emptyList());
            for (String roleCode : specialtyPersonRoleOrder()) {
                SpecialtyRoleSplitRespVO roleRow = findRoleRow(specialtyRoleList, specialtyCode, roleCode);
                if (roleRow == null || roleRow.getPersons() == null) {
                    continue;
                }
                BigDecimal roleRatio = ratio(roleRow.getRoleRatio());
                row.getRoleRatioMap().put(roleCode, roleRatio);
                for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
                    if (!isValidSpecialtyRolePerson(person)) {
                        continue;
                    }
                    BigDecimal personRatio = ratio(person.getPersonRatio());
                    BigDecimal personSpecialtyRatio = multiplyRatio(roleRatio, personRatio);
                    row.setAdjustedPersonRatio(addRatio(row.getAdjustedPersonRatio(), personSpecialtyRatio));
                    String personKey = buildSpecialtyPersonModuleKey(person.getEmployeeId(), person.getEmployeeName());
                    row.getAdjustedPersonRatioMap().put(personKey,
                            addRatio(row.getAdjustedPersonRatioMap().get(personKey), personSpecialtyRatio));

                    String rolePersonKey = buildSpecialtyRolePersonModuleKey(roleCode, person.getEmployeeId(), person.getEmployeeName());
                    SpecialtyPersonOutputExcelBuilder.RolePersonRatioValue rolePersonValue =
                            row.getRolePersonValueMap().computeIfAbsent(rolePersonKey, key -> {
                                SpecialtyPersonOutputExcelBuilder.RolePersonRatioValue value =
                                        new SpecialtyPersonOutputExcelBuilder.RolePersonRatioValue();
                                value.setWorkRatio(ZERO_RATIO);
                                value.setSpecialtyRatio(ZERO_RATIO);
                                return value;
                            });
                    rolePersonValue.setWorkRatio(addRatio(rolePersonValue.getWorkRatio(), personRatio));
                    rolePersonValue.setSpecialtyRatio(addRatio(rolePersonValue.getSpecialtyRatio(), personSpecialtyRatio));

                    SpecialtyPersonOutputExcelBuilder.PersonAmountValue personAmountValue =
                            row.getPersonAmountMap().computeIfAbsent(personKey, key -> new SpecialtyPersonOutputExcelBuilder.PersonAmountValue());
                    BigDecimal personAmountRatio = multiplyRatio(specialtyBaseRatio, roleRatio, personRatio);
                    accumulateSpecialtyPersonAmount(personAmountValue, quarterSummary, personAmountRatio);
                }
            }
            rows.add(row);
        }
        return rows;
    }

    private void accumulateSpecialtyPersonAmount(SpecialtyPersonOutputExcelBuilder.PersonAmountValue value,
                                                 QuarterSummary quarterSummary,
                                                 BigDecimal amountRatio) {
        value.setQuarterOneRatio(addRatio(value.getQuarterOneRatio(),
                multiplyRatio(quarterSummary.getQuarterOneRatio(), amountRatio)));
        value.setQuarterOneAmountWan(addAmountValue(value.getQuarterOneAmountWan(),
                amountToWan(multiplyAmount(quarterSummary.getQuarterOneAmount(), amountRatio))));
        value.setQuarterTwoRatio(addRatio(value.getQuarterTwoRatio(),
                multiplyRatio(quarterSummary.getQuarterTwoRatio(), amountRatio)));
        value.setQuarterTwoAmountWan(addAmountValue(value.getQuarterTwoAmountWan(),
                amountToWan(multiplyAmount(quarterSummary.getQuarterTwoAmount(), amountRatio))));
        value.setQuarterThreeRatio(addRatio(value.getQuarterThreeRatio(),
                multiplyRatio(quarterSummary.getQuarterThreeRatio(), amountRatio)));
        value.setQuarterThreeAmountWan(addAmountValue(value.getQuarterThreeAmountWan(),
                amountToWan(multiplyAmount(quarterSummary.getQuarterThreeAmount(), amountRatio))));
        value.setQuarterFourRatio(addRatio(value.getQuarterFourRatio(),
                multiplyRatio(quarterSummary.getQuarterFourRatio(), amountRatio)));
        value.setQuarterFourAmountWan(addAmountValue(value.getQuarterFourAmountWan(),
                amountToWan(multiplyAmount(quarterSummary.getQuarterFourAmount(), amountRatio))));
        value.setYearTotalRatio(addRatio(value.getYearTotalRatio(),
                multiplyRatio(quarterSummary.getCurrentYearRatio(), amountRatio)));
        value.setYearTotalAmountWan(addAmountValue(value.getYearTotalAmountWan(),
                amountToWan(multiplyAmount(quarterSummary.getYearTotalAmount(), amountRatio))));
    }

    private BigDecimal addAmountValue(BigDecimal left, BigDecimal right) {
        return amount(left).add(amount(right)).setScale(2, RoundingMode.HALF_UP);
    }

    private boolean isValidSpecialtyRolePerson(SpecialtyRolePersonRespVO person) {
        return person != null && person.getEmployeeId() != null && !defaultString(person.getEmployeeName()).isEmpty();
    }

    private List<String> specialtyPersonRoleOrder() {
        List<String> roleCodes = new ArrayList<>();
        roleCodes.add(OutputSplitBizConstants.ROLE_DIRECTOR);
        roleCodes.add(OutputSplitBizConstants.ROLE_CHECK);
        roleCodes.add(OutputSplitBizConstants.ROLE_DESIGN);
        roleCodes.add(OutputSplitBizConstants.ROLE_REVIEW);
        roleCodes.add(OutputSplitBizConstants.ROLE_APPROVE);
        return roleCodes;
    }

    private String buildSpecialtyRolePersonModuleKey(String roleCode, Long employeeId, String employeeName) {
        return defaultString(roleCode) + ":" + buildSpecialtyPersonModuleKey(employeeId, employeeName);
    }

    private String buildSpecialtyPersonModuleKey(Long employeeId, String employeeName) {
        if (employeeId != null) {
            return String.valueOf(employeeId);
        }
        return defaultString(employeeName);
    }

    private PlanningOfficeContribution buildPlanningOfficeContribution(ProjectPlanningDO planning,
                                                                       ProjectOutputSplitDO outputSplit,
                                                                       QuarterSummary quarterSummary,
                                                                       List<SpecialtyRoleSplitRespVO> roleList,
                                                                       Set<Long> employeeIds) {
        PlanningOfficeContribution contribution = new PlanningOfficeContribution();
        contribution.setTotalOutputAmount(ZERO_AMOUNT);
        contribution.setHistoricalIssuedAmount(ZERO_AMOUNT);
        contribution.setCurrentSettlementAmount(ZERO_AMOUNT);
        contribution.setPendingAmount(ZERO_AMOUNT);
        if (roleList == null || roleList.isEmpty() || employeeIds == null || employeeIds.isEmpty()) {
            return contribution;
        }

        BigDecimal totalAssessmentOutput = multiplyAmount(amount(planning.getAssessmentOutputValue()),
                defaultDistributionRatio(planning));
        BigDecimal historicalIssuedAmount = multiplyAmount(amount(planning.getAssessmentOutputValue()),
                quarterSummary.getHistoricalIssuedRatio());
        BigDecimal pendingAmount = multiplyAmount(amount(planning.getAssessmentOutputValue()),
                quarterSummary.getPendingRatio());
        for (SpecialtyRoleSplitRespVO roleRow : roleList) {
            if (roleRow == null || roleRow.getPersons() == null || roleRow.getPersons().isEmpty()) {
                continue;
            }
            BigDecimal categoryRatio = getCategoryRatio(outputSplit, roleRow.getSpecialtyCode());
            BigDecimal roleRatio = ratio(roleRow.getRoleRatio());
            for (SpecialtyRolePersonRespVO person : roleRow.getPersons()) {
                if (person == null || person.getEmployeeId() == null || !employeeIds.contains(person.getEmployeeId())) {
                    continue;
                }
                BigDecimal personRatio = ratio(person.getPersonRatio());
                BigDecimal amountRatio = multiplyRatio(categoryRatio, roleRatio, personRatio);
                if (amountRatio.compareTo(ZERO_RATIO) <= 0) {
                    continue;
                }

                contribution.setTotalOutputAmount(addAmountValue(contribution.getTotalOutputAmount(),
                        multiplyAmount(totalAssessmentOutput, amountRatio)));
                contribution.setHistoricalIssuedAmount(addAmountValue(contribution.getHistoricalIssuedAmount(),
                        multiplyAmount(historicalIssuedAmount, amountRatio)));
                contribution.setCurrentSettlementAmount(addAmountValue(contribution.getCurrentSettlementAmount(),
                        multiplyAmount(quarterSummary.getYearTotalAmount(), amountRatio)));
                contribution.setPendingAmount(addAmountValue(contribution.getPendingAmount(),
                        multiplyAmount(pendingAmount, amountRatio)));

                EmployeeAccumulator accumulator = contribution.getEmployeeAmountMap()
                        .computeIfAbsent(person.getEmployeeId(), key -> new EmployeeAccumulator());
                accumulator.addQuarterAmount(1, multiplyAmount(quarterSummary.getQuarterOneAmount(), amountRatio));
                accumulator.addQuarterAmount(2, multiplyAmount(quarterSummary.getQuarterTwoAmount(), amountRatio));
                accumulator.addQuarterAmount(3, multiplyAmount(quarterSummary.getQuarterThreeAmount(), amountRatio));
                accumulator.addQuarterAmount(4, multiplyAmount(quarterSummary.getQuarterFourAmount(), amountRatio));
            }
        }
        return contribution;
    }

    private void appendProjectOverviewContribution(ProjectOverviewAccumulator accumulator,
                                                   ProjectPlanningDO planning,
                                                   ProjectDO project,
                                                   PlanningOfficeContribution contribution) {
        accumulator.setProjectName(defaultString(accumulator.getProjectName()));
        accumulator.setTotalOutputAmount(addAmountValue(accumulator.getTotalOutputAmount(),
                contribution.getTotalOutputAmount()));
        accumulator.setHistoricalIssuedAmount(addAmountValue(accumulator.getHistoricalIssuedAmount(),
                contribution.getHistoricalIssuedAmount()));
        accumulator.setCurrentSettlementAmount(addAmountValue(accumulator.getCurrentSettlementAmount(),
                contribution.getCurrentSettlementAmount()));
        accumulator.setPendingAmount(addAmountValue(accumulator.getPendingAmount(),
                contribution.getPendingAmount()));

        String progressText = buildProgressText(project, planning);
        if (!progressText.isEmpty()) {
            accumulator.getProgressTexts().add(progressText);
        }
        String workStage = buildProjectOverviewWorkStage(planning);
        if (!workStage.isEmpty()) {
            accumulator.getWorkStages().add(workStage);
        }

        for (Map.Entry<Long, EmployeeAccumulator> entry : contribution.getEmployeeAmountMap().entrySet()) {
            EmployeeAccumulator target = accumulator.getEmployeeAmountMap()
                    .computeIfAbsent(entry.getKey(), key -> new EmployeeAccumulator());
            EmployeeAccumulator source = entry.getValue();
            target.addQuarterAmount(1, source.getQuarterOneAmount());
            target.addQuarterAmount(2, source.getQuarterTwoAmount());
            target.addQuarterAmount(3, source.getQuarterThreeAmount());
            target.addQuarterAmount(4, source.getQuarterFourAmount());
        }
    }

    private String buildProjectOverviewWorkStage(ProjectPlanningDO planning) {
        String planningContent = defaultString(planning == null ? null : planning.getPlanningContent());
        if (!planningContent.isEmpty()) {
            return planningContent;
        }
        return defaultString(planning == null ? null : planning.getDesignStage());
    }

    private List<ProjectOverviewOutputExcelBuilder.EmployeeColumn> buildProjectOverviewEmployeeColumns(
            List<EmployeeDO> employeeList) {
        if (employeeList == null || employeeList.isEmpty()) {
            return Collections.emptyList();
        }
        List<ProjectOverviewOutputExcelBuilder.EmployeeColumn> columns = new ArrayList<>();
        for (EmployeeDO employee : employeeList) {
            ProjectOverviewOutputExcelBuilder.EmployeeColumn column =
                    new ProjectOverviewOutputExcelBuilder.EmployeeColumn();
            column.setEmployeeId(employee.getId());
            column.setEmployeeName(defaultString(employee.getEmployeeName()));
            columns.add(column);
        }
        return columns;
    }

    private List<ProjectOverviewOutputExcelBuilder.ProjectRow> buildProjectOverviewRows(
            List<ProjectOverviewAccumulator> accumulatorList, List<EmployeeDO> employeeList, String sortType) {
        if (accumulatorList == null || accumulatorList.isEmpty()) {
            return Collections.emptyList();
        }
        sortProjectOverviewAccumulators(accumulatorList, sortType);
        List<ProjectOverviewOutputExcelBuilder.ProjectRow> rows = new ArrayList<>();
        int serialNo = 1;
        for (ProjectOverviewAccumulator accumulator : accumulatorList) {
            ProjectOverviewOutputExcelBuilder.ProjectRow row = new ProjectOverviewOutputExcelBuilder.ProjectRow();
            row.setSerialNo(serialNo++);
            row.setProjectName(defaultString(accumulator.getProjectName()));
            row.setProgressText(String.join("；", accumulator.getProgressTexts()));
            row.setWorkStage(String.join("；", accumulator.getWorkStages()));
            row.setTotalOutputAmount(accumulator.getTotalOutputAmount());
            row.setHistoricalIssuedRatio(divideAmountRatio(
                    accumulator.getHistoricalIssuedAmount(), accumulator.getTotalOutputAmount()));
            row.setCurrentSettlementRatio(divideAmountRatio(
                    accumulator.getCurrentSettlementAmount(), accumulator.getTotalOutputAmount()));
            row.setCurrentSettlementAmount(accumulator.getCurrentSettlementAmount());
            row.setPendingRatio(divideAmountRatio(
                    accumulator.getPendingAmount(), accumulator.getTotalOutputAmount()));
            row.setEmployeeAmountMap(buildProjectOverviewEmployeeAmountMap(accumulator.getEmployeeAmountMap(), employeeList));
            rows.add(row);
        }
        return rows;
    }

    private Map<Long, ProjectOverviewOutputExcelBuilder.EmployeeAmountValue> buildProjectOverviewEmployeeAmountMap(
            Map<Long, EmployeeAccumulator> accumulatorMap, List<EmployeeDO> employeeList) {
        Map<Long, ProjectOverviewOutputExcelBuilder.EmployeeAmountValue> result = new LinkedHashMap<>();
        if (employeeList == null || employeeList.isEmpty()) {
            return result;
        }
        for (EmployeeDO employee : employeeList) {
            EmployeeAccumulator accumulator = accumulatorMap == null ? null : accumulatorMap.get(employee.getId());
            if (accumulator == null) {
                continue;
            }
            ProjectOverviewOutputExcelBuilder.EmployeeAmountValue value =
                    new ProjectOverviewOutputExcelBuilder.EmployeeAmountValue();
            value.setQuarterOneAmount(accumulator.getQuarterOneAmount());
            value.setQuarterTwoAmount(accumulator.getQuarterTwoAmount());
            value.setQuarterThreeAmount(accumulator.getQuarterThreeAmount());
            value.setQuarterFourAmount(accumulator.getQuarterFourAmount());
            value.setAnnualTotalAmount(accumulator.getAnnualTotalAmount());
            result.put(employee.getId(), value);
        }
        return result;
    }

    private ProjectOverviewOutputExcelBuilder.ProjectRow buildProjectOverviewTotalRow(
            List<ProjectOverviewAccumulator> accumulatorList, List<EmployeeDO> employeeList) {
        ProjectOverviewOutputExcelBuilder.ProjectRow totalRow = new ProjectOverviewOutputExcelBuilder.ProjectRow();
        totalRow.setProjectName("合计");
        totalRow.setTotalOutputAmount(ZERO_AMOUNT);
        totalRow.setCurrentSettlementAmount(ZERO_AMOUNT);
        BigDecimal historicalIssuedAmount = ZERO_AMOUNT;
        BigDecimal pendingAmount = ZERO_AMOUNT;
        Map<Long, EmployeeAccumulator> employeeAccumulatorMap = new LinkedHashMap<>();
        if (employeeList != null) {
            employeeList.forEach(item -> employeeAccumulatorMap.put(item.getId(), new EmployeeAccumulator()));
        }
        if (accumulatorList != null) {
            for (ProjectOverviewAccumulator accumulatorItem : accumulatorList) {
                totalRow.setTotalOutputAmount(addAmountValue(
                        totalRow.getTotalOutputAmount(), accumulatorItem.getTotalOutputAmount()));
                totalRow.setCurrentSettlementAmount(addAmountValue(
                        totalRow.getCurrentSettlementAmount(), accumulatorItem.getCurrentSettlementAmount()));
                historicalIssuedAmount = addAmountValue(
                        historicalIssuedAmount, accumulatorItem.getHistoricalIssuedAmount());
                pendingAmount = addAmountValue(pendingAmount, accumulatorItem.getPendingAmount());
                for (Map.Entry<Long, EmployeeAccumulator> entry : accumulatorItem.getEmployeeAmountMap().entrySet()) {
                    EmployeeAccumulator accumulator = employeeAccumulatorMap
                            .computeIfAbsent(entry.getKey(), key -> new EmployeeAccumulator());
                    EmployeeAccumulator value = entry.getValue();
                    accumulator.addQuarterAmount(1, value.getQuarterOneAmount());
                    accumulator.addQuarterAmount(2, value.getQuarterTwoAmount());
                    accumulator.addQuarterAmount(3, value.getQuarterThreeAmount());
                    accumulator.addQuarterAmount(4, value.getQuarterFourAmount());
                }
            }
        }
        totalRow.setHistoricalIssuedRatio(divideAmountRatio(historicalIssuedAmount, totalRow.getTotalOutputAmount()));
        totalRow.setCurrentSettlementRatio(divideAmountRatio(
                totalRow.getCurrentSettlementAmount(), totalRow.getTotalOutputAmount()));
        totalRow.setPendingRatio(divideAmountRatio(pendingAmount, totalRow.getTotalOutputAmount()));
        totalRow.setEmployeeAmountMap(buildProjectOverviewEmployeeAmountMap(employeeAccumulatorMap, employeeList));
        return totalRow;
    }

    private void sortProjectOverviewAccumulators(List<ProjectOverviewAccumulator> rows, String sortType) {
        Comparator<ProjectOverviewAccumulator> comparator;
        if (Objects.equals(sortType, SORT_OUTPUT_ASC)) {
            comparator = Comparator.comparing(ProjectOverviewAccumulator::getTotalOutputAmount, this::compareAmount)
                    .thenComparing(ProjectOverviewAccumulator::getProjectName, String.CASE_INSENSITIVE_ORDER);
        } else if (Objects.equals(sortType, SORT_NAME_ASC)) {
            comparator = Comparator.comparing(ProjectOverviewAccumulator::getProjectName, String.CASE_INSENSITIVE_ORDER);
        } else {
            comparator = Comparator.comparing(ProjectOverviewAccumulator::getTotalOutputAmount, this::compareAmount)
                    .reversed()
                    .thenComparing(ProjectOverviewAccumulator::getProjectName, String.CASE_INSENSITIVE_ORDER);
        }
        rows.sort(comparator);
    }

    private String buildProgressText(ProjectDO project, ProjectPlanningDO planning) {
        String projectStatus = project == null ? "" : defaultString(project.getProjectStatus());
        String planningRemark = planning == null ? "" : defaultString(planning.getProgressRemark());
        if (projectStatus.isEmpty()) {
            return planningRemark;
        }
        if (planningRemark.isEmpty()) {
            return projectStatus;
        }
        return projectStatus + "；" + planningRemark;
    }

    private String getOfficeName(Map<Long, OfficeDO> officeMap, Long officeId) {
        if (officeId == null || officeMap == null || !officeMap.containsKey(officeId)) {
            return "";
        }
        return defaultString(officeMap.get(officeId).getOfficeName());
    }

    private void sortEmployeeRows(List<EmployeeOutputSummaryExcelRespVO> rows, String sortType) {
        Comparator<EmployeeOutputSummaryExcelRespVO> comparator;
        if (Objects.equals(sortType, SORT_NAME_ASC)) {
            comparator = Comparator.comparing(EmployeeOutputSummaryExcelRespVO::getEmployeeName,
                    String.CASE_INSENSITIVE_ORDER);
        } else if (Objects.equals(sortType, SORT_ANNUAL_TOTAL_ASC)) {
            comparator = Comparator.comparing(EmployeeOutputSummaryExcelRespVO::getAnnualTotalAmount, this::compareAmount)
                    .thenComparing(EmployeeOutputSummaryExcelRespVO::getEmployeeName, String.CASE_INSENSITIVE_ORDER);
        } else {
            comparator = Comparator.comparing(EmployeeOutputSummaryExcelRespVO::getAnnualTotalAmount, this::compareAmount)
                    .reversed()
                    .thenComparing(EmployeeOutputSummaryExcelRespVO::getEmployeeName, String.CASE_INSENSITIVE_ORDER);
        }
        rows.sort(comparator);
    }

    private int compareAmount(BigDecimal left, BigDecimal right) {
        return amount(left).compareTo(amount(right));
    }

    private boolean hasYearAmount(List<ProjectPlanningQuarterDO> quarterList, Integer year) {
        if (quarterList == null || quarterList.isEmpty()) {
            return false;
        }
        return quarterList.stream()
                .anyMatch(item -> Objects.equals(item.getDistributionYear(), year)
                        && amount(item.getDistributionAmount()).compareTo(ZERO_AMOUNT) > 0);
    }

    private BigDecimal firstNonZero(List<BigDecimal> values) {
        if (values == null || values.isEmpty()) {
            return ZERO_AMOUNT;
        }
        return values.stream()
                .filter(Objects::nonNull)
                .map(this::amount)
                .filter(value -> value.compareTo(BigDecimal.ZERO) > 0)
                .findFirst()
                .orElse(ZERO_AMOUNT);
    }

    private BigDecimal defaultDistributionRatio(ProjectPlanningDO planning) {
        if (planning == null || planning.getTotalDistributionAmount() == null) {
            return new BigDecimal("1.0000");
        }
        return ratio(planning.getTotalDistributionAmount());
    }

    private BigDecimal amount(BigDecimal value) {
        return value == null ? ZERO_AMOUNT : value.setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal amountToWan(BigDecimal value) {
        return amount(value).divide(new BigDecimal("10000"), 2, RoundingMode.HALF_UP);
    }

    private BigDecimal ratio(BigDecimal value) {
        return value == null ? ZERO_RATIO : value.setScale(4, RoundingMode.HALF_UP);
    }

    private BigDecimal multiplyAmount(BigDecimal amount, BigDecimal... ratios) {
        BigDecimal result = amount(amount);
        for (BigDecimal ratio : ratios) {
            result = result.multiply(ratio(ratio));
        }
        return result.setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal multiplyRatio(BigDecimal... ratios) {
        BigDecimal result = BigDecimal.ONE;
        for (BigDecimal ratio : ratios) {
            result = result.multiply(ratio(ratio));
        }
        return result.setScale(4, RoundingMode.HALF_UP);
    }

    private BigDecimal addRatio(BigDecimal left, BigDecimal right) {
        return ratio(left).add(ratio(right)).setScale(4, RoundingMode.HALF_UP);
    }

    private BigDecimal divideAmountRatio(BigDecimal numerator, BigDecimal denominator) {
        BigDecimal denominatorAmount = amount(denominator);
        if (denominatorAmount.compareTo(BigDecimal.ZERO) == 0) {
            return ZERO_RATIO;
        }
        return amount(numerator).divide(denominatorAmount, 4, RoundingMode.HALF_UP);
    }

    private String percentText(BigDecimal value) {
        return ratio(value).multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) + "%";
    }

    private String buildFileName(String prefix, String suffix) {
        return sanitizeFileName(prefix) + "_" + sanitizeFileName(suffix) + ".xlsx";
    }

    private String buildFileName(String prefix, Integer reportYear, String suffix) {
        if (reportYear == null) {
            return buildFileName(prefix, suffix);
        }
        return sanitizeFileName(prefix) + "_" + reportYear + "_" + sanitizeFileName(suffix) + ".xlsx";
    }

    private String sanitizeFileName(String value) {
        if (value == null || value.trim().isEmpty()) {
            return "报表";
        }
        return value.replaceAll("[\\\\/:*?\"<>|]", "_");
    }

    private String defaultString(String value) {
        return value == null ? "" : value.trim();
    }

    @Data
    private static class SpecialtyPersonOutputBuildResult {
        private ProjectPlanningDO anchorPlanning;
        private ProjectDO project;
        private Integer reportYear;
        private SpecialtyPersonOutputExcelBuilder.ExportData data;
    }

    @Data
    private static class SpecialtyQuarterPreviewAggregate {
        private BigDecimal specialtyQuarterOneAmountWan = ZERO_AMOUNT;
        private BigDecimal specialtyQuarterTwoAmountWan = ZERO_AMOUNT;
        private BigDecimal specialtyQuarterThreeAmountWan = ZERO_AMOUNT;
        private BigDecimal specialtyQuarterFourAmountWan = ZERO_AMOUNT;
        private BigDecimal specialtyYearTotalAmountWan = ZERO_AMOUNT;
        private Map<String, SpecialtyPersonOutputExcelBuilder.PersonAmountValue> rolePersonAmountMap = new LinkedHashMap<>();
    }

    @Data
    private static class PlanningOfficeContribution {
        private BigDecimal totalOutputAmount = ZERO_AMOUNT;
        private BigDecimal historicalIssuedAmount = ZERO_AMOUNT;
        private BigDecimal currentSettlementAmount = ZERO_AMOUNT;
        private BigDecimal pendingAmount = ZERO_AMOUNT;
        private Map<Long, EmployeeAccumulator> employeeAmountMap = new LinkedHashMap<>();
    }

    @Data
    private static class ProjectOverviewAccumulator {
        private Long projectId;
        private String projectName;
        private BigDecimal totalOutputAmount = ZERO_AMOUNT;
        private BigDecimal historicalIssuedAmount = ZERO_AMOUNT;
        private BigDecimal currentSettlementAmount = ZERO_AMOUNT;
        private BigDecimal pendingAmount = ZERO_AMOUNT;
        private Set<String> progressTexts = new LinkedHashSet<>();
        private Set<String> workStages = new LinkedHashSet<>();
        private Map<Long, EmployeeAccumulator> employeeAmountMap = new LinkedHashMap<>();
    }

    @Data
    private static class QuarterSummary {
        private BigDecimal historicalIssuedRatio;
        private BigDecimal quarterOneRatio;
        private BigDecimal quarterTwoRatio;
        private BigDecimal quarterThreeRatio;
        private BigDecimal quarterFourRatio;
        private BigDecimal currentYearRatio;
        private BigDecimal pendingRatio;
        private BigDecimal quarterOneAmount;
        private BigDecimal quarterTwoAmount;
        private BigDecimal quarterThreeAmount;
        private BigDecimal quarterFourAmount;
        private BigDecimal yearTotalAmount;
    }

    @Data
    private static class EmployeeAccumulator {
        private BigDecimal quarterOneAmount = ZERO_AMOUNT;
        private BigDecimal quarterTwoAmount = ZERO_AMOUNT;
        private BigDecimal quarterThreeAmount = ZERO_AMOUNT;
        private BigDecimal quarterFourAmount = ZERO_AMOUNT;

        public void addQuarterAmount(int quarterNo, BigDecimal amount) {
            if (quarterNo == 1) {
                quarterOneAmount = quarterOneAmount.add(amount == null ? ZERO_AMOUNT : amount).setScale(2, RoundingMode.HALF_UP);
                return;
            }
            if (quarterNo == 2) {
                quarterTwoAmount = quarterTwoAmount.add(amount == null ? ZERO_AMOUNT : amount).setScale(2, RoundingMode.HALF_UP);
                return;
            }
            if (quarterNo == 3) {
                quarterThreeAmount = quarterThreeAmount.add(amount == null ? ZERO_AMOUNT : amount).setScale(2, RoundingMode.HALF_UP);
                return;
            }
            if (quarterNo == 4) {
                quarterFourAmount = quarterFourAmount.add(amount == null ? ZERO_AMOUNT : amount).setScale(2, RoundingMode.HALF_UP);
            }
        }

        public BigDecimal getAnnualTotalAmount() {
            return quarterOneAmount.add(quarterTwoAmount).add(quarterThreeAmount).add(quarterFourAmount)
                    .setScale(2, RoundingMode.HALF_UP);
        }
    }

}
