积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思绪(优化前一万多导出失败,优化后支持百万级跨库表导出,已开源)
若条件允许建议使用数仓来做,性能会好很多很多,查询一秒内,下载百万明细数据几十秒,参考文档
数据仓库技能选型方案文档
Flink CDC MySQL数据同步到Doris表同步配置生成工具类
新版报表系统(明细报表、看板、数据大屏)方案&介绍
反馈官方进度
跟官方反馈这个方案后,官方直接优化了但是把本来这个免费的大数据导出做成了收费功能!绝了,我直接把我方案开源
反馈后看到后续的版本里有说升级了这个,于是我升级了包测试,直接这个功能必要企业版才给用了,寄,不如直接用下载中央模式得了,有空把这个下载中央的代码抽出来封装好开源放github 0
下载中央这个本领抽出来脱敏成开源项目已预备的差不多了,预备好后更新文章,帮忙点star支持开源
地址:https://github.com/HumorChen/JimuExportDataExtension
优化结果
原积木导出有两种导出,直接导出和大数据导出(大数据导出是做了优化去掉了一些样式之类的,性能更好)
实测中发现
原积木大数据导出性能:1万条数据导出耗时30秒,1.5万条耗时1.5分钟导出失败,数据超过一万条后经常导出失败,还会导致容器实例探活失败/内存撑爆重启
异步+自实现导出功能优化方案导出性能:48万条数据一次导出耗时2.5分钟,导出时快速返回空数据文件,制止页面死等不可用,导出后的表格是在线OSS的文件URL直接每个人都可以快速下载,可随时多次下载。
需求背景息争决方案的思考
具体讲了需求来由、解决历程的思考心路历程,有点长,想学习解决思绪的可认真阅读,只必要解决方案的可以不看。我这正是由于当前项目组还没有专门做数据仓库的人,先用的积木如许的报表来做的。数据量大之后性能看着很心累,数据量大建议招专业的做数仓的。
项目组的数据报表、导出最开始的技能职员选型的是开源的积木报表(积木报表GitHub地址(5.4K star)),后续积木闭源了。在后续数据量增加后,积木报表经常性导出失败,运营同砚反馈多次,于是优化报表的重担交给了我。
一期我先优化了SQL、涉及表的索引,取消走SQL join 一二十张表的方案,改为API代码分页实现查询。上线后结果仍旧不行,于是继续深入分析性能瓶颈,观察链路跟踪后发现,在导出时内存和CPU都飙升,甚至内存会被拉爆。
我看了下实际SQL执行时间其实相对于整个导出过程并没有很长,在500ms左右一个分页(5000条一个页,淘汰页到1000,2000未发现明显性能提拔),在整个导出操作流程中,我通过抓包和链路跟踪查看,链路是导出按钮调用了积木的一个exportAllExcelStream接口,然后积木内部代码又去调用我配置的表数据源接口地址,将分页和查询条件拿去调我们项目接口获取数据,并导出为excel写入导出哀求的响应流里,那既然我们获取数据的接口耗时占整个导出操作的占比低,其实性能瓶颈在积木框架内部。
于是我打开积木的代码一看,好家伙,都是abcdefg如许的命名,做过代码肴杂加密的就一眼看得出,变量名被重命名肴杂过了,甚至连积木导出接口的字符串都搜刮不到没法找到入口,看来接口字符串都做了隐藏,于是我怀疑是excel导出大量数据有性能问题,我看了下积木依赖的pom文件里引入的是org.apache.poi,我去百度搜刮了相干文章后,看到了其他博主说Apache POI导出有性能问题,性能低下,占用过多的内存,数据量大的时候特别明显,保举使用阿里巴巴的EasyExcel来导出表格,只有十分之一的消耗。
于是网络完情况后,现在摆在面前的两个痛点,第一个是导出慢会慢到网关直接504返回超时(一个接口调用几分钟以上),第二个是积木报表自身用了POI导出很慢,和领导讨论了一下,超时问题可以走异步实现解决,写一个导出下载中央,导出操作接口快速返回一个空的(注解+AOP切面实现拦截),然后异步去再次调用积木导出接口,并设置上哀求头,方便拦截接口导出的切面识别到这个是异步执行,就返回实际数据不要返回空数据,如许就不会接口超时了,这个方案我先实现了下确实可行。
第二个问题就是导出慢的解决,由于闭源还有代码肴杂加密操作,无法二开代码,因此要想解决POI这个问题你就必要本身实现导出,第一个想到的操作是直接在积木报表的页面加个快速导出按钮,可页面代码是积木本身生成的,不方便植入,第二个思绪是,识别积木的导出操作,由于积木导出的时候还是要来我们接口这获取数据的,获取的时候带了分页信息,我们根据每页数据和配置的积木导出时每页数据条数这个配置对比就能识别到是不是导出操作,平常查看是10条每页,导出的时候会变5000条一页,很好区分,但区分完了,怎么实现导出操作呢?报表都是在积木的报表设计里用鼠标点点点的UI操作设计的,你本身接口怎么知道表头、数据源、表名、哪个账户导出的?
带着这些问题我深入查看了积木的数据库表,你积木能读取我就能读取,我看看你到底把这些关键信息存储在哪里了,翻开数据表里可以看到的,确实把表格的字段、宽度、样式、表格ID之类的信息都在Mysql里存着呢,那我只必要写一个解析这些数据的服务就可以像积木一样导出了。
方案思绪可行性得到确认,那我就开始将复杂使命流程关键问题拆解并解决:
第一个是积木点击导出时的参数信息要完备拿到并生存到我们本身设计的导出使命里
(走积木自定义header预留的钩子实现透传哀求数据)
第二个是要能正确读取我们人工在积木报表里配置的数据源API、表头名、表头字段等信息
(自行配置一个示范表并看数据库数据怎么存的,写代码解析这个数据结构并返回我们标准化的解析方法)
第三个是要自行实现调用数据源AP时积木的那次导出操作被拦截返回空数据,我们本身的导出正常导出数据
(新加注解,拦截注解的API,识别是否为积木导出操作,是则拦截,生成导出使命,返回空数据集合,否则直接执行原接口逻辑返回正常数据)
第四个是正确根据积木里的表头字段等信息使用EasyExcel生成表格并将表格上传到OSS,中间不建议产生呆板上的物理文件(使用二进制流生存)
(写一个指定接口分页导出工具,按分页不断调用数据获取接口得到元数据,利用前面得到的表头字段举行解析生成EasyExcel导出数据时必要的结构体,末了使用EasyExcel将数据导出为excel文件的二进制流)
解决方案
在数据获取的api接口上加注解标识这个导出走下载中央方案,若要恢复原样,则注释这个注解即可
流程形貌:
识别并拦截积木导出操作,透传导出时参数生成本身设计的导出使命,异步执行导出使命(自行实现积木导出的逻辑),并使用高性能的阿里巴巴开源的EasyExcel工具,末了将EasyExcel导出的表格文件上传到OSS变为表格URL(记得做安全防护,制止表格走漏)
积木的逻辑
积木前端导出按钮->积木导出接口->分页调取你的数据接口拉数据->生成excel返回
关键代码
大部分代码是不必要你改的,你可以根据本身项目连合使用,比方OSS上传的地方改为你本身的。后面的代码有点长~必要的人依次把代码弄进本身项目。
引入easy excel
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>easyexcel-core</artifactId>
- <version>3.1.1</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>easyexcel</artifactId>
- <version>3.1.1</version>
- </dependency>
复制代码 新建数据库表
- CREATE TABLE `t_download_task` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
- `account` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提交任务的账号',
- `title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '下载任务标题',
- `icon` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标',
- `url` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件URL',
- `file_size` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件大小',
- `percent` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '进度(例如50%)',
- `state` tinyint(4) DEFAULT '0' COMMENT '任务状态(0 等待执行,1执行中,2执行成功,3执行失败)',
- `error` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '执行报错信息(有则填)',
- `json` varchar(4096) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '{}' COMMENT '预留的json扩展字段',
- `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (`id`),
- KEY `idx_account_create_time` (`account`,`create_time`),
- KEY `idx_create_time` (`create_time`)
- ) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='下载中心的任务';
复制代码 下载使命
使用mabatis plus code generator生成service、mapper等文件
识别所需注解、基类
数据获取api所调用服务上方法上标注的注解
- /**
- * @author: humorchen
- * date: 2024/1/15
- * description: 该报表接口使用下载任务中心代理掉,完成下载任务
- * 使用要求:
- * 参数中需要有一个参数是DownloadCenterBaseParam的子类,方法返回值类型需要是支持泛型的JimuPageDto类,方法上加注@UseDownloadTaskCenter注解
- * 参考cn.sffix.recovery.report.service.impl.ReportServiceImpl#dashboardNewVersion(cn.sffix.recovery.report.entity.dto.DashBoardQueryDto)
- **/
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.METHOD})
- @Documented
- public @interface UseDownloadTaskCenter {
- }
复制代码 示范给数据接口调用的服务加注解
- @Override
- @UseDownloadTaskCenter
- public JimuPageDto<DashBoardDataDto> dashboardNewVersion(DashBoardQueryDto dashBoardQueryDto) {
- // 你的数据接口逻辑
- }
复制代码 参数基类
- /**
- * @author: humorchen
- * date: 2024/1/15
- * description:
- **/
- @Data
- public class DownloadCenterBaseParam {
- /**
- * 分页数据页号和页大小
- */
- private Integer pageNo;
- /**
- * 分页数据页号和页大小
- */
- private Integer pageSize;
- }
复制代码 结果基类
- /**
- * @author: humorchen
- * date: 2023/12/19
- * description:
- **/
- @Data
- @FieldNameConstants
- @Accessors(chain = true)
- public class JimuPageDto<T> {
- /**
- * 数据
- */
- private List<T> data;
- /**
- * 积木的count是总数据条数,不是当前页多少条!!!
- */
- private long count;
- /**
- * 积木的total是总页数,不是总数据条数!!!
- */
- private long total;
- public static final JimuPageDto EMPTY = new JimuPageDto().setData(Collections.emptyList()).setTotal(0).setCount(0);
- }
复制代码- /**
- * @author: humorchen
- * date: 2024/1/5
- * description: 下载任务状态
- **/
- @Getter
- public enum DownloadTaskStateEnum {
- WAIT(0, "等待执行"),
- RUNNING(1, "执行中"),
- SUCCESS(2, "执行成功"),
- FAILED(3, "执行失败"),
- ;
- private final int state;
- private final String title;
- DownloadTaskStateEnum(int state, String title) {
- this.state = state;
- this.title = title;
- }
- /**
- * 根据状态获取枚举
- *
- * @param state
- * @return
- */
- public static DownloadTaskStateEnum of(int state) {
- for (DownloadTaskStateEnum value : values()) {
- if (value.state == state) {
- return value;
- }
- }
- return null;
- }
- }
复制代码 下载中央服务(报表下载的导出使命)
实现类
- /**
- * <p>
- * 下载任务 服务实现类
- * </p>
- *
- * @author humorchen
- * @since 2024-01-05
- */
- @Service
- @Slf4j
- public class DownloadTaskServiceImpl extends ServiceImpl<DownloadTaskMapper, DownloadTask> implements IDownloadTaskService {
- @Autowired
- private DownloadTaskMapper downloadTaskMapper;
- @Autowired
- private IReportDataGetService reportDataGetService;
- @Autowired
- private RedisTemplate<String, String> redisTemplate;
- /**
- * 注入spring 事件发布器
- */
- @Autowired
- private ApplicationEventPublisher eventPublisher;
- /**
- * 注册任务
- *
- * @param downloadTask
- * @return
- */
- @Override
- public DownloadTask registerTask(@NonNull DownloadTask downloadTask) {
- downloadTaskMapper.insert(downloadTask);
- return downloadTask;
- }
- /**
- * 10秒内是否有相同任务未完成,不给再次注册下载任务
- *
- * @param account
- * @param requestBody
- * @return
- */
- @Override
- public boolean setSameTaskLock(String account, String requestBody) {
- DownloadTaskSubmitLimitCacheKey limitCacheKey = new DownloadTaskSubmitLimitCacheKey(account, MD5.create().digestHex(requestBody));
- Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(limitCacheKey.getKey(), DateUtil.now(), limitCacheKey.getExpire(), limitCacheKey.getTimeUnit());
- return Boolean.TRUE.equals(setIfAbsent);
- }
- /**
- * 更新任务
- *
- * @param downloadTask
- * @return
- */
- @Override
- public int updateTaskById(@NonNull DownloadTask downloadTask) {
- return downloadTaskMapper.updateById(downloadTask);
- }
- /**
- * 更新任务进度
- *
- * @param id
- * @param percent
- * @return
- */
- @Override
- public int changeTaskPercent(int id, @NonNull String percent) {
- UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
- updateWrapper.eq(DownloadTask.ID, id);
- updateWrapper.set(DownloadTask.PERCENT, percent);
- log.info("【下载中心】更新任务进度 id:{} percent:{}", id, percent);
- return downloadTaskMapper.update(null, updateWrapper);
- }
- /**
- * 更新任务状态
- *
- * @param id
- * @param state
- * @return
- */
- @Override
- public int changeTaskState(int id, @NonNull DownloadTaskStateEnum state) {
- UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
- updateWrapper.eq(DownloadTask.ID, id);
- updateWrapper.set(DownloadTask.STATE, state.getState());
- return downloadTaskMapper.update(null, updateWrapper);
- }
- /**
- * 更新任务状态
- *
- * @param id
- * @param expectState
- * @param targetState
- * @return
- */
- @Override
- public int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState) {
- UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
- updateWrapper.eq(DownloadTask.ID, id);
- updateWrapper.eq(DownloadTask.STATE, expectState.getState());
- updateWrapper.set(DownloadTask.STATE, targetState.getState());
- return downloadTaskMapper.update(null, updateWrapper);
- }
- /**
- * 根据任务ID获取任务
- *
- * @param id
- * @return
- */
- @Override
- public DownloadTask getDownloadTaskById(int id) {
- return downloadTaskMapper.selectById(id);
- }
- /**
- * 查下载任务
- *
- * @param pageListDownloadTaskDto
- * @return
- */
- @Override
- public IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
- Integer id = pageListDownloadTaskDto.getId();
- String startTime = pageListDownloadTaskDto.getStartTime();
- String endTime = pageListDownloadTaskDto.getEndTime();
- String fileName = pageListDownloadTaskDto.getFileName();
- Integer taskState = pageListDownloadTaskDto.getTaskState();
- UserInfo userInfo = UserInfoHolder.get();
- String account = userInfo.getAccount();
- int pageNo = Optional.ofNullable(pageListDownloadTaskDto.getPageNo()).orElse(1);
- int pageSize = Optional.ofNullable(pageListDownloadTaskDto.getPageSize()).orElse(10);
- QueryWrapper<DownloadTask> queryWrapper = new QueryWrapper<>();
- queryWrapper.eq(DownloadTask.ACCOUNT, account);
- queryWrapper.eq(id != null, DownloadTask.ID, id);
- queryWrapper.between(startTime != null && endTime != null, DownloadTask.CREATE_TIME, startTime, endTime);
- queryWrapper.like(StrUtil.isNotBlank(fileName), DownloadTask.TITLE, "%" + fileName + "%");
- queryWrapper.eq(taskState != null, DownloadTask.STATE, taskState);
- // 最新的在前
- queryWrapper.orderByDesc(DownloadTask.CREATE_TIME);
- return page(new Page<>(pageNo, pageSize), queryWrapper);
- }
- /**
- * 重新执行下载任务
- *
- * @param taskId
- * @return
- */
- @Override
- public Result<String> rerunTask(Integer taskId) {
- DownloadTask downloadTask = getDownloadTaskById(taskId);
- if (downloadTask == null) {
- return Result.fail("未找到该任务,请刷新后重试");
- }
- if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
- return Result.fail("任务正在执行中,请稍后重试");
- }
- eventPublisher.publishEvent(new DownloadTaskPublishEvent(taskId));
- return Result.ok("重新执行中");
- }
- /**
- * 根据报表ID获取报表名称
- *
- * @param reportId
- * @return
- */
- @Override
- public String getReportNameByReportId(String reportId) {
- if (StrUtil.isBlank(reportId)) {
- return "";
- }
- String sql = "select name from report.jimu_report where id = '" + reportId + "'";
- JSONObject jsonObject = reportDataGetService.getOne(sql);
- return Optional.ofNullable(jsonObject.getString("name")).orElse("");
- }
- /**
- * 从请求体中获取报表ID
- *
- * @param requestBody
- * @return
- */
- @Override
- public String getReportIdFromRequestBody(String requestBody) {
- if (StrUtil.isNotBlank(requestBody)) {
- JSONObject jsonObject = JSONObject.parseObject(requestBody);
- return jsonObject.getString("excelConfigId");
- }
- return null;
- }
- /**
- * 根据报表ID获取报表API地址或者SQL
- *
- * @param reportId
- * @return
- */
- @Override
- public JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId) {
- JimuReportDataSourceDTO jimuReportDataSourceDTO = new JimuReportDataSourceDTO();
- if (StrUtil.isNotBlank(reportId)) {
- String sql = "select db_dyn_sql,api_url from report.jimu_report_db where jimu_report_id = '" + reportId + "'";
- JSONObject jsonObject = reportDataGetService.getOne(sql);
- jimuReportDataSourceDTO.setSql(jsonObject.getString("db_dyn_sql"));
- jimuReportDataSourceDTO.setApiUrl(jsonObject.getString("api_url"));
- }
- List<List<String>> head = new ArrayList<>();
- EasyExcel.write(new OutputStream() {
- @Override
- public void write(int b) throws IOException {
- }
- }).head(head).sheet("sheet").doWrite(new ArrayList<>());
- return jimuReportDataSourceDTO;
- }
- /**
- * 获取积木报表的头
- *
- * @param reportId
- * @return
- */
- @Override
- public List<JimuReportDataColumnDTO> getReportHead(String reportId) {
- if (StrUtil.isBlank(reportId)) {
- return Collections.emptyList();
- }
- String sql = "select json_str from report.jimu_report where id = '" + reportId + "'";
- JSONObject jsonObject = reportDataGetService.getOne(sql);
- String jsonStr = jsonObject.getString("json_str");
- JSONObject json = JSONObject.parseObject(jsonStr);
- JSONObject rows = json.getJSONObject("rows");
- JSONObject rows0Cells = rows.getJSONObject("0").getJSONObject("cells");
- JSONObject rows1Cells = rows.getJSONObject("1").getJSONObject("cells");
- Set<String> rows0KeySets = rows0Cells.keySet();
- List<JimuReportDataColumnDTO> heads = rows0KeySets.stream().map(key -> {
- JSONObject keyObject = rows0Cells.getJSONObject(key);
- JSONObject columnObject = rows1Cells.getJSONObject(key);
- if (keyObject == null || columnObject == null) {
- return null;
- }
- String name = keyObject.getString("text");
- String column = columnObject.getString("text");
- if (StrUtil.isBlank(name) || StrUtil.isBlank(column)) {
- return null;
- }
- // 处理 #{vpjcgifyua.orderId}
- int indexOf = column.lastIndexOf(".");
- int indexOf2 = column.lastIndexOf("}");
- if (column.startsWith("#") && indexOf >= 0 && indexOf2 >= 0) {
- column = column.substring(indexOf + 1, indexOf2);
- if (StrUtil.isBlank(column)) {
- return null;
- }
- }
- JimuReportDataColumnDTO jimuReportDataColumnDTO = new JimuReportDataColumnDTO();
- jimuReportDataColumnDTO.setName(name);
- jimuReportDataColumnDTO.setColumn(column);
- jimuReportDataColumnDTO.setIndex(Integer.parseInt(key));
- return jimuReportDataColumnDTO;
- }).filter(Objects::nonNull).sorted(Comparator.comparing(JimuReportDataColumnDTO::getIndex)).collect(Collectors.toList());
- log.info("【下载中心】获取积木报表的头 reportId:{},heads:{}", reportId, heads);
- return heads;
- }
- /**
- * 从积木请求体中获取请求参数
- *
- * @param json
- * @return
- */
- @Override
- public String getRequestParamFromJson(String json) {
- if (StrUtil.isNotBlank(json)) {
- JSONObject jsonObject = JSONObject.parseObject(json);
- if (jsonObject.containsKey("param")) {
- return jsonObject.getJSONObject("param").toJSONString();
- }
- return "{}";
- }
- return "{}";
- }
- }
复制代码 数据跨库获取工具
服务接口
- /**
- * @author: humorchen
- * date: 2023/12/19
- * description: 获取数据服务,直接SQL跨库拿数据
- **/
- @DS("slave_1")
- public interface IReportDataGetService<T> {
- /**
- * 执行SQL返回数据
- *
- * @param sql
- * @return
- */
- JSONObject getOne(String sql);
- /**
- * 执行SQL返回数据,数据封装到类cls对象里
- *
- * @param sql
- * @param cls
- * @return
- */
- T getOne(String sql, Class<T> cls);
- /**
- * 执行SQL返回数据
- *
- * @param sql
- * @return
- */
- JSONArray getList(String sql);
- /**
- * 执行SQL返回数据,数据封装到类cls对象里
- *
- * @param sql
- * @param cls
- * @return
- */
- List<T> getList(String sql, Class<T> cls);
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @return
- */
- JSONArray pageGetList(String sql, int page, int pageSize);
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @return
- */
- JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize);
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @param cls
- * @return
- */
- JimuPageDto<T> pageGetListForJimu(String sql, int page, int pageSize, Class<T> cls);
- /**
- * 计数
- *
- * @param sql
- * @return
- */
- long count(String sql);
- /**
- * 生成in语句
- *
- * @param columnName
- * @param elements
- * @return string
- */
- default String getColumnInSql(String columnName, List<String> elements) {
- StringBuilder sqlBuilder = new StringBuilder();
- sqlBuilder.append(" ");
- sqlBuilder.append(columnName);
- sqlBuilder.append(" in (");
- for (int i = 0; i < elements.size(); i++) {
- String id = elements.get(i);
- if (i > 0) {
- sqlBuilder.append(",");
- }
- sqlBuilder.append("'");
- sqlBuilder.append(id);
- sqlBuilder.append("'");
- }
- sqlBuilder.append(")");
- return sqlBuilder.toString();
- }
- }
复制代码 服务实现
- /**
- * @author: chenfuxing
- * date: 2023/12/19
- * description:
- **/
- @Service
- @Slf4j
- public class ReportDataGetServiceImpl implements IReportDataGetService {
- @Autowired
- private DataSource dataSource;
- /**
- * 执行SQL返回数据
- *
- * @param sql
- * @return
- */
- @Override
- public JSONObject getOne(String sql) {
- JSONObject ret = null;
- Connection connection = null;
- Statement statement = null;
- ResultSet resultSet = null;
- try {
- connection = dataSource.getConnection();
- statement = connection.createStatement();
- logSql(sql);
- resultSet = statement.executeQuery(sql);
- if (resultSet != null) {
- while (resultSet.next()) {
- if (ret != null) {
- throw new RuntimeException("查询结果不止一条数据");
- }
- ret = new JSONObject();
- ResultSetMetaData metaData = resultSet.getMetaData();
- int columnCount = metaData.getColumnCount();
- for (int i = 1; i <= columnCount; i++) {
- String columnName = metaData.getColumnLabel(i);
- ret.put(columnName, resultSet.getObject(columnName));
- }
- }
- }
- } catch (Exception e) {
- log.error("获取数据报错", e);
- } finally {
- // 释放资源
- IoUtil.close(resultSet);
- IoUtil.close(statement);
- IoUtil.close(connection);
- }
- return ret;
- }
- /**
- * 执行SQL返回数据
- *
- * @param sql
- * @return
- */
- @Override
- public JSONArray getList(String sql) {
- JSONArray ret = new JSONArray();
- Connection connection = null;
- Statement statement = null;
- ResultSet resultSet = null;
- try {
- connection = dataSource.getConnection();
- statement = connection.createStatement();
- logSql(sql);
- resultSet = statement.executeQuery(sql);
- if (resultSet != null) {
- while (resultSet.next()) {
- // 组装数据为json 对象
- JSONObject data = new JSONObject();
- ResultSetMetaData metaData = resultSet.getMetaData();
- int columnCount = metaData.getColumnCount();
- for (int i = 1; i <= columnCount; i++) {
- String columnName = metaData.getColumnLabel(i);
- data.put(columnName, resultSet.getObject(columnName));
- }
- ret.add(data);
- }
- }
- } catch (Exception e) {
- log.error("获取数据报错", e);
- } finally {
- // 释放资源
- IoUtil.close(resultSet);
- IoUtil.close(statement);
- IoUtil.close(connection);
- }
- return ret;
- }
- private void logSql(String sql) {
- int len = 5000;
- // 执行SQL
- log.info("执行的SQL:{}", StrUtil.isNotBlank(sql) && sql.length() > len ? sql.substring(0, len) : sql);
- }
- /**
- * 计数
- *
- * @param sql
- * @return
- */
- @Override
- public long count(String sql) {
- String countSQL = getCountSqlFromQuerySql(sql);
- if (StrUtil.isBlank(countSQL)) {
- throw new RuntimeException("计数语句不得为空,SQL为:" + sql);
- }
- long ret = 0;
- Connection connection = null;
- Statement statement = null;
- ResultSet resultSet = null;
- try {
- connection = dataSource.getConnection();
- statement = connection.createStatement();
- logSql(sql);
- resultSet = statement.executeQuery(countSQL);
- if (resultSet != null) {
- while (resultSet.next()) {
- ret = resultSet.getLong(1);
- }
- }
- } catch (Exception e) {
- log.error("获取数据报错", e);
- } finally {
- // 释放资源
- if (resultSet != null) {
- try {
- resultSet.close();
- } catch (Exception ignored) {
- }
- }
- if (statement != null) {
- try {
- statement.close();
- } catch (Exception ignored) {
- }
- }
- if (connection != null) {
- try {
- connection.close();
- } catch (Exception ignored) {
- }
- }
- }
- return ret;
- }
- /**
- * 从查询语句变计数语句
- *
- * @param sql
- * @return
- */
- public String getCountSqlFromQuerySql(String sql) {
- String selectStr = "select";
- int selectIndex = sql.indexOf(selectStr);
- int fromIndex = sql.indexOf("from");
- return sql.replace(sql.substring(selectIndex + selectStr.length(), fromIndex), " count(*) as c ");
- }
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @return
- */
- @Override
- public JSONArray pageGetList(String sql, int page, int pageSize) {
- String querySql = getPageSqlFromQuerySql(sql, page, pageSize);
- if (StrUtil.isBlank(querySql)) {
- throw new RuntimeException("分页查询解析失败,SQL:" + sql + " 页号: " + page + " 每页数量:" + pageSize);
- }
- return getList(querySql);
- }
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @return
- */
- @Override
- public JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize) {
- JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
- // 查count
- long count = count(sql);
- long total = count / pageSize + (count % pageSize > 0 ? 1 : 0);
- log.info("数据总条数:{} 条,每页:{} 条,总页数:{} 页", count, pageSize, total);
- jimuPageDto.setTotal(total);
- // 查分页数据
- JSONArray data = pageGetList(sql, page, pageSize);
- List<JSONObject> dataList = new ArrayList<>(data.size());
- for (int i = 0; i < data.size(); i++) {
- JSONObject jsonObject = data.getJSONObject(i);
- dataList.add(jsonObject);
- }
- jimuPageDto.setData(dataList);
- jimuPageDto.setCount(count);
- return jimuPageDto;
- }
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @return
- */
- public String getPageSqlFromQuerySql(String sql, int page, int pageSize) {
- Assert.isTrue(page >= 1, () -> new IllegalArgumentException("page不得小于1"));
- Assert.isTrue(pageSize >= 1, () -> new IllegalArgumentException("pageSize不得小于1"));
- int skip = (page - 1) * pageSize;
- StringBuilder builder = new StringBuilder(sql);
- builder.append(" limit ");
- if (skip > 0) {
- builder.append(skip);
- builder.append(",");
- }
- builder.append(pageSize);
- String querySql = builder.toString();
- log.info("分页查询原SQL:{}\n分页SQL处理后:{}", sql, querySql);
- return querySql;
- }
- /**
- * 执行SQL返回数据,数据封装到类cls对象里
- *
- * @param sql
- * @param cls
- * @return
- */
- @Override
- public Object getOne(String sql, Class cls) {
- return getOne(sql).toJavaObject(cls);
- }
- /**
- * 执行SQL返回数据,数据封装到类cls对象里
- *
- * @param sql
- * @param cls
- * @return
- */
- @Override
- public List getList(String sql, Class cls) {
- return getList(sql).toJavaList(cls);
- }
- /**
- * 分页查询
- *
- * @param sql
- * @param page
- * @param pageSize
- * @param cls
- * @return
- */
- @Override
- public JimuPageDto pageGetListForJimu(String sql, int page, int pageSize, Class cls) {
- JimuPageDto<JSONObject> jimuPageDto = pageGetListForJimu(sql, page, pageSize);
- JimuPageDto ret = new JimuPageDto<>();
- List list = new ArrayList(jimuPageDto.getData().size());
- for (int i = 0; i < jimuPageDto.getData().size(); i++) {
- list.add(jimuPageDto.getData().get(i).toJavaObject(cls));
- }
- ret.setData(list);
- ret.setTotal(jimuPageDto.getTotal());
- ret.setCount(jimuPageDto.getCount());
- return ret;
- }
- }
复制代码 拦截切面
- /**
- * @author: humorchen
- * date: 2024/1/15
- * description: 下载任务切面
- * 对加上了@UseDownloadTaskCenter注解的方法进行切面,使用下载任务中心代理掉,完成下载任务
- **/
- @Aspect
- @Component
- @Slf4j
- public class DownloadTaskAspect {
- @Autowired
- private IDownloadTaskService downloadTaskService;
- /**
- * 注入spring 事件发布器
- */
- @Autowired
- private ApplicationEventPublisher eventPublisher;
- /**
- * 环绕通知
- *
- * @return
- */
- @Around("@annotation(cn.sffix.recovery.report.annotations.UseDownloadTaskCenter))")
- @Order(50)
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- log.info("【下载中心】进入下载中心切面");
- // 是下载中心发的请求则直接执行分页数据
- if (DownloadCenterUtil.isDownloadCenterRequest()) {
- log.info("【下载中心】下载中心发的请求,直接执行分页数据");
- return joinPoint.proceed();
- }
- // 识别下载请求
- int pageNo = 1;
- int pageSize = 20;
- Object[] args = joinPoint.getArgs();
- if (args != null && args.length > 0) {
- DownloadCenterBaseParam downloadCenterBaseParam = null;
- // 找到参数
- for (Object arg : args) {
- if (arg instanceof DownloadCenterBaseParam) {
- downloadCenterBaseParam = (DownloadCenterBaseParam) arg;
- break;
- }
- }
- // 检查参数
- if (downloadCenterBaseParam != null) {
- pageNo = Optional.ofNullable(downloadCenterBaseParam.getPageNo()).orElse(pageNo);
- pageSize = Optional.ofNullable(downloadCenterBaseParam.getPageSize()).orElse(pageSize);
- }
- log.info("【下载中心】下载中心切面,downloadCenterBaseParam:{}", downloadCenterBaseParam);
- if (downloadCenterBaseParam != null) {
- Object target = joinPoint.getTarget();
- Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
- Class<?> returnType = method.getReturnType();
- // 返回值类型检查
- if (returnType.equals(JimuPageDto.class)) {
- // 如果是导出请求,则使用下载任务中心代理掉
- if (isExportFirstPageRequest(pageNo, pageSize)) {
- // 如果是导出第一页请求,则使用下载任务中心代理掉
- DownloadTask downloadTask = registerTask(downloadCenterBaseParam, target, method, args);
- if (downloadTask == null || downloadTask.getId() == null) {
- log.error("【下载中心】注册下载任务失败,任务信息:{}", downloadTask);
- return joinPoint.proceed();
- }
- log.info("【下载中心】注册下载任务成功,任务信息:{}", downloadTask);
- // 返回积木所需要的数据
- JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
- jimuPageDto.setTotal(0);
- jimuPageDto.setCount(0);
- JSONObject jsonObject = new JSONObject();
- String downloadTaskJsonStr = downloadTask.getJson();
- DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTaskJsonStr, DownloadTaskJson.class);
- String requestBody = downloadTaskJson.getRequestBody();
- String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
- List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
- log.info("【下载中心】reportHead:{}", reportHead);
- if (CollectionUtil.isNotEmpty(reportHead) && reportHead.size() > 1) {
- String column = reportHead.get(1).getColumn();
- jsonObject.put(column, "请前往报表中台-下载中心查看(任务ID " + downloadTask.getId() + ")");
- log.info("【下载中心】返回数据:{}", jsonObject);
- } else {
- log.info("【下载中心】返回数据为空");
- }
- List<JSONObject> list = Collections.singletonList(jsonObject);
- jimuPageDto.setData(list);
- eventPublisher.publishEvent(new DownloadTaskPublishEvent(downloadTask.getId()));
- return jimuPageDto;
- } else {
- log.info("【下载中心】不是导出请求,直接执行分页数据");
- }
- } else {
- log.error("【下载中心】返回值类型不是JimuPageDto,无法使用下载任务中心代理掉");
- }
- }
- }
- return joinPoint.proceed();
- }
- /**
- * 生成下载任务
- *
- * @param downloadTaskParam
- * @return
- */
- private DownloadTask registerTask(DownloadCenterBaseParam downloadTaskParam, Object proxyTarget, Method method, Object[] args) {
- UserInfo loginUser = UserInfoHolder.get();
- String account = loginUser.getAccount();
- HttpServletRequest currentRequest = RequestUtil.getCurrentRequest();
- String requestBody = DownloadCenterUtil.getRequestBodyFromHeader(currentRequest);
- // 防止10秒内重复点击
- if (!downloadTaskService.setSameTaskLock(account, requestBody)) {
- log.error("【下载中心】10秒内重复点击,不给再次注册下载任务");
- return null;
- }
- String title = "导出-" + DateUtil.now().replace(" ", "_") + ".xlsx";
- try {
- title = downloadTaskService.getReportNameByReportId(downloadTaskService.getReportIdFromRequestBody(requestBody)) + title;
- } catch (Exception e) {
- log.error("【下载中心】获取报表名称失败", e);
- }
- String url = generateFileUrl();
- DownloadTask downloadTask = new DownloadTask();
- downloadTask.setAccount(account);
- downloadTask.setTitle(title);
- downloadTask.setIcon("");
- downloadTask.setUrl(url);
- downloadTask.setFileSize("");
- downloadTask.setPercent("0%");
- downloadTask.setState(DownloadTaskStateEnum.WAIT.getState());
- DownloadTaskJson downloadTaskJson = new DownloadTaskJson();
- // 拷贝最开始请求积木的token和requestBody,执行下载任务时需要
- downloadTaskJson.setRequestToken(DownloadCenterUtil.getRequestTokenFromHeader(currentRequest));
- downloadTaskJson.setRequestBody(requestBody);
- downloadTaskJson.setProxyMethod(method.getName());
- if (args != null) {
- for (Object arg : args) {
- if (arg instanceof DownloadCenterBaseParam) {
- downloadTaskJson.setParam((DownloadCenterBaseParam) arg);
- break;
- }
- }
- }
- downloadTask.setJson(JSONObject.toJSONString(downloadTaskJson));
- downloadTask = downloadTaskService.registerTask(downloadTask);
- return downloadTask;
- }
- /**
- * 生成文件url地址
- *
- * @return
- */
- private String generateFileUrl() {
- // todo 生成文件url地址
- return "";
- }
- /**
- * 注入jm report分页大小
- */
- @Value("${jeecg.jmreport.page-size-number:5000}")
- private int jmReportPageSizeNumber;
- /**
- * 判断是否为导出请求
- *
- * @param pageNo
- * @param pageSize
- * @return
- */
- private boolean isExportPageRequest(int pageNo, int pageSize) {
- return pageSize == jmReportPageSizeNumber;
- }
- /**
- * 判断是否为导出请求
- *
- * @param pageNo
- * @param pageSize
- * @return
- */
- private boolean isExportFirstPageRequest(int pageNo, int pageSize) {
- log.info("【下载中心】判断是否为导出请求 isExportFirstPageRequest pageNo:{},pageSize:{},积木报表导出size:{}", pageNo, pageSize, jmReportPageSizeNumber);
- return pageNo == 1 && isExportPageRequest(pageNo, pageSize);
- }
- }
复制代码 报表下载使命处理事件
暂时由于导出项目只部署一个实例,导出使命使用的是spring 本地event来异步处理的,你可以使用MQ、调理来执行掉使命,可以参考我这个
上传华为云OSS的部分你更换为你的OSS上传操作,这个地方我是复用项目里已有的存储上传服务
内里的异步线程池、环境工具类、积木导出每页大小要设置本身的
- /**
- * @author: humorchen
- * date: 2024/1/16
- * description: 下载任务发布事件
- **/
- @Getter
- public class DownloadTaskPublishEvent extends ApplicationEvent {
- /**
- * 任务ID
- */
- private final Integer taskId;
- public DownloadTaskPublishEvent(Integer taskId) {
- super(taskId);
- this.taskId = taskId;
- }
-
- }
复制代码 处理器
- /**
- * @author: humorchen
- * date: 2024/1/16
- * description: 处理下载任务发布事件
- **/
- @Component
- @Slf4j
- public class DownloadTaskPublishEventHandler implements ApplicationListener<DownloadTaskPublishEvent> {
- @Autowired
- private IDownloadTaskService downloadTaskService;
- @Autowired
- private IReportService reportService;
- @Autowired
- @Qualifier(AsyncConfig.ASYNC_THREAD_POOL)
- private Executor executor;
- @Autowired
- private HwProperties hwProperties;
- @Autowired
- private EnvUtil envUtil;
- /**
- * 注入jm report分页大小
- */
- @Value("${jeecg.jmreport.page-size-number:5000}")
- private int jmReportPageSizeNumber;
- @Autowired
- private RedissonClient redissonClient;
- /**
- * 超时时间
- */
- private static final int TIMEOUT_MILLS = 1000 * 60 * 20;
- @Autowired
- private HwObjectStorageService hwObjectStorageService;
- /**
- * 上传文件到OSS的路径
- *
- * @param account
- * @param fileName
- * @return
- */
- private final StoragePath getStoragePath(String account, String fileName) {
- return StoragePath.path().addFolder("downloadTask").addFolder(account).fileName(fileName);
- }
- /**
- * Handle an application event.
- *
- * @param event the event to respond to
- */
- @Override
- @Async(AsyncConfig.ASYNC_THREAD_POOL)
- public void onApplicationEvent(DownloadTaskPublishEvent event) {
- Integer taskId = event.getTaskId();
- log.info("【下载中心】执行下载任务 taskId:{}", taskId);
- DownloadTask downloadTask = downloadTaskService.getById(taskId);
- if (downloadTask == null) {
- log.error("【下载中心】下载任务不存在,taskId:{}", taskId);
- return;
- }
- if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
- log.error("【下载中心】下载任务正在执行中,taskId:{}", taskId);
- return;
- }
- try {
- log.info("【下载中心】下载任务开始执行,taskId:{}", taskId);
- // 改状态到执行中
- DownloadTaskStateEnum downloadTaskStateEnum = Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT);
- int compareAndSwapTaskState = downloadTaskService.compareAndSwapTaskState(taskId, downloadTaskStateEnum, DownloadTaskStateEnum.RUNNING);
- if (compareAndSwapTaskState < 1) {
- log.info("【下载中心】下载任务状态不对,taskId:{}, state:{}", taskId, downloadTaskStateEnum);
- return;
- }
- DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTask.getJson(), DownloadTaskJson.class);
- // 获取数据
- String requestBody = downloadTaskJson.getRequestBody();
- String requestToken = downloadTaskJson.getRequestToken();
- String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
- String reportName = downloadTaskService.getReportNameByReportId(reportId);
- String requestParam = downloadTaskService.getRequestParamFromJson(downloadTask.getJson());
- JimuReportDataSourceDTO dataSourceDTO = downloadTaskService.getReportApiOrSqlByReportId(reportId);
- List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
- // 打印上面拿到的数据
- log.info("reportId :{} \n reportName:{} \n requestParam:{} \n requestBody:{} \n dataSourceDTO:{} \n reportHead:{}", reportId, reportName, requestParam, requestBody, dataSourceDTO, reportHead);
- JimuReportDynamicEasyExcelImpl jimuReportDynamicEasyExcel = new JimuReportDynamicEasyExcelImpl(reportId, reportName, taskId, downloadTaskService, requestParam, requestToken, dataSourceDTO, reportHead);
- // 生成excel文件
- List<List<String>> head = reportHead.stream().map(d -> Collections.singletonList(d.getName())).collect(Collectors.toList());
- // 分页写数据
- InputStream inputStream = DynamicColumnEasyExcelUtil.writePageData(head, jimuReportDynamicEasyExcel, jmReportPageSizeNumber);
- // 上传excel文件到oss
- StoragePath storagePath = getStoragePath(downloadTask.getAccount(), downloadTask.getTitle());
- downloadTask.setPercent("100%");
- StopWatch stopWatch = new StopWatch();
- stopWatch.start();
- URI uri = hwObjectStorageService.savePublicFile(inputStream, storagePath);
- stopWatch.stop();
- log.info("【下载中心】上传文件到OSS,耗时:{} ms,uri:{}", stopWatch.getLastTaskTimeMillis(), uri);
- // 更新任务信息
- String url = getUrlPrefix() + uri.getPath();
- downloadTask.setUrl(url);
- downloadTask.setState(DownloadTaskStateEnum.SUCCESS.getState());
- log.info("【下载中心】下载任务成功,taskId:{},task:{}", taskId, downloadTask);
- boolean updated = downloadTaskService.updateById(downloadTask);
- log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
- } catch (Exception e) {
- log.error("【下载中心】下载任务执行失败", e);
- // 更新任务信息
- downloadTask.setState(DownloadTaskStateEnum.FAILED.getState());
- downloadTask.setError("【下载中心】执行失败(" + e.getMessage() + ")");
- boolean updated = downloadTaskService.updateById(downloadTask);
- log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
- } finally {
- log.info("【下载中心】下载任务 {} 执行完毕", taskId);
- }
- }
- /**
- * 根据环境获取文件url前缀
- *
- * @return
- */
- private String getUrlPrefix() {
- String envCode = envUtil.isPreOrProdEnv() ? "pro" : "test";
- String prefix = "https://test-obs.xxxxx.com";
- for (String key : hwProperties.getUrlMap().keySet()) {
- if (key.contains(envCode)) {
- prefix = hwProperties.getUrlMap().get(key);
- }
- }
- return prefix;
- }
- }
复制代码 透传信息
紧张是在这个customApiHeader() 方法里DownloadCenterUtil.copyDownloadCenterHeader(request, header);自定义获取数据哀求的header这,读取这次积木导出哀求的参数信息,写到获取数据哀求的header里去
下载中央工具类
异步线程池
- /**
- * @author: humorchen
- * date: 2024/1/16
- * description: 异步配置
- **/
- @Slf4j
- @Configuration
- public class AsyncConfig {
- public static final String ASYNC_THREAD_POOL = "asyncThreadPool";
- /**
- * 异步线程池
- */
- @Bean(name = ASYNC_THREAD_POOL)
- public Executor asyncExecutor() {
- ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
- executor.setCorePoolSize(8);
- executor.setMaxPoolSize(64);
- executor.setQueueCapacity(128);
- executor.setThreadNamePrefix("asyncThreadPool-");
- executor.initialize();
- return executor;
- }
- }
复制代码 给前端袒露接口
- /**
- * @author humorchen
- * date: 2024/2/28
- * description: 下载中心API
- **/
- @RequestMapping("/report/form/downloadCenter")
- public interface DownloadCenterApi {
- /**
- * 分页查下载任务
- *
- * @param pageListDownloadTaskDto
- * @return
- */
- @RequestMapping("/pageListDownloadTask")
- Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);
- /**
- * 删除下载任务
- *
- * @param taskId
- * @return
- */
- @RequestMapping("/deleteTask")
- Result<String> deleteTask(Integer taskId);
- /**
- * 重新执行下载任务
- *
- * @param taskId
- * @return
- */
- @RequestMapping("/rerunTask")
- Result<String> rerunTask(Integer taskId);
- }
复制代码- /**
- * @author humorchen
- * date: 2024/2/28
- * description: 下载中心
- **/
- @RestController
- public class DownloadCenterController implements DownloadCenterApi {
- @Autowired
- private IDownloadTaskService downloadTaskService;
- /**
- * 分页查下载任务
- *
- * @param pageListDownloadTaskDto
- * @return
- */
- @Override
- public Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
- IPage<DownloadTask> downloadTaskPages = downloadTaskService.pageListDownloadTask(pageListDownloadTaskDto);
- Page<DownloadTaskVo> downloadTaskVoPage = new Page<>();
- downloadTaskVoPage.setCurrent(downloadTaskPages.getCurrent());
- downloadTaskVoPage.setPages(downloadTaskPages.getPages());
- downloadTaskVoPage.setSize(downloadTaskPages.getSize());
- downloadTaskVoPage.setTotal(downloadTaskPages.getTotal());
- List<DownloadTaskVo> downloadTaskVos = downloadTaskPages.getRecords().stream().map(downloadTask -> {
- DownloadTaskVo downloadTaskVo = BeanUtils.convert(downloadTask, DownloadTaskVo.class);
- downloadTaskVo.setStateStr(Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT).getTitle());
- return downloadTaskVo;
- }).collect(Collectors.toList());
- downloadTaskVoPage.setRecords(downloadTaskVos);
- return Result.ok(downloadTaskVoPage);
- }
- /**
- * 删除下载任务
- *
- * @param taskId
- * @return
- */
- @Override
- public Result<String> deleteTask(Integer taskId) {
- boolean removeById = downloadTaskService.removeById(taskId);
- return removeById ? Result.ok("删除成功") : Result.fail("未找到该任务,请刷新后重试)");
- }
- /**
- * 重新执行下载任务
- *
- * @param taskId
- * @return
- */
- @Override
- public Result<String> rerunTask(Integer taskId) {
- return downloadTaskService.rerunTask(taskId);
- }
- }
复制代码 DynamicColumnEasyExcelUtil 动态字段EasyExcel工具
- /**
- * @author humorchen
- * date: 2024/3/5
- * description: 动态列easyexcel工具类
- **/
- @Slf4j
- public class DynamicColumnEasyExcelUtil {
- public static final String ROW = "=row()";
- public static interface DynamicColumnEasyExcelInterface<T> {
- /**
- * 分页获取数据
- *
- * @param page
- * @param size
- * @return
- */
- JimuPageDto<T> pageGetData(int page, int size);
- /**
- * 数据对象转换为字符串
- *
- * @param t
- * @return
- */
- List<String> mapDataToStringList(T t);
- /**
- * 分页获取数据加载第i页时触发函数,用于实现进度变更
- *
- * @param pageNo
- * @param pageSize
- */
- void onLoadedPage(int pageNo, int pageSize, int pages);
- }
- /**
- * 从数据库分页读数据并写入成Excel文件,把文件内容写到输出流
- *
- * @param head
- * @param dynamicColumnEasyExcelInterface
- * @param pageSize
- * @param <T>
- * @return
- */
- public static <T> ByteArrayInputStream writePageData(List<List<String>> head, DynamicColumnEasyExcelInterface<T> dynamicColumnEasyExcelInterface, int pageSize) {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ExcelWriter excelWriter = EasyExcel.write(outputStream).head(head).build();
- // 当前分页
- int currentPage = 1;
- // 总页数
- long pages = 1;
- // 当前写入的sheet页
- int sheetNo = 1;
- // 写入计数(用于自动翻sheet页)
- int writeRowCount = 0;
- // 积木序号编号
- int index = 1;
- StopWatch stopWatch = new StopWatch("报表分页写入Excel");
- WriteSheet sheet = getWriteSheet(sheetNo);
- do {
- // 加载数据
- stopWatch.start("加载第" + currentPage + "页数据");
- JimuPageDto<T> jimuPageDto = dynamicColumnEasyExcelInterface.pageGetData(currentPage, pageSize);
- stopWatch.stop();
- // 数据判空
- List<T> records = jimuPageDto.getData();
- if (CollectionUtil.isEmpty(records)) {
- break;
- }
- // 转换数据
- stopWatch.start("转换第" + currentPage + "页数据");
- List<List<String>> data = records.stream().map(dynamicColumnEasyExcelInterface::mapDataToStringList).collect(Collectors.toList());
- stopWatch.stop();
- // 处理序号 row()
- if (CollectionUtil.isNotEmpty(data) && CollectionUtil.isNotEmpty(data.get(0)) && ROW.equals(data.get(0).get(0))) {
- for (List<String> stringList : data) {
- if (CollectionUtil.isNotEmpty(stringList) && ROW.equals(stringList.get(0))) {
- stringList.set(0, String.valueOf(index));
- ++index;
- }
- }
- }
- // 自动跳sheet页
- if (writeRowCount + data.size() >= MAX_SIZE_PER_SHEET) {
- ++sheetNo;
- writeRowCount = 0;
- index = 1;
- sheet = getWriteSheet(sheetNo);
- }
- // 写入数据
- stopWatch.start("写入第" + currentPage + "页数据(" + data.size() + "条数据)");
- excelWriter.write(data, sheet);
- stopWatch.stop();
- pages = jimuPageDto.getTotal();
- // 更新进度
- dynamicColumnEasyExcelInterface.onLoadedPage(currentPage, pageSize, (int) pages);
- log.info("【下载中心】 分页获取数据,第{}页,总页数:{} 第一行数据是:{}", currentPage, pages, data.get(0));
- // 自增
- currentPage++;
- writeRowCount += data.size();
- } while (currentPage <= pages);
- log.info("【下载中心】 耗时打印");
- for (StopWatch.TaskInfo taskInfo : stopWatch.getTaskInfo()) {
- log.info("【下载中心】 {} 耗时:{} ms", taskInfo.getTaskName(), taskInfo.getTimeMillis());
- }
- excelWriter.finish();
- return new ByteArrayInputStream(outputStream.toByteArray());
- }
- /**
- * 生成WriteSheet对象
- *
- * @param sheetNo 1开始
- * @return WriteSheet
- */
- private static WriteSheet getWriteSheet(int sheetNo) {
- WriteSheet sheet = new WriteSheet();
- sheet.setSheetName("sheet" + sheetNo);
- sheet.setSheetNo(sheetNo - 1);
- return sheet;
- }
- /**
- * 获取字段宽度策略
- *
- * @return
- */
- private AbstractHeadColumnWidthStyleStrategy getAbstractColumnWidthStyleStrategy() {
- return new AbstractHeadColumnWidthStyleStrategy() {
- /**
- * Returns the column width corresponding to each column head.
- *
- * <p>
- * if return null, ignore
- *
- * @param head Nullable.
- * @param columnIndex Not null.
- * @return
- */
- @Override
- protected Integer columnWidth(Head head, Integer columnIndex) {
- return null;
- }
- };
- }
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |