动态表头实现
controller接口
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
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实现
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
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实现
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文件
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 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') <= #{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') <= #{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') <= #{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
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
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
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
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;
}
导出工具类
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)));
}
}