excel动态表头实现+异步使用

动态表头实现

controller接口

java 复制代码
package cn.com.fsg.ihro.report.controller;

import cn.com.fsg.common.pojo.R;
import cn.com.fsg.ihro.report.constant.AsyncTaskInfoEnum;
import cn.com.fsg.ihro.report.pojo.entity.asyncTaskInfo.AsyncTaskInfoDO;
import cn.com.fsg.ihro.report.pojo.vo.purchase.PurchaseReqVO;
import cn.com.fsg.ihro.report.service.PurchaseReportService;
import cn.com.fsg.ihro.report.service.asyncTaskInfo.AsyncTaskInfoService;
import cn.com.fsg.plugins.web.core.context.UserContextHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

/**
 * 采购报表
 *
 * @author zkg
 * @since 2025-10-17 15:56
 */

@Slf4j
@Validated
@RestController
@RequestMapping("/purchase")
@Tag(name = "采购报表")
@RequiredArgsConstructor
public class PurchaseReportController {

    private final PurchaseReportService purchaseReportService;

    private final AsyncTaskInfoService asyncTaskInfoService;

    @GetMapping("/export")
    @Operation(summary = "体检/商管/商业保险采购名单报表")
    // @PreAuthorize("@ss.hasPermission('report:purchase:query')")
    public R physicalExam(@Valid PurchaseReqVO reqVO) {
        // 根据任务类型初始化异步任务
        AsyncTaskInfoEnum taskType = AsyncTaskInfoEnum.valueOf(reqVO.getPurTaskType());
        AsyncTaskInfoDO taskInfo = asyncTaskInfoService.initAsyncTaskInfo(taskType);
        taskInfo.setRemark(reqVO.getReportDateStart() + "至" + reqVO.getReportDateEnd());

        // 根据不同类型调用相应的方法
        if (AsyncTaskInfoEnum.PHYSICAL_EXAM.name().equals(reqVO.getPurTaskType())) {
            // 异步处理:体检采购订单名单导出
            reqVO.setDeptId(UserContextHolder.getDeptId());
            purchaseReportService.physicalExam(reqVO, taskInfo);
        } else if (AsyncTaskInfoEnum.BUSINESS_MANAGEMENT.name().equals(reqVO.getPurTaskType())) {
            // 异步处理:商管采购名单导出
            reqVO.setDeptId(UserContextHolder.getDeptId());
            purchaseReportService.businessManagement(reqVO, taskInfo);
        } else if (AsyncTaskInfoEnum.BUSINESS_INSURANCE.name().equals(reqVO.getPurTaskType())) {
            // 异步处理:商业保险采购名单导出
            purchaseReportService.businessInsurance(reqVO, taskInfo);
        }

        return R.success();
    }

}

异步service

java 复制代码
package cn.com.fsg.ihro.report.service.asyncTaskInfo;

import cn.com.fsg.common.pojo.PageParam;
import cn.com.fsg.common.pojo.PageResult;
import cn.com.fsg.ihro.report.constant.AsyncTaskInfoEnum;
import cn.com.fsg.ihro.report.pojo.entity.asyncTaskInfo.AsyncTaskInfoDO;
import cn.com.fsg.ihro.report.pojo.vo.asyncTaskInfo.AsyncTaskInfoQueryReqVO;
import cn.com.fsg.ihro.report.pojo.vo.asyncTaskInfo.AsyncTaskInfoRespVO;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * 任务清单表(AsyncTaskInfo)服务接口
 *
 * @author makejava
 * @since 2025-10-11 11:09:49
 */
public interface AsyncTaskInfoService extends IService<AsyncTaskInfoDO> {

    /**
     * 分页查询
     *
     * @param reqVO 入参
     * @param pageReqVO 分页入参
     * @return 结果
     */
    PageResult<AsyncTaskInfoRespVO> pageQuery(AsyncTaskInfoQueryReqVO reqVO, PageParam pageReqVO);


    /**
     * 初始化数据到异步表
     *
     * @param infoEnum 入参
     * @return 结果
     */
    AsyncTaskInfoDO initAsyncTaskInfo(AsyncTaskInfoEnum infoEnum);

    /**
     * 初始化数据到异步表(自定义任务名)
     *
     * @param attachType 业务类型
     * @param taskName   任务名称
     * @return 结果
     */
    AsyncTaskInfoDO initAsyncTaskInfo(String attachType, String taskName);

    /**
     * 使用新的事务-修改异步任务数据
     *
     * @param taskInfo 入参
     * @param message  异常信息
     */
    void updateErrorInfoNewTx(AsyncTaskInfoDO taskInfo, String message);

}

异步service实现

java 复制代码
package cn.com.fsg.ihro.report.service.asyncTaskInfo;

import cn.com.fsg.common.pojo.PageParam;
import cn.com.fsg.common.pojo.PageResult;
import cn.com.fsg.ihro.report.constant.AsyncTaskInfoEnum;
import cn.com.fsg.ihro.report.constant.AsyncTaskStatusEnum;
import cn.com.fsg.ihro.report.convert.asyncTaskInfo.AsyncTaskInfoConvert;
import cn.com.fsg.ihro.report.dao.asyncTaskInfo.AsyncTaskInfoMapper;
import cn.com.fsg.ihro.report.pojo.entity.asyncTaskInfo.AsyncTaskInfoDO;
import cn.com.fsg.ihro.report.pojo.vo.asyncTaskInfo.AsyncTaskInfoQueryReqVO;
import cn.com.fsg.ihro.report.pojo.vo.asyncTaskInfo.AsyncTaskInfoRespVO;
import cn.com.fsg.plugins.mybatis.core.query.LambdaQueryWrapperPlus;
import cn.com.fsg.plugins.web.core.context.UserContextHolder;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * 任务清单表(AsyncTaskInfo)服务实现类
 *
 * @author makejava
 * @since 2025-10-11 11:09:49
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AsyncTaskInfoServiceImpl extends ServiceImpl<AsyncTaskInfoMapper, AsyncTaskInfoDO> implements AsyncTaskInfoService {

    @Resource
    private AsyncTaskInfoMapper asyncTaskInfoMapper;

    /**
     * 分页查询
     *
     * @param reqVO 入参
     * @param pageReqVO 分页入参
     * @return 结果
     */
    @Override
    public PageResult<AsyncTaskInfoRespVO> pageQuery(AsyncTaskInfoQueryReqVO reqVO, PageParam pageReqVO) {
        LambdaQueryWrapperPlus<AsyncTaskInfoDO> wrapperPlus = new LambdaQueryWrapperPlus<AsyncTaskInfoDO>()
                .inIfPresent(AsyncTaskInfoDO::getAttachType, reqVO.getAttachTypeList())
                .inIfPresent(AsyncTaskInfoDO::getTaskName, reqVO.getTaskNameList())
                .inIfPresent(AsyncTaskInfoDO::getProjectId, reqVO.getProjectIdList())
                .eqIfPresent(AsyncTaskInfoDO::getTaskStatus, reqVO.getTaskStatus())
                .eq(AsyncTaskInfoDO::getCreator, UserContextHolder.getUserId())
                .orderByDesc(AsyncTaskInfoDO::getId);
        PageResult<AsyncTaskInfoDO> page = asyncTaskInfoMapper.selectPage(pageReqVO, wrapperPlus);
        return AsyncTaskInfoConvert.INSTANCE.convertPage(page.getList(), page.getTotal());
    }

    /**
     * 初始化数据到异步表
     *
     * @param infoEnum 入参
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AsyncTaskInfoDO initAsyncTaskInfo(AsyncTaskInfoEnum infoEnum) {
        return initAsyncTaskInfo(infoEnum.name(), infoEnum.getTaskName());
    }

    /**
     * 初始化数据到异步表(自定义任务名)
     *
     * @param attachType 业务类型
     * @param taskName   任务名称
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AsyncTaskInfoDO initAsyncTaskInfo(String attachType, String taskName) {
        log.info("initAsyncTaskInfo===初始化数据到异步表===start===attachType:{}, taskName:{}", attachType, taskName);
        // 新增数据到异步表
        AsyncTaskInfoDO taskInfo = AsyncTaskInfoDO.builder()
                .taskNo(IdUtil.fastSimpleUUID())
                .taskName(taskName)
                .attachType(attachType)
                .taskStartDate(LocalDateTime.now())
                .taskStatus(AsyncTaskStatusEnum.INIT.getStatus())
                .build();
        taskInfo.setDeleted(false);
        asyncTaskInfoMapper.insert(taskInfo);
        log.info("initAsyncTaskInfo===初始化数据到异步表===end===taskInfo:{}", taskInfo);
        return taskInfo;
    }

    /**
     * 使用新的事务-修改异步任务数据
     *
     * @param taskInfo 入参
     * @param message  失败原因
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateErrorInfoNewTx(AsyncTaskInfoDO taskInfo, String message) {
        // 任务结束时间
        taskInfo.setTaskEndDate(LocalDateTime.now());
        // 设置任务状态为失败
        taskInfo.setTaskStatus(AsyncTaskStatusEnum.FAILED.getStatus());
        // 失败原因
        taskInfo.setTaskErrorInfo("失败原因:" + message);
        // 修改异步任务数据
        asyncTaskInfoMapper.updateById(taskInfo);
        log.info("updateErrorInfoNewTx===使用新的事务-修改异步任务数据===taskInfo:{}", taskInfo);
    }

}

导出Service

java 复制代码
package cn.com.fsg.ihro.report.service;

import cn.com.fsg.ihro.report.pojo.entity.asyncTaskInfo.AsyncTaskInfoDO;
import cn.com.fsg.ihro.report.pojo.vo.purchase.PurchaseReqVO;

/**
 * 采购报表
 *
 * @author zkg
 * @since 2025-10-17 15:56
 */
public interface PurchaseReportService {

    /**
     * 体检采购订单名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    void physicalExam(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo);

    /**
     * 商管采购名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    void businessManagement(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo);

    /**
     * 商业保险采购名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    void businessInsurance(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo);
}

导出Service实现

java 复制代码
package cn.com.fsg.ihro.report.service.impl;

import cn.com.fsg.ihro.report.constant.AsyncTaskStatusEnum;
import cn.com.fsg.ihro.report.constant.EmployeeEnum;
import cn.com.fsg.ihro.report.convert.DynamicExcelConvert;
import cn.com.fsg.ihro.report.convert.PurchaseConvert;
import cn.com.fsg.ihro.report.convert.employeeFamily.EmployeeFamilyConvert;
import cn.com.fsg.ihro.report.dao.PurchaseReportMapper;
import cn.com.fsg.ihro.report.dao.employeeAccount.EmployeeAccountMapper;
import cn.com.fsg.ihro.report.dao.employeeFamily.EmployeeFamilyMapper;
import cn.com.fsg.ihro.report.pojo.dto.CustShareDTO;
import cn.com.fsg.ihro.report.pojo.entity.asyncTaskInfo.AsyncTaskInfoDO;
import cn.com.fsg.ihro.report.pojo.entity.employeeAccount.EmployeeAccountDO;
import cn.com.fsg.ihro.report.pojo.entity.employeeFamily.EmployeeFamilyDO;
import cn.com.fsg.ihro.report.pojo.vo.purchase.*;
import cn.com.fsg.ihro.report.service.EmployeeService;
import cn.com.fsg.ihro.report.service.PurchaseReportService;
import cn.com.fsg.ihro.report.service.asyncTaskInfo.AsyncTaskInfoService;
import cn.com.fsg.module.basic.api.file.FileApi;
import cn.com.fsg.module.basic.dto.UploadReqDTO;
import cn.com.fsg.module.basic.dto.UploadRespDTO;
import cn.com.fsg.plugins.excel.core.util.ExcelUtils;
import cn.com.fsg.plugins.excel.core.vo.ExcelSheetDynamicVO;
import cn.com.fsg.plugins.excel.core.vo.ExcelSheetVO;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 采购报表
 *
 * @author zkg
 * @since 2025-10-17 15:56
 */
@Slf4j
@Service
public class PurchaseReportServiceImpl implements PurchaseReportService {

    @Resource
    private PurchaseReportMapper purchaseReportMapper;

    @Resource
    private FileApi fileApi;

    @Resource
    private AsyncTaskInfoService asyncTaskInfoService;

    @Resource
    private EmployeeAccountMapper employeeAccountMapper;

    @Resource
    private EmployeeFamilyMapper employeeFamilyMapper;

    @Resource
    private EmployeeService employeeService;

    /**
     * 体检采购订单名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    @Override
    @Async
    @DSTransactional()
    public void physicalExam(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo) {
        log.info("physicalExam===体检采购订单名单导出===reqVO:{},taskInfo:{}", reqVO, taskInfo);
        try {
            // 体检采购订单名单导出-费用合计
            List<FeeTotalExcelVO> list1 = purchaseReportMapper.queryFeeTotal(reqVO);

            // 体检采购订单名单导出-人员名单
            List<EmployeeListExcelVO> list2 = purchaseReportMapper.queryEmployeeList(reqVO);

            if (CollectionUtils.isNotEmpty(list1) || CollectionUtils.isNotEmpty(list2)) {
                List<ExcelSheetVO<?>> excelSheetVOList = new ArrayList<>();

                ExcelSheetVO<FeeTotalExcelVO> sheet1 = new ExcelSheetVO<>();
                sheet1.setSheetName("费用合计");
                sheet1.setHeadClass(FeeTotalExcelVO.class);
                sheet1.setData(list1);
                excelSheetVOList.add(sheet1);

                ExcelSheetVO<EmployeeListExcelVO> sheet2 = new ExcelSheetVO<>();
                sheet2.setSheetName("人员名单");
                sheet2.setHeadClass(EmployeeListExcelVO.class);
                sheet2.setData(list2);
                excelSheetVOList.add(sheet2);
                // 生成并上传文件
                this.generateAndUploadReport(taskInfo, excelSheetVOList,
                        DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN) + "体检采购订单名单导出.xlsx");
                // 更新 服务采购订单.是否生成报表标识="是"
                List<Long> idList = list2.stream().map(EmployeeListExcelVO::getPurSerOrderId).distinct().collect(Collectors.toList());
                purchaseReportMapper.updateIsGenerateReport(idList);
            } else {
                // 失败提示
                taskInfo.setTaskErrorInfo("没有符合条件的数据,请检查");
            }
            // 设置任务状态为成功
            taskInfo.setTaskStatus(AsyncTaskStatusEnum.SUCCESS.getStatus());
            // 任务结束时间
            taskInfo.setTaskEndDate(LocalDateTime.now());
            asyncTaskInfoService.updateById(taskInfo);
        } catch (Exception e) {
            // 使用新的事务-修改异步任务数据
            asyncTaskInfoService.updateErrorInfoNewTx(taskInfo, e.getMessage());
            log.error("businessInsurance===体检采购订单名单导出====异常: {}", e.getMessage(), e);
        }
    }

    /**
     * 商管采购名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    @Override
    @Async
    @DSTransactional()
    public void businessManagement(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo) {
        log.info("businessManagement===商管采购名单导出===reqVO:{},taskInfo:{}", reqVO, taskInfo);
        try {
            // 商管采购名单导出-发放名单
            List<DistributionListExcelVO> list2 = purchaseReportMapper.queryDistributionList(reqVO);

            if (CollectionUtils.isNotEmpty(list2)) {
                List<ExcelSheetVO<?>> excelSheetVOList = new ArrayList<>();

                // 收集雇员id
                List<Long> empDutyIdList = list2.stream().map(DistributionListExcelVO::getEmpDutyId).distinct().collect(Collectors.toList());
                // 获取雇员上岗客户信息
                Map<Long, List<CustShareDTO>> custShareMap = employeeService.getCustShareRatios(empDutyIdList).getCustMap();
                for (DistributionListExcelVO distributionListExcelVO : list2) {
                    List<CustShareDTO> custList = custShareMap.get(distributionListExcelVO.getEmpDutyId());
                    // 取默认结算客户,如果没有取第一条数据
                    CustShareDTO custShareDTO = custList.stream().filter(CustShareDTO::getDefaultSettle).findFirst().orElse(custList.get(0));
                    // 设置客户编码
                    distributionListExcelVO.setCustomerCode(custShareDTO.getCustNo());
                    // 获取客户名称
                    distributionListExcelVO.setCustomerName(custShareDTO.getCustName());
                }

                // 商管采购名单导出-费用汇总
                List<FeeSummaryExcelVO> list1 = PurchaseConvert.INSTANCE.convertList(list2);
                ExcelSheetVO<FeeSummaryExcelVO> sheet1 = new ExcelSheetVO<>();
                sheet1.setSheetName("费用汇总");
                sheet1.setHeadClass(FeeSummaryExcelVO.class);
                sheet1.setData(list1);
                excelSheetVOList.add(sheet1);

                ExcelSheetVO<DistributionListExcelVO> sheet2 = new ExcelSheetVO<>();
                sheet2.setSheetName("发放名单");
                sheet2.setHeadClass(DistributionListExcelVO.class);
                sheet2.setData(list2);
                excelSheetVOList.add(sheet2);
                // 生成并上传文件
                this.generateAndUploadReport(taskInfo, excelSheetVOList,
                        DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN) + "商管采购名单导出.xlsx");
                // 更新 服务采购订单.是否生成报表标识="是"
                List<Long> idList = list2.stream().map(DistributionListExcelVO::getPurSerOrderId).distinct().collect(Collectors.toList());
                purchaseReportMapper.updateIsGenerateReport(idList);
            } else {
                // 失败提示
                taskInfo.setTaskErrorInfo("没有符合条件的数据,请检查");
            }
            // 设置任务状态为成功
            taskInfo.setTaskStatus(AsyncTaskStatusEnum.SUCCESS.getStatus());
            // 任务结束时间
            taskInfo.setTaskEndDate(LocalDateTime.now());
            asyncTaskInfoService.updateById(taskInfo);
        } catch (Exception e) {
            // 使用新的事务-修改异步任务数据
            asyncTaskInfoService.updateErrorInfoNewTx(taskInfo, e.getMessage());
            log.error("businessManagement===商管采购名单导出====异常: {}", e.getMessage(), e);
        }
    }

    /**
     * 商业保险采购名单导出
     *
     * @param reqVO    入参
     * @param taskInfo 任务信息
     */
    @Override
    @Async
    @DSTransactional()
    public void businessInsurance(PurchaseReqVO reqVO, AsyncTaskInfoDO taskInfo) {
        log.info("businessInsurance===商业保险采购名单导出===reqVO:{},taskInfo:{}", reqVO, taskInfo);
        try {
            // 商业保险采购名单导出-加保
            List<AddInsuranceExcelVO> list1 = purchaseReportMapper.queryAddInsurance(reqVO);

            // 商业保险采购名单导出-减保
            List<SubInsuranceExcelVO> list2 = purchaseReportMapper.queryReduceInsurance(reqVO);

            if (CollectionUtils.isNotEmpty(list1) || CollectionUtils.isNotEmpty(list2)) {
                List<ExcelSheetDynamicVO> excelSheetVOList = new ArrayList<>();
                if (CollectionUtils.isNotEmpty(list1)) {
                    log.info("businessInsurance===加保===list1:{}", list1);
                    List<AddInsuranceExcelVO> addList = new ArrayList<>();
                    // 收集雇员id
                    List<Long> empIdList = list1.stream().map(AddInsuranceExcelVO::getEmpId).distinct().collect(Collectors.toList());
                    // 根据雇员id查询雇员默认银行卡信息
                    List<EmployeeAccountDO> employeeAccountList = employeeAccountMapper.selectDefaultAccount(empIdList);
                    // 雇员id为key,EmployeeAccountDO为value,将employeeAccountList转为map
                    Map<Long, EmployeeAccountDO> employeeAccountMap = employeeAccountList.stream()
                            .collect(Collectors.toMap(EmployeeAccountDO::getEmployeeId, employeeAccountDO -> employeeAccountDO));

                    // 收集类型为子女的雇员id
                    List<Long> childEmpIdList = list1.stream().filter(vo -> EmployeeEnum.EmployeeType.CHILD.getValue().equals(vo.getEmpType()))
                            .map(AddInsuranceExcelVO::getEmpId).distinct().collect(Collectors.toList());
                    Map<Long, List<EmployeeFamilyDO>> employeeFamilyMap = new HashMap<>();
                    if (CollectionUtils.isNotEmpty(childEmpIdList)) {
                        // 根据雇员id+日期,查询02雇员子女信息
                        List<EmployeeFamilyDO> employeeFamilyList = employeeFamilyMapper.selectByEmpIdAndDate(childEmpIdList, reqVO.getReportDateStart(), reqVO.getReportDateEnd());
                        // 雇员id为key,List<EmployeeFamilyDO>为value,将 employeeFamilyList 转为map
                        employeeFamilyMap = employeeFamilyList.stream().collect(Collectors.groupingBy(EmployeeFamilyDO::getEmployeeId));
                    }

                    for (AddInsuranceExcelVO addInsuranceVO : list1) {
                        Long empId = addInsuranceVO.getEmpId();
                        // 银行卡信息
                        EmployeeAccountDO employeeAccountDO = employeeAccountMap.get(empId);
                        if (employeeAccountDO != null) {
                            addInsuranceVO.setBankAccountName(employeeAccountDO.getBankAccountName());
                            addInsuranceVO.setBankAccountCode(employeeAccountDO.getBankAccountCode());
                            addInsuranceVO.setBankCode(employeeAccountDO.getBankCode());
                        }

                        // 如果是雇员保持原有数据,如果是子女需要变成多行
                        if (EmployeeEnum.EmployeeType.EMPLOYEE.getValue().equals(addInsuranceVO.getEmpType())) {
                            addList.add(addInsuranceVO);
                        } else if (EmployeeEnum.EmployeeType.CHILD.getValue().equals(addInsuranceVO.getEmpType())) {
                            // 子女信息
                            List<EmployeeFamilyDO> employeeFamilyDOS = employeeFamilyMap.get(empId);
                            if (CollectionUtils.isNotEmpty(employeeFamilyDOS)) {
                                for (EmployeeFamilyDO employeeFamilyDO : employeeFamilyDOS) {
                                    AddInsuranceExcelVO addInsuranceVONew = EmployeeFamilyConvert.INSTANCE.convertVO(addInsuranceVO);
                                    addInsuranceVONew.setInsuredName(employeeFamilyDO.getFamilyName());
                                    addInsuranceVONew.setInsuredIdType(employeeFamilyDO.getFamilyIdType());
                                    addInsuranceVONew.setInsuredIdNumber(employeeFamilyDO.getFamilyIdNumber());
                                    addInsuranceVONew.setInsuredGender(employeeFamilyDO.getGender());
                                    addInsuranceVONew.setInsuredBirthday(employeeFamilyDO.getBirthday());
                                    addList.add(addInsuranceVONew);
                                }
                            } else {
                                addList.add(addInsuranceVO);
                            }
                        }
                    }

                    // 将数据分组并实现行转列
                    List<AddInsuranceExcelVO> dataList = DynamicExcelConvert.INSTANCE.convertListExcel(addList);
                    ExcelSheetDynamicVO<AddInsuranceExcelVO> sheet1 = new ExcelSheetDynamicVO<>();
                    sheet1.setSheetName("加保");
                    sheet1.setDynamic(true);
                    sheet1.setDataList(dataList);
                    excelSheetVOList.add(sheet1);

                    // 更新 雇员采购订单.加保标识="是"
                    List<Long> idList = list1.stream().flatMap(vo -> Arrays.stream(vo.getPurEmpOrderIds().split(",")))
                            .map(Long::parseLong).distinct().collect(Collectors.toList());
                    purchaseReportMapper.updateIsAddInsurance(idList);
                    log.info("businessInsurance===更新加保标识===idList:{}", idList);
                }
                if (CollectionUtils.isNotEmpty(list2)) {
                    log.info("businessInsurance===减保===list2:{}", list2);
                    List<SubInsuranceExcelVO> subList = new ArrayList<>();

                    // 收集类型为子女的雇员id
                    List<Long> childEmpIdList = list2.stream().filter(vo -> EmployeeEnum.EmployeeType.CHILD.getValue().equals(vo.getEmpType()))
                            .map(SubInsuranceExcelVO::getEmpId).distinct().collect(Collectors.toList());
                    Map<Long, List<EmployeeFamilyDO>> employeeFamilyMap = new HashMap<>();
                    if (CollectionUtils.isNotEmpty(childEmpIdList)) {
                        // 根据雇员id+日期,查询02雇员子女信息
                        List<EmployeeFamilyDO> employeeFamilyList = employeeFamilyMapper.selectByEmpId(childEmpIdList);
                        // 雇员id为key,List<EmployeeFamilyDO>为value,将 employeeFamilyList 转为map
                        employeeFamilyMap = employeeFamilyList.stream().collect(Collectors.groupingBy(EmployeeFamilyDO::getEmployeeId));
                    }

                    for (SubInsuranceExcelVO subInsuranceVO : list2) {
                        Long empId = subInsuranceVO.getEmpId();
                        // 如果是雇员保持原有数据,如果是子女需要变成多行
                        if (EmployeeEnum.EmployeeType.EMPLOYEE.getValue().equals(subInsuranceVO.getEmpType())) {
                            subList.add(subInsuranceVO);
                        } else if (EmployeeEnum.EmployeeType.CHILD.getValue().equals(subInsuranceVO.getEmpType())) {
                            // 子女信息
                            List<EmployeeFamilyDO> employeeFamilyDOS = employeeFamilyMap.get(empId);
                            if (CollectionUtils.isNotEmpty(employeeFamilyDOS)) {
                                for (EmployeeFamilyDO employeeFamilyDO : employeeFamilyDOS) {
                                    SubInsuranceExcelVO subInsuranceVONew = EmployeeFamilyConvert.INSTANCE.convertReduceVO(subInsuranceVO);
                                    subInsuranceVONew.setInsuredName(employeeFamilyDO.getFamilyName());
                                    subInsuranceVONew.setInsuredIdType(employeeFamilyDO.getFamilyIdType());
                                    subInsuranceVONew.setInsuredIdNumber(employeeFamilyDO.getFamilyIdNumber());
                                    subList.add(subInsuranceVONew);
                                }
                            }
                        }
                    }


                    // 按 雇员 ID + 商社名称 + 商保类型 + 生效日期年月 去重
                    Map<String, SubInsuranceExcelVO> uniqueMap = new LinkedHashMap<>();
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");

                    for (SubInsuranceExcelVO vo : subList) {
                        String updateMonth = vo.getCreateTime() != null ? vo.getCreateTime().format(formatter) : "UNKNOWN";
                        String uniqueKey = String.format("%s_%s_%s_%s",
                            vo.getEmpId(),
                            vo.getCompanyName() != null ? vo.getCompanyName() : "",
                            vo.getEmpType() != null ? vo.getEmpType() : "",
                            updateMonth
                        );

                        if (!uniqueMap.containsKey(uniqueKey)) {
                            uniqueMap.put(uniqueKey, vo);
                        } else {
                            SubInsuranceExcelVO existingVO = uniqueMap.get(uniqueKey);
                            String existingIds = existingVO.getPurEmpOrderIds();
                            String newIds = vo.getPurEmpOrderIds();
                            if (StrUtil.isNotBlank(newIds) && !existingIds.contains(newIds)) {
                                existingVO.setPurEmpOrderIds(existingIds + "," + newIds);
                            }
                        }
                    }
                    List<SubInsuranceExcelVO> deduplicatedList = new ArrayList<>(uniqueMap.values());
                    log.info("businessInsurance===减保去重===原始条数:{}, 去重后条数:{}", subList.size(), deduplicatedList.size());
                    ExcelSheetDynamicVO<SubInsuranceExcelVO> sheet2 = new ExcelSheetDynamicVO<>();
                    sheet2.setSheetName("减保");
                    sheet2.setDynamic(false);
                    sheet2.setDataList(deduplicatedList);
                    excelSheetVOList.add(sheet2);
                    // 更新 雇员采购订单.减保标识="是"
                    List<Long> idList = deduplicatedList.stream().flatMap(vo -> Arrays.stream(vo.getPurEmpOrderIds().split(",")))
                            .map(Long::parseLong).distinct().collect(Collectors.toList());
                    purchaseReportMapper.updateIsSubInsurance(idList);
                    log.info("businessInsurance===更新减保标识===idList:{}", idList);
                }

                // 生成并上传文件
                this.generateAndUploadReportDynamic(taskInfo, excelSheetVOList,
                        DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN) + "商业保险采购名单导出.xlsx");
            } else {
                // 失败提示
                taskInfo.setTaskErrorInfo("没有符合条件的数据,请检查");
            }
            // 设置任务状态为成功
            taskInfo.setTaskStatus(AsyncTaskStatusEnum.SUCCESS.getStatus());
            // 任务结束时间
            taskInfo.setTaskEndDate(LocalDateTime.now());
            asyncTaskInfoService.updateById(taskInfo);
        } catch (Exception e) {
            // 使用新的事务-修改异步任务数据
            asyncTaskInfoService.updateErrorInfoNewTx(taskInfo, e.getMessage());
            log.error("businessInsurance===商业保险采购名单导出====异常: {}", e.getMessage(), e);
        }
    }

    /**
     * 生成并上传文件
     *
     * @param taskInfo         异步任务信息
     * @param excelSheetVOList 数据列表
     * @param fileName         文件名
     */
    private void generateAndUploadReport(AsyncTaskInfoDO taskInfo,
                                         List<ExcelSheetVO<?>> excelSheetVOList,
                                         String fileName) throws Exception {
        try {
            // 1. 生成文件 单独拆出来一个方法 确保在使用之前 文件流已经写入
            ByteArrayOutputStream outputStream = ExcelUtils.write(excelSheetVOList);

            // 准备上传请求
            UploadReqDTO uploadDTO = new UploadReqDTO();
            uploadDTO.setName(fileName);
            uploadDTO.setPath(fileName);
            uploadDTO.setPublicRead(true);
            uploadDTO.setContent(outputStream.toByteArray());
            // 2. 上传文件
            UploadRespDTO respDTO = fileApi.createFile(uploadDTO);

            // 3.异步任务信息
            taskInfo.setFileName(fileName);
            // taskInfo.setFileId(respDTO.getFileId());
            // taskInfo.setOriginFile(respDTO.getUrl());
            taskInfo.setObjectId(respDTO.getObjectKey());
            taskInfo.setLinkAddress(respDTO.getUrl());

        } catch (Exception e) {
            log.error("generateAndUploadReport===生成并上传文件异常: {}", e.getMessage(), e);
            throw e;
        }
    }

    /**
     * 生成并上传文件
     *
     * @param taskInfo      异步任务信息
     * @param dynamicVOList 数据列表
     * @param fileName      文件名
     */
    private void generateAndUploadReportDynamic(AsyncTaskInfoDO taskInfo,
                                                List<ExcelSheetDynamicVO> dynamicVOList,
                                                String fileName) throws Exception {
        try {
            log.info("generateAndUploadReportDynamic===开始生成并上传文件===dynamicVOList:{}",dynamicVOList);
            // 1. 生成文件 单独拆出来一个方法 确保在使用之前 文件流已经写入
            ByteArrayOutputStream outputStream = ExcelUtils.dynamicWrite(dynamicVOList);

            // 准备上传请求
            UploadReqDTO uploadDTO = new UploadReqDTO();
            uploadDTO.setName(fileName);
            uploadDTO.setPath(fileName);
            uploadDTO.setPublicRead(true);
            uploadDTO.setContent(outputStream.toByteArray());
            // 2. 上传文件
            UploadRespDTO respDTO = fileApi.createFile(uploadDTO);

            // 3.异步任务信息
            taskInfo.setFileName(fileName);
            // taskInfo.setFileId(respDTO.getFileId());
            // taskInfo.setOriginFile(respDTO.getUrl());
            taskInfo.setObjectId(respDTO.getObjectKey());
            taskInfo.setLinkAddress(respDTO.getUrl());

        } catch (Exception e) {
            log.error("generateAndUploadReport===生成并上传文件异常: {}", e.getMessage(), e);
            throw e;
        }
    }

}

导出Mapper文件

java 复制代码
package cn.com.fsg.ihro.report.dao;

import cn.com.fsg.ihro.report.pojo.vo.purchase.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 采购报表
 *
 * @author zkg
 * @since 2025-10-17 15:56
 */

@Mapper
public interface PurchaseReportMapper {

    /**
     * 体检采购订单名单导出-费用合计
     *
     * @param reqVO 入参
     * @return 结果
     */
    List<FeeTotalExcelVO> queryFeeTotal(@Param("reqVO") PurchaseReqVO reqVO);

    /**
     * 体检采购订单名单导出-员工名单
     *
     * @param reqVO 入参
     * @return 结果
     */
    List<EmployeeListExcelVO> queryEmployeeList(@Param("reqVO") PurchaseReqVO reqVO);

    /**
     * 商管采购名单导出-发放名单
     *
     * @param reqVO 入参
     * @return 结果
     */
    List<DistributionListExcelVO> queryDistributionList(@Param("reqVO") PurchaseReqVO reqVO);

    /**
     * 商业保险采购名单导出-加保
     *
     * @param reqVO 入参
     * @return 结果
     */
    List<AddInsuranceExcelVO> queryAddInsurance(@Param("reqVO") PurchaseReqVO reqVO);

    /**
     * 商业保险采购名单导出-减保
     *
     * @param reqVO 入参
     * @return 结果
     */
    List<SubInsuranceExcelVO> queryReduceInsurance(@Param("reqVO") PurchaseReqVO reqVO);

    /**
     *  更新 服务采购订单.是否生成报表标识="是"
     *
     * @param list 入参
     */
    void updateIsGenerateReport(@Param("list") List<Long> list);

    /**
     *  更新 雇员采购订单.加保标识="是"
     *
     * @param list 入参
     */
    void updateIsAddInsurance(@Param("list") List<Long> list);

    /**
     *  更新 雇员采购订单.减保标识="是"
     *
     * @param list 入参
     */
    void updateIsSubInsurance(@Param("list") List<Long> list);
}

导出XML文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.com.fsg.ihro.report.dao.PurchaseReportMapper">

    <!-- 更新 服务采购订单.是否生成报表标识="是" -->
    <update id="updateIsGenerateReport">
        update t_pur_ser_order
        set is_generate_report = 1
        where id in
        <foreach item="item" collection="list" open="(" separator="," close=")">
            #{item}
        </foreach>
    </update>

    <!-- 更新 雇员采购订单.加保标识="是" -->
    <update id="updateIsAddInsurance">
        update t_pur_emp_order
        set is_add_insurance = 1
        where id in
        <foreach item="item" collection="list" open="(" separator="," close=")">
            #{item}
        </foreach>
    </update>

    <!-- 更新 雇员采购订单.减保标识="是" -->
    <update id="updateIsSubInsurance">
        update t_pur_emp_order
        set is_sub_insurance = 1
        where id in
        <foreach item="item" collection="list" open="(" separator="," close=")">
            #{item}
        </foreach>
    </update>

    <!-- 体检采购订单名单导出-费用合计 -->
    <select id="queryFeeTotal" resultType="cn.com.fsg.ihro.report.pojo.vo.purchase.FeeTotalExcelVO">
        select
            st.name orgName,
            tpp.pur_name purName,
            count(distinct tpso.id) quantity,
            sum(tpso.amount) totalAmount,
            tpso.create_time reportStartDate,
            tpso.create_time reportEndDate
        from
            -- 服务采购订单
            t_pur_ser_order tpso
            -- 雇员销售订单
            INNER JOIN t_employee_sales_plan tesp ON tesp.id = tpso.emp_sales_plan_id AND tesp.deleted = 0
            -- 雇员上岗
            INNER JOIN t_employee_duty ted ON ted.id = tesp.emp_duty_id AND ted.deleted = 0
            -- 雇员
            INNER JOIN t_employee_info tei ON tei.emp_id = ted.emp_id AND tei.deleted = 0
            -- 采购方案
            INNER JOIN t_pur_plan tpp ON tpp.id = tpso.pur_id and tpp.deleted = 0
            -- 外包项目
            INNER JOIN t_project_info tpi ON tpi.id = tpso.project_id and tpi.deleted = 0
            -- 机构
            LEFT JOIN system_tenant st ON st.id = tpso.tenant_id AND st.deleted = 0
        where
            tpso.deleted = 0
          and tpp.sup_name = '上海外服门诊部有限公司'
          <include refid="queryConditions"/>
        group by st.name, tpp.pur_name, tpso.create_time
    </select>

    <!-- 查询条件 <include refid="queryConditions"/>  -->
    <sql id="queryConditions">
        and tpso.is_generate_report != 1
        and tpi.status != '9000'
        and tpi.dept_id = #{reqVO.deptId}
        and date_format(tpso.create_time, '%Y-%m-%d') >= #{reqVO.reportDateStart}
        and date_format(tpso.create_time, '%Y-%m-%d') &lt;= #{reqVO.reportDateEnd}
        <if test="reqVO.projectIdList != null and reqVO.projectIdList.size() > 0">
            AND tpi.id in
            <foreach item="item" collection="reqVO.projectIdList" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>
    </sql>

    <!-- 体检采购订单名单导出-员工名单 -->
    <select id="queryEmployeeList" resultType="cn.com.fsg.ihro.report.pojo.vo.purchase.EmployeeListExcelVO">
        select
            tpso.id purSerOrderId,
            tei.emp_name as empName,
            tei.gender as gender,
            '普通员工' as employeeAttribute,
            tei.id_type as idType,
            tei.id_number as idNumber,
            tpp.pur_name as purName,
            '否' as vipPurchase,
            tei.id_number as empIdNumber,
            tei.birthday as birthday,
            tei.mobile_phone as mobilePhone,
            '否' as isForeigner,

            sd.name as deptName,
            tpi.project_name as projectName,
            GROUP_CONCAT(DISTINCT su.nickname SEPARATOR ',') as salesman,
            tpso.create_time as createTime
        from
            -- 服务采购订单
            t_pur_ser_order tpso
            -- 雇员销售订单
            INNER JOIN t_employee_sales_plan tesp ON tesp.id = tpso.emp_sales_plan_id AND tesp.deleted = 0
            -- 雇员上岗
            INNER JOIN t_employee_duty ted ON ted.id = tesp.emp_duty_id AND ted.deleted = 0
            -- 雇员
            INNER JOIN t_employee_info tei ON tei.emp_id = ted.emp_id AND tei.deleted = 0
            -- 采购方案
            INNER JOIN t_pur_plan tpp ON tpp.id = tpso.pur_id and tpp.deleted = 0
            -- 外包项目
            INNER JOIN t_project_info tpi ON tpi.id = tpso.project_id and tpi.deleted = 0
            -- 项目业务人员
            LEFT JOIN t_project_business tpb ON tpb.project_id = tpi.id AND tpb.deleted = 0
            -- 部门表
            LEFT JOIN system_dept sd ON sd.id = tpb.dept_id AND sd.deleted = 0
            -- 用户表
            LEFT JOIN system_users su ON su.id = tpb.user_id AND su.deleted = 0
        where
            tpso.deleted = 0
          and tpp.sup_name = '上海外服门诊部有限公司'
          <include refid="queryConditions"/>
        GROUP BY
            tpso.id, tei.emp_name, tei.gender, tei.id_type, tei.id_number, tpp.pur_name,
            tei.birthday, tei.mobile_phone, sd.name, tpi.project_name, tpso.create_time
    </select>

    <!-- 商管采购名单导出-发放名单 -->
    <select id="queryDistributionList" resultType="cn.com.fsg.ihro.report.pojo.vo.purchase.DistributionListExcelVO">
        select
            tpso.id AS purSerOrderId,
            tesp.emp_duty_id AS empDutyId,
            sd.name AS deptName,
            GROUP_CONCAT(DISTINCT su.nickname SEPARATOR ',') AS salesman,

            tpp.pur_name AS purName,
            tpso.amount AS amount,
            1 AS quantity,
            tei.emp_name AS empName,
            tei.id_number AS idNumber,
            tei.mobile_phone AS mobilePhone,
            DATE_FORMAT(tei.birthday, '%Y-%m') AS birthday,
            tpso.create_time AS createTime
        from
            -- 服务采购订单
            t_pur_ser_order tpso 
            -- 雇员销售订单
            INNER JOIN t_employee_sales_plan tesp ON tesp.id = tpso.emp_sales_plan_id AND tesp.deleted = 0
            -- 雇员上岗
            INNER JOIN t_employee_duty ted ON ted.id = tesp.emp_duty_id AND ted.deleted = 0
            -- 雇员
            INNER JOIN t_employee_info tei ON tei.emp_id = ted.emp_id AND tei.deleted = 0

            -- 采购方案
            INNER JOIN t_pur_plan tpp ON tpp.id = tpso.pur_id and tpp.deleted = 0

            -- 外包项目
            INNER JOIN t_project_info tpi ON tpi.id = tpso.project_id and tpi.deleted = 0
            -- 项目业务人员
            LEFT JOIN t_project_business tpb ON tpb.project_id = tpi.id AND tpb.deleted = 0
            -- 部门表
            LEFT JOIN system_dept sd ON sd.id = tpb.dept_id AND sd.deleted = 0
            -- 用户表
            LEFT JOIN system_users su ON su.id = tpb.user_id AND su.deleted = 0
        where
            tpso.deleted = 0
          and tpp.sup_name = '上海外服商务管理有限公司'
          <include refid="queryConditions"/>
        GROUP BY
        tpso.id, tesp.emp_duty_id, sd.name, tpp.pur_name, tpso.amount,
        tei.emp_name, tei.id_number, tei.mobile_phone, tei.birthday, tpso.create_time
    </select>

    <!-- 商业保险采购名单导出-加保 -->
    <select id="queryAddInsurance" resultType="cn.com.fsg.ihro.report.pojo.vo.purchase.AddInsuranceExcelVO">
        select
            group_concat(tpeo.id) AS purEmpOrderIds,
            tei.emp_id AS empId,
            st.name AS companyName,
            '' AS computerNo,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), '子女', '员工') AS empType,
            tpeo.create_time AS createTime,
            ted.duty_date AS dutyDate,
            ted.layoff_date AS layoffDate,

            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), null, tei.emp_name) AS insuredName,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), null, tei.id_type) AS insuredIdType,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), null, tei.id_number) AS insuredIdNumber,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), null, tei.gender) AS insuredGender,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), null, tei.birthday) AS insuredBirthday,
            '' AS insuredNationality,

            tei.emp_name AS mainInsuredName,
            tei.id_type AS mainInsuredIdType,
            tei.id_number AS mainInsuredIdNumber,
            tei.gender AS mainInsuredGender,
            tei.birthday AS mainInsuredBirthday,
            tei.mobile_phone AS mainInsuredPhone,
            '' AS mainInsuredEmail,

            tpp.pur_name AS purName,
            tpp.plan_desc AS planDesc,
            tpi.project_name AS projectName,
            sd.name AS deptName,
            GROUP_CONCAT(DISTINCT su.nickname SEPARATOR ',') AS salesman
        from
            -- 雇员采购订单
            t_pur_emp_order tpeo
            -- 雇员上岗
            INNER JOIN t_employee_duty ted ON ted.id = tpeo.emp_duty_id AND ted.deleted = 0
            -- 雇员
            INNER JOIN t_employee_info tei ON tei.emp_id = ted.emp_id AND tei.deleted = 0

            -- 采购方案
            INNER JOIN t_pur_plan tpp ON tpp.id = tpeo.pur_id and tpp.deleted = 0
            -- 采购项
            LEFT JOIN t_pur_item tpi2 ON tpi2.id = tpp.pur_item_id and tpi2.deleted = 0

            -- 外包项目
            INNER JOIN t_project_info tpi ON tpi.id = ted.project_id and tpi.deleted = 0
            -- 项目业务人员
            LEFT JOIN t_project_business tpb ON tpb.project_id = tpi.id AND tpb.deleted = 0
            -- 部门表
            LEFT JOIN system_dept sd ON sd.id = tpb.dept_id AND sd.deleted = 0
            -- 用户表
            LEFT JOIN system_users su ON su.id = tpb.user_id AND su.deleted = 0

            -- 机构
            LEFT JOIN system_tenant st ON st.id = tpeo.tenant_id AND st.deleted = 0
        where
            tpeo.deleted = 0
            and tpi.status != '9000'
            and tpp.sup_name = '上海外服(集团)有限公司'
            and tpeo.status = '01'
            and ( tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗') or tpeo.is_add_insurance != 1 )

            and date_format(tpeo.create_time, '%Y-%m-%d') >= #{reqVO.reportDateStart}
            and date_format(tpeo.create_time, '%Y-%m-%d') &lt;= #{reqVO.reportDateEnd}
            <include refid="queryConditionsProject"/>
        GROUP BY tei.emp_id, st.name, tpi2.name, tpeo.create_time, ted.duty_date, ted.layoff_date,
        tei.emp_name, tei.id_type, tei.id_number, tei.gender, tei.birthday, tei.mobile_phone,
        tpp.pur_name, tpp.plan_desc, tpi.project_name, sd.name
    </select>

    <!-- 商业保险采购名单导出-减保 -->
    <select id="queryReduceInsurance" resultType="cn.com.fsg.ihro.report.pojo.vo.purchase.SubInsuranceExcelVO">
        select
            group_concat(tpeo.id) AS purEmpOrderIds,
            tei.emp_id AS empId,
            st.name AS companyName,
            '' AS computerNo,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), '子女', '员工') AS empType,
            tpeo.update_time AS createTime,
            ted.duty_date AS dutyDate,
            ted.layoff_date AS layoffDate,

            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), '', tei.emp_name) AS insuredName,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), '', tei.id_type) AS insuredIdType,
            if(tpi2.name in ('商业保险 - 子女医疗','商业保险 - 多子女医疗'), '', tei.id_number) AS insuredIdNumber,

            tei.emp_name AS mainInsuredName,
            tei.id_type AS mainInsuredIdType,
            tei.id_number AS mainInsuredIdNumber,
            tpi.project_name AS projectName,
            sd.name AS deptName,
            GROUP_CONCAT(DISTINCT su.nickname SEPARATOR ',') AS salesman
        from
            -- 雇员采购订单
            t_pur_emp_order tpeo
            -- 雇员上岗
            INNER JOIN t_employee_duty ted ON ted.id = tpeo.emp_duty_id AND ted.deleted = 0
            -- 雇员
            INNER JOIN t_employee_info tei ON tei.emp_id = ted.emp_id AND tei.deleted = 0

            -- 采购方案
            INNER JOIN t_pur_plan tpp ON tpp.id = tpeo.pur_id and tpp.deleted = 0
            -- 采购项
            LEFT JOIN t_pur_item tpi2 ON tpi2.id = tpp.pur_item_id and tpi2.deleted = 0

            -- 外包项目
            INNER JOIN t_project_info tpi ON tpi.id = ted.project_id and tpi.deleted = 0
            -- 项目业务人员
            LEFT JOIN t_project_business tpb ON tpb.project_id = tpi.id AND tpb.deleted = 0
            -- 部门表
            LEFT JOIN system_dept sd ON sd.id = tpb.dept_id AND sd.deleted = 0
            -- 用户表
            LEFT JOIN system_users su ON su.id = tpb.user_id AND su.deleted = 0

            -- 机构
            LEFT JOIN system_tenant st ON st.id = tpeo.tenant_id AND st.deleted = 0
        where
            tpeo.deleted = 0
          and tpi.status != '9000'
          and tpp.sup_name = '上海外服(集团)有限公司'
          and tpeo.status = '02'
          and tpi2.name in ('商业保险 - 人身意外', '商业保险 - 医疗', '商业保险 - 子女医疗', '商业保险 - 多子女医疗', '商业保险 - 重疾', '商业保险 - 住院医疗', '商业保险 - 家庭财产保障' )
          and tpeo.is_sub_insurance != 1
          and date_format(tpeo.update_time, '%Y-%m-%d') >= #{reqVO.reportDateStart}
          and date_format(tpeo.update_time, '%Y-%m-%d') &lt;= #{reqVO.reportDateEnd}
          <include refid="queryConditionsProject"/>
        GROUP BY tei.emp_id, st.name, tpi2.name, tpeo.update_time, ted.duty_date, ted.layoff_date,
        tei.emp_name, tei.id_type, tei.id_number, tpi.project_name, sd.name
    </select>

    <!-- 查询条件 <include refid="queryConditionsProject"/>  -->
    <sql id="queryConditionsProject">
        <if test="reqVO.projectIdList != null and reqVO.projectIdList.size() > 0">
            AND tpi.id in
            <foreach item="item" collection="reqVO.projectIdList" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>
    </sql>
</mapper>

导出Vo继承动态表头:BasicDynamicExcelVO

java 复制代码
package cn.com.fsg.ihro.report.pojo.vo.purchase;

import cn.com.fsg.plugins.excel.core.annotations.DictFormat;
import cn.com.fsg.plugins.excel.core.convert.DictConvert;
import cn.com.fsg.plugins.excel.core.vo.BasicDynamicExcelVO;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDate;

/**
 * 商业保险采购名单导出-加保
 *
 * @author zkg
 * @since 2025-09-22 15:21
 */
@Data
public class AddInsuranceExcelVO extends BasicDynamicExcelVO {

    @Schema(description = "雇员采购订单id")
    @ExcelIgnore
    private String purEmpOrderIds;

    @Schema(description = "雇员id")
    @ExcelIgnore
    private Long empId;

    @Schema(description = "商社名称")
    @ExcelProperty(value = "商社名称")
    private String companyName;

    @Schema(description = "电脑号:默认空")
    @ExcelProperty(value = "电脑号")
    private String computerNo;

    @Schema(description = "人员类别")
    @ExcelProperty(value = "人员类别")
    private String empType;

    @Schema(description = "生效日期")
    @ExcelProperty(value = "生效日期")
    private LocalDate createTime;

    @Schema(description = "上岗日期")
    @ExcelProperty(value = "上岗日期")
    private LocalDate dutyDate;

    @Schema(description = "下岗日期")
    @ExcelProperty(value = "下岗日期")
    private LocalDate layoffDate;

    @Schema(description = "被保险人姓名")
    @ExcelProperty(value = "被保险人姓名")
    private String insuredName;

    @Schema(description = "被保险人证件类型")
    @ExcelProperty(value = "被保险人证件类型", converter = DictConvert.class)
    @DictFormat("id_type")
    private String insuredIdType;

    @Schema(description = "被保险人证件号码")
    @ExcelProperty(value = "被保险人证件号码")
    private String insuredIdNumber;

    @Schema(description = "被保险人性别")
    @ExcelProperty(value = "被保险人性别", converter = DictConvert.class)
    @DictFormat("system_user_sex")
    private String insuredGender;

    @Schema(description = "被保险人出生日期")
    @ExcelProperty(value = "被保险人出生日期")
    private LocalDate insuredBirthday;

    @Schema(description = "被保险人国籍")
    @ExcelProperty(value = "被保险人国籍")
    private String insuredNationality;

    @Schema(description = "主被保险人姓名")
    @ExcelProperty(value = "主被保险人姓名")
    private String mainInsuredName;

    @Schema(description = "主被保险人证件类型")
    @ExcelProperty(value = "主被保险人证件类型", converter = DictConvert.class)
    @DictFormat("id_type")
    private String mainInsuredIdType;

    @Schema(description = "主被保险人证件号码")
    @ExcelProperty(value = "主被保险人证件号码")
    private String mainInsuredIdNumber;

    @Schema(description = "主被保险人性别")
    @ExcelProperty(value = "主被保险人性别", converter = DictConvert.class)
    @DictFormat("system_user_sex")
    private String mainInsuredGender;

    @Schema(description = "主被保险人出生日期")
    @ExcelProperty(value = "主被保险人出生日期")
    private LocalDate mainInsuredBirthday;

    @Schema(description = "主被保险人手机")
    @ExcelProperty(value = "主被保险人手机")
    private String mainInsuredPhone;

    @Schema(description = "主被保险人邮箱")
    @ExcelProperty(value = "主被保险人邮箱")
    private String mainInsuredEmail;

    @Schema(description = "账户名")
    @ExcelProperty(value = "账户名")
    private String bankAccountName;

    @Schema(description = "账号")
    @ExcelProperty(value = "账号")
    private String bankAccountCode;

    @Schema(description = "开户行")
    @ExcelProperty(value = "开户行", converter = DictConvert.class)
    @DictFormat("salary_bank_code")
    private String bankCode;

    @Schema(description = "小盘方案号:默认空")
    @ExcelProperty(value = "小盘方案号")
    private String planNo;

    @Schema(description = "项目名称")
    @ExcelProperty(value = "项目名称")
    private String projectName;

    @Schema(description = "业务部门")
    @ExcelProperty(value = "业务部门")
    private String deptName;

    @Schema(description = "业务员:多个业务员用,隔开")
    @ExcelProperty(value = "业务员")
    private String salesman;

    // ================动态表头================
    @Schema(description = "采购方案名称")
    @ExcelIgnore
    private String purName;

    @Schema(description = "产品内容:方案说明")
    @ExcelIgnore
    private String planDesc;

}

动态列写入Excel基类VO

java 复制代码
import lombok.Data;

import java.util.Map;

/**
 * <p><b>Description:</b> 动态列写入Excel基类VO
 * <p><b>Company:</b> FSG
 *
 * @author elvira
 * @version V1.0
 * @since 2025/10/31 13:35
 */
@Data
public class BasicDynamicExcelVO {
    /**
     * 动态数据 => key=表头,value=数据
     */
    private Map<String, Object> dynamicData;
}

动态头excelVO

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

/**
 * <p><b>Description:</b> 动态(头-数据)对
 * <p><b>Company:</b> FSG
 *
 * @author elvira
 * @version V1.0
 * @since 2025/10/31 14:54
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelDynamicPair {
    /**
     * 动态头
     */
    private List<List<String>> heads;

    /**
     * 动态数据
     */
    private List<Map<Integer, Object>> data;
}

动态excelVO

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * <p><b>Description:</b> 多Sheet动态列写入VO对象
 * <p><b>Company:</b> FSG
 *
 * @author elvira
 * @version V1.0
 * @since 2025/10/28 14:52
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelSheetDynamicVO<T extends BasicDynamicExcelVO> {
    /**
     * sheet 名
     */
    private String sheetName;

    /**
     * 是否动态列
     */
    private boolean isDynamic;

    /**
     * 动态数据列表
     */
    private List<T> dataList;
}

导出工具类

java 复制代码
import cn.com.ihro.common.pojo.R;
import cn.com.ihro.common.util.json.JsonUtils;
import cn.com.ihro.plugins.excel.core.vo.BasicDynamicExcelVO;
import cn.com.ihro.plugins.excel.core.vo.ExcelDynamicPair;
import cn.com.ihro.plugins.excel.core.vo.ExcelSheetDynamicVO;
import cn.com.ihro.plugins.excel.core.vo.ExcelSheetVO;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static cn.com.ihro.common.exception.enums.GlobalErrorCodeConstants.DOWNLOAD_ERR;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE;

/**
 * Excel 工具类
 */
@Slf4j
public final class ExcelUtils {

    private ExcelUtils() {
        // 私有构造函数
    }

    /**
     * 将列表以 Excel 响应给前端(多个 Sheet)
     *
     * @param filename    文件名
     * @param excelSheets Sheet 页签数据
     * @param response    响应
     */
    public static void write(String filename, List<ExcelSheetVO<?>> excelSheets, HttpServletResponse response) {
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).autoCloseStream(false)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build()) {

            Assert.noNullElements(excelSheets, "Sheet data must not be null or empty!");

            ExcelSheetVO<?> sheetVO;
            WriteSheet writeSheet;
            for (int i = 0; i < excelSheets.size(); i++) {
                sheetVO = excelSheets.get(i);
                writeSheet = EasyExcel.writerSheet(i, sheetVO.getSheetName()).head(sheetVO.getHeadClass()).build();
                excelWriter.write(sheetVO.getData(), writeSheet);
            }

            // 设置 Excel 下载的响应头
            settingExcelHead(filename, response);
        } catch (Exception e) {
            resetResponse(response, e);
        }
    }

    public static <T> void write(String filename, ExcelSheetVO<T> excelSheet, HttpServletResponse response) {
        write(response, filename, excelSheet.getSheetName(), excelSheet.getHeadClass(), excelSheet.getData());
    }

    /**
     * 将列表以 Excel 响应给前端(单个 Sheet)
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) {
        try {
            Assert.hasText(filename, "filename must not be null or empty!");

            EasyExcel.write(response.getOutputStream(), head)
                    // 流交给 Servlet 处理
                    .autoCloseStream(false)
                    // column 长度自动适配,最大 255 宽度
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .sheet(sheetName).doWrite(data);

            // 设置 Excel 下载的响应头
            settingExcelHead(filename, response);
        } catch (Exception e) {
            resetResponse(response, e);
        }
    }

    /**
     * 动态列写入
     *
     * @param filename 文件名
     * @param heads    Excel head 头
     * @param data     数据列表
     * @param response 响应
     */
    public static <T> void dynamicWrite(String filename, List<List<String>> heads, List<List<T>> data,
                                        HttpServletResponse response) {
        try {
            EasyExcel.write(response.getOutputStream())
                    .head(heads)
                    .autoCloseStream(false)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .sheet("数据").doWrite(data);

            // 设置 Excel 下载的响应头
            settingExcelHead(filename, response);
        } catch (Exception e) {
            resetResponse(response, e);
        }
    }

    /**
     * 将固定列的多sheet数据写入到输出流
     *
     * @param excelSheetVOList 数据列表
     */
    public static ByteArrayOutputStream write(List<ExcelSheetVO<?>> excelSheetVOList) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
             ExcelWriter excelWriter = EasyExcel.write(baos).autoCloseStream(false)
                     .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build()) {

            WriteSheet writeSheet;
            ExcelSheetVO<?> sheetVO;
            for (int i = 0; i < excelSheetVOList.size(); i++) {
                sheetVO = excelSheetVOList.get(i);
                writeSheet = EasyExcel.writerSheet(i, sheetVO.getSheetName())
                        .head(sheetVO.getHeadClass())
                        .build();
                excelWriter.write(sheetVO.getData(), writeSheet);
            }
            return baos;
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * 将动态列的多sheet数据写入到输出流
     *
     * @param excelSheetVOList 数据列表
     */
    public static ByteArrayOutputStream dynamicWrite(List<ExcelSheetDynamicVO> excelSheetVOList) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
        // 动态写入
        dynamicWrite(excelSheetVOList, baos);
        return baos;
    }

    /**
     * 将动态列的多sheet数据写入到 HttpServletResponse
     *
     * @param excelSheetVOList 数据列表
     */
    public static void dynamicWrite(String filename, List<ExcelSheetDynamicVO> excelSheetVOList, HttpServletResponse response) {
        try {
            // 动态写入
            dynamicWrite(excelSheetVOList, response.getOutputStream());
            settingExcelHead(filename, response);
        } catch (Exception e) {
            resetResponse(response, e);
        }
    }

    public static void dynamicWrite(List<ExcelSheetDynamicVO> excelSheetList, OutputStream os) {

        Assert.notEmpty(excelSheetList, "Dynamic Sheet VO must not be null or empty!");

        try (ExcelWriter excelWriter = EasyExcel.write(os).autoCloseStream(false)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build()) {

            ExcelSheetDynamicVO sheetVO;
            WriteSheet writeSheet;
            ExcelDynamicPair excelDynamicPair;
            for (int i = 0; i < excelSheetList.size(); i++) {
                sheetVO = excelSheetList.get(i);

                List<? extends BasicDynamicExcelVO> dataList = sheetVO.getDataList();

                Assert.notEmpty(dataList, "Sheet data must not be null or empty!");

                excelDynamicPair = buildHeadAndData(dataList, sheetVO.isDynamic());

                writeSheet = EasyExcel.writerSheet(i, sheetVO.getSheetName())
                        .head(excelDynamicPair.getHeads())
                        .build();
                excelWriter.write(excelDynamicPair.getData(), writeSheet);
            }
        }
    }

    /**
     * 构建表头:固定表头+动态表头
     * 构建动态数据:固定数据+动态数据
     *
     * @param dataList
     * @param dynamic
     * @return
     */
    private static ExcelDynamicPair buildHeadAndData(List<? extends BasicDynamicExcelVO> dataList, boolean dynamic) {
        BasicDynamicExcelVO firstVO = dataList.get(0);
        Class<? extends BasicDynamicExcelVO> dynExcelClass = firstVO.getClass();

        // 获取所有带 ExcelProperty 注解的字段
        List<Field> allFields = new ArrayList<>();
        ReflectionUtils.doWithFields(dynExcelClass,
                allFields::add,
                field -> field.isAnnotationPresent(ExcelProperty.class));

        // 构建固定列头
        List<List<String>> heads = allFields.stream()
                .map(field -> Arrays.asList(field.getAnnotation(ExcelProperty.class).value()))
                .collect(Collectors.toList());

        if (dynamic) {
            // 提取动态表头
            Set<String> dynamicHeaders = extractDynamicMap(firstVO).keySet();
            // 构建动态列头
            dynamicHeaders.stream()
                    .map(Collections::singletonList)
                    .forEach(heads::add);
        }

        // 构建数据
        List<Map<Integer, Object>> excelData = dataList.stream().map(vo -> {
            Map<Integer, Object> rowMap = new LinkedHashMap<>();
            int index = 0;
            AtomicInteger indexHolder = new AtomicInteger(0);

            // 固定字段
            for (Field field : allFields) {
                field.setAccessible(true);
                try {
                    rowMap.put(indexHolder.getAndIncrement(), field.get(vo));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("Excel实体类中的字段读取失败:" + field.getName(), e);
                }
            }

            if (dynamic) {
                // 动态字段
                extractDynamicMap(vo).values().forEach(value -> rowMap.put(indexHolder.getAndIncrement(), value));
            }

            return rowMap;
        }).collect(Collectors.toList());

        return new ExcelDynamicPair(heads, excelData);
    }

    /**
     * 提取 VO 中的动态Map
     *
     * @param dynamicExcelVO 数据对象
     */
    private static Map<String, Object> extractDynamicMap(BasicDynamicExcelVO dynamicExcelVO) {
        Map<String, Object> dynamicMap = Maps.newHashMap();

        Class<? extends BasicDynamicExcelVO> dynExcelClass = dynamicExcelVO.getClass();
        try {
            Field dynamicDataField = ReflectionUtils.findField(dynExcelClass, "dynamicData");
            dynamicDataField.setAccessible(true);

            dynamicMap = (Map<String, Object>) dynamicDataField.get(dynamicExcelVO);
        } catch (Exception ignored) {
            log.info("Dynamic data field not found in class: " + dynExcelClass.getName());
        }

        return dynamicMap;
    }

    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                // 不要自动关闭,交给 Servlet 自己处理
                .autoCloseStream(false)
                .doReadAllSync();
    }

    @SneakyThrows
    private static void settingExcelHead(String filename, HttpServletResponse response) {
        // 最后设置是防止报错时,响应的 contentType 已被修改,影响到响应错误 JSON 输出
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    @SneakyThrows
    private static void resetResponse(HttpServletResponse response, Exception e) {
        log.error("Excel 文件下载失败,{}", e.getMessage(), e);

        // 重置 Response(如果不重置,默认失败了会返回一个有部分数据的Excel)
        response.reset();
        response.setContentType(APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().println(JsonUtils.toJsonString(R.error(DOWNLOAD_ERR)));
    }
}
相关推荐
像我这样帅的人丶你还1 天前
Java 后端详解(四):分页与搜索
java·javascript·后端
她的男孩1 天前
数据权限为什么不能只靠注解?Forge 的 Mapper 层 SQL 改写源码拆解
java·后端·架构
tntxia1 天前
Mybatis的日志输入
java
亦暖筑序1 天前
Java 8老系统Browser Agent实战:三层拦截把AI操作后台变成可审计流程
java·后端·设计模式
用户298698530141 天前
Java 实现 Word 文档加密与权限解除
java·后端
Yeats_Liao1 天前
14:Servlet中的页面跳转-Java Web
java·后端·架构
未秃头的程序猿1 天前
告别"if-else地狱"!Java 21模式匹配,代码优雅了10倍
java·后端·面试
鹤望兰6751 天前
字节跳动国际支付-后端开发-三面面经
java
Flittly1 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
RainCity1 天前
Java Swing 自定义组件库分享(十二)
java·笔记·后端