积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分 ...

打印 上一主题 下一主题

主题 872|帖子 872|积分 2616

积木报表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

  1.                         <dependency>
  2.                 <groupId>com.alibaba</groupId>
  3.                 <artifactId>easyexcel-core</artifactId>
  4.                 <version>3.1.1</version>
  5.             </dependency>
  6.             <dependency>
  7.                 <groupId>com.alibaba</groupId>
  8.                 <artifactId>easyexcel</artifactId>
  9.                 <version>3.1.1</version>
  10.             </dependency>
复制代码
新建数据库表

  1. CREATE TABLE `t_download_task` (
  2.   `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  3.   `account` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提交任务的账号',
  4.   `title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '下载任务标题',
  5.   `icon` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标',
  6.   `url` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件URL',
  7.   `file_size` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件大小',
  8.   `percent` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '进度(例如50%)',
  9.   `state` tinyint(4) DEFAULT '0' COMMENT '任务状态(0 等待执行,1执行中,2执行成功,3执行失败)',
  10.   `error` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '执行报错信息(有则填)',
  11.   `json` varchar(4096) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '{}' COMMENT '预留的json扩展字段',
  12.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  13.   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  14.   PRIMARY KEY (`id`),
  15.   KEY `idx_account_create_time` (`account`,`create_time`),
  16.   KEY `idx_create_time` (`create_time`)
  17. ) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='下载中心的任务';
复制代码
下载使命
  1. /**
  2. * <p>
  3. * 下载任务
  4. * </p>
  5. *
  6. * @author humorchen
  7. * @since 2024-01-05
  8. */
  9. @Data
  10. @EqualsAndHashCode(callSuper = false)
  11. @Accessors(chain = true)
  12. @TableName("t_download_task")
  13. public class DownloadTask implements Serializable {
  14.     private static final long serialVersionUID = 1L;
  15.     /**
  16.      * ID
  17.      */
  18.     @TableId(value = "id", type = IdType.AUTO)
  19.     private Integer id;
  20.     /**
  21.      * 创建下载任务的账号
  22.      */
  23.     @TableField("account")
  24.     private String account;
  25.     /**
  26.      * 下载任务标题
  27.      */
  28.     @TableField("title")
  29.     private String title;
  30.     /**
  31.      * 图标
  32.      */
  33.     @TableField("icon")
  34.     private String icon;
  35.     /**
  36.      * 文件URL
  37.      */
  38.     @TableField("url")
  39.     private String url;
  40.     /**
  41.      * 文件大小
  42.      */
  43.     @TableField("file_size")
  44.     private String fileSize;
  45.     /**
  46.      * 进度(例如50%)
  47.      */
  48.     @TableField("percent")
  49.     private String percent;
  50.     /**
  51.      * 任务状态(0 等待执行,1执行中,2执行成功,3执行失败)
  52.      */
  53.     @TableField("state")
  54.     private Integer state;
  55.     /**
  56.      * 执行报错信息(有则填)
  57.      */
  58.     @TableField("error")
  59.     private String error;
  60.     /**
  61.      * 预留的json扩展字段
  62.      */
  63.     @TableField("json")
  64.     private String json;
  65.     /**
  66.      * 创建时间
  67.      */
  68.     @TableField("create_time")
  69.     private Date createTime;
  70.     /**
  71.      * 更新时间
  72.      */
  73.     @TableField("update_time")
  74.     private Date updateTime;
  75.     public static final String ID = "id";
  76.     public static final String ACCOUNT = "account";
  77.     public static final String TITLE = "title";
  78.     public static final String ICON = "icon";
  79.     public static final String URL = "url";
  80.     public static final String FILE_SIZE = "file_size";
  81.     public static final String PERCENT = "percent";
  82.     public static final String STATE = "state";
  83.     public static final String ERROR = "error";
  84.     public static final String JSON = "json";
  85.     public static final String CREATE_TIME = "create_time";
  86.     public static final String UPDATE_TIME = "update_time";
  87. }
复制代码
使用mabatis plus code generator生成service、mapper等文件
识别所需注解、基类

数据获取api所调用服务上方法上标注的注解
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/15
  4. * description: 该报表接口使用下载任务中心代理掉,完成下载任务
  5. * 使用要求:
  6. * 参数中需要有一个参数是DownloadCenterBaseParam的子类,方法返回值类型需要是支持泛型的JimuPageDto类,方法上加注@UseDownloadTaskCenter注解
  7. * 参考cn.sffix.recovery.report.service.impl.ReportServiceImpl#dashboardNewVersion(cn.sffix.recovery.report.entity.dto.DashBoardQueryDto)
  8. **/
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Target({ElementType.METHOD})
  11. @Documented
  12. public @interface UseDownloadTaskCenter {
  13. }
复制代码
示范给数据接口调用的服务加注解
  1.     @Override
  2.     @UseDownloadTaskCenter
  3.     public JimuPageDto<DashBoardDataDto> dashboardNewVersion(DashBoardQueryDto dashBoardQueryDto) {
  4.     //  你的数据接口逻辑
  5.     }
复制代码
参数基类
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/15
  4. * description:
  5. **/
  6. @Data
  7. public class DownloadCenterBaseParam {
  8.     /**
  9.      * 分页数据页号和页大小
  10.      */
  11.     private Integer pageNo;
  12.     /**
  13.      * 分页数据页号和页大小
  14.      */
  15.     private Integer pageSize;
  16. }
复制代码
结果基类
  1. /**
  2. * @author: humorchen
  3. * date: 2023/12/19
  4. * description:
  5. **/
  6. @Data
  7. @FieldNameConstants
  8. @Accessors(chain = true)
  9. public class JimuPageDto<T> {
  10.     /**
  11.      * 数据
  12.      */
  13.     private List<T> data;
  14.     /**
  15.      * 积木的count是总数据条数,不是当前页多少条!!!
  16.      */
  17.     private long count;
  18.     /**
  19.      * 积木的total是总页数,不是总数据条数!!!
  20.      */
  21.     private long total;
  22.     public static final JimuPageDto EMPTY = new JimuPageDto().setData(Collections.emptyList()).setTotal(0).setCount(0);
  23. }
复制代码
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/5
  4. * description: 下载任务状态
  5. **/
  6. @Getter
  7. public enum DownloadTaskStateEnum {
  8.     WAIT(0, "等待执行"),
  9.     RUNNING(1, "执行中"),
  10.     SUCCESS(2, "执行成功"),
  11.     FAILED(3, "执行失败"),
  12.     ;
  13.     private final int state;
  14.     private final String title;
  15.     DownloadTaskStateEnum(int state, String title) {
  16.         this.state = state;
  17.         this.title = title;
  18.     }
  19.     /**
  20.      * 根据状态获取枚举
  21.      *
  22.      * @param state
  23.      * @return
  24.      */
  25.     public static DownloadTaskStateEnum of(int state) {
  26.         for (DownloadTaskStateEnum value : values()) {
  27.             if (value.state == state) {
  28.                 return value;
  29.             }
  30.         }
  31.         return null;
  32.     }
  33. }
复制代码
下载中央服务(报表下载的导出使命)

  1. /**
  2. * <p>
  3. * 下载任务 服务类
  4. * </p>
  5. *
  6. * @author humorchen
  7. * @since 2024-01-05
  8. */
  9. public interface IDownloadTaskService extends IService<DownloadTask> {
  10.     /**
  11.      * 注册任务
  12.      *
  13.      * @param downloadTask
  14.      * @return
  15.      */
  16.     DownloadTask registerTask(@NonNull DownloadTask downloadTask);
  17.     /**
  18.      * 10秒内是否有相同任务未完成,不给再次注册下载任务
  19.      *
  20.      * @param account
  21.      * @param requestBody
  22.      * @return
  23.      */
  24.     boolean setSameTaskLock(String account, String requestBody);
  25.     /**
  26.      * 更新任务
  27.      *
  28.      * @param downloadTask
  29.      * @return
  30.      */
  31.     int updateTaskById(@NonNull DownloadTask downloadTask);
  32.     /**
  33.      * 更新任务进度
  34.      *
  35.      * @param id
  36.      * @param percent
  37.      * @return
  38.      */
  39.     int changeTaskPercent(int id, @NonNull String percent);
  40.     /**
  41.      * 更新任务状态
  42.      *
  43.      * @param id
  44.      * @param state
  45.      * @return
  46.      */
  47.     int changeTaskState(int id, @NonNull DownloadTaskStateEnum state);
  48.     /**
  49.      * 更新任务状态
  50.      *
  51.      * @param id
  52.      * @param expectState
  53.      * @param targetState
  54.      * @return
  55.      */
  56.     int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState);
  57.     /**
  58.      * 根据任务ID获取任务
  59.      *
  60.      * @param id
  61.      * @return
  62.      */
  63.     DownloadTask getDownloadTaskById(int id);
  64.     /**
  65.      * 分页查下载任务
  66.      *
  67.      * @param pageListDownloadTaskDto
  68.      * @return
  69.      */
  70.     IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);
  71.     /**
  72.      * 重新执行下载任务
  73.      *
  74.      * @param taskId
  75.      * @return
  76.      */
  77.     @RequestMapping("/rerunTask")
  78.     Result<String> rerunTask(Integer taskId);
  79.     /**
  80.      * 根据报表ID获取报表名称
  81.      *
  82.      * @param reportId
  83.      * @return
  84.      */
  85.     String getReportNameByReportId(String reportId);
  86.     /**
  87.      * 从请求体中获取报表ID
  88.      *
  89.      * @param requestBody
  90.      * @return
  91.      */
  92.     String getReportIdFromRequestBody(String requestBody);
  93.     /**
  94.      * 根据报表ID获取报表API地址或者SQL
  95.      *
  96.      * @param reportId
  97.      * @return
  98.      */
  99.     JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId);
  100.     /**
  101.      * 获取积木报表的头
  102.      *
  103.      * @param reportId
  104.      * @return
  105.      */
  106.     List<JimuReportDataColumnDTO> getReportHead(String reportId);
  107.     /**
  108.      * 从积木请求体中获取请求参数
  109.      *
  110.      * @param json
  111.      * @return
  112.      */
  113.     String getRequestParamFromJson(String json);
  114. }
复制代码
实现类
  1. /**
  2. * <p>
  3. * 下载任务 服务实现类
  4. * </p>
  5. *
  6. * @author humorchen
  7. * @since 2024-01-05
  8. */
  9. @Service
  10. @Slf4j
  11. public class DownloadTaskServiceImpl extends ServiceImpl<DownloadTaskMapper, DownloadTask> implements IDownloadTaskService {
  12.     @Autowired
  13.     private DownloadTaskMapper downloadTaskMapper;
  14.     @Autowired
  15.     private IReportDataGetService reportDataGetService;
  16.     @Autowired
  17.     private RedisTemplate<String, String> redisTemplate;
  18.     /**
  19.      * 注入spring 事件发布器
  20.      */
  21.     @Autowired
  22.     private ApplicationEventPublisher eventPublisher;
  23.     /**
  24.      * 注册任务
  25.      *
  26.      * @param downloadTask
  27.      * @return
  28.      */
  29.     @Override
  30.     public DownloadTask registerTask(@NonNull DownloadTask downloadTask) {
  31.         downloadTaskMapper.insert(downloadTask);
  32.         return downloadTask;
  33.     }
  34.     /**
  35.      * 10秒内是否有相同任务未完成,不给再次注册下载任务
  36.      *
  37.      * @param account
  38.      * @param requestBody
  39.      * @return
  40.      */
  41.     @Override
  42.     public boolean setSameTaskLock(String account, String requestBody) {
  43.         DownloadTaskSubmitLimitCacheKey limitCacheKey = new DownloadTaskSubmitLimitCacheKey(account, MD5.create().digestHex(requestBody));
  44.         Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(limitCacheKey.getKey(), DateUtil.now(), limitCacheKey.getExpire(), limitCacheKey.getTimeUnit());
  45.         return Boolean.TRUE.equals(setIfAbsent);
  46.     }
  47.     /**
  48.      * 更新任务
  49.      *
  50.      * @param downloadTask
  51.      * @return
  52.      */
  53.     @Override
  54.     public int updateTaskById(@NonNull DownloadTask downloadTask) {
  55.         return downloadTaskMapper.updateById(downloadTask);
  56.     }
  57.     /**
  58.      * 更新任务进度
  59.      *
  60.      * @param id
  61.      * @param percent
  62.      * @return
  63.      */
  64.     @Override
  65.     public int changeTaskPercent(int id, @NonNull String percent) {
  66.         UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
  67.         updateWrapper.eq(DownloadTask.ID, id);
  68.         updateWrapper.set(DownloadTask.PERCENT, percent);
  69.         log.info("【下载中心】更新任务进度 id:{} percent:{}", id, percent);
  70.         return downloadTaskMapper.update(null, updateWrapper);
  71.     }
  72.     /**
  73.      * 更新任务状态
  74.      *
  75.      * @param id
  76.      * @param state
  77.      * @return
  78.      */
  79.     @Override
  80.     public int changeTaskState(int id, @NonNull DownloadTaskStateEnum state) {
  81.         UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
  82.         updateWrapper.eq(DownloadTask.ID, id);
  83.         updateWrapper.set(DownloadTask.STATE, state.getState());
  84.         return downloadTaskMapper.update(null, updateWrapper);
  85.     }
  86.     /**
  87.      * 更新任务状态
  88.      *
  89.      * @param id
  90.      * @param expectState
  91.      * @param targetState
  92.      * @return
  93.      */
  94.     @Override
  95.     public int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState) {
  96.         UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
  97.         updateWrapper.eq(DownloadTask.ID, id);
  98.         updateWrapper.eq(DownloadTask.STATE, expectState.getState());
  99.         updateWrapper.set(DownloadTask.STATE, targetState.getState());
  100.         return downloadTaskMapper.update(null, updateWrapper);
  101.     }
  102.     /**
  103.      * 根据任务ID获取任务
  104.      *
  105.      * @param id
  106.      * @return
  107.      */
  108.     @Override
  109.     public DownloadTask getDownloadTaskById(int id) {
  110.         return downloadTaskMapper.selectById(id);
  111.     }
  112.     /**
  113.      * 查下载任务
  114.      *
  115.      * @param pageListDownloadTaskDto
  116.      * @return
  117.      */
  118.     @Override
  119.     public IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
  120.         Integer id = pageListDownloadTaskDto.getId();
  121.         String startTime = pageListDownloadTaskDto.getStartTime();
  122.         String endTime = pageListDownloadTaskDto.getEndTime();
  123.         String fileName = pageListDownloadTaskDto.getFileName();
  124.         Integer taskState = pageListDownloadTaskDto.getTaskState();
  125.         UserInfo userInfo = UserInfoHolder.get();
  126.         String account = userInfo.getAccount();
  127.         int pageNo = Optional.ofNullable(pageListDownloadTaskDto.getPageNo()).orElse(1);
  128.         int pageSize = Optional.ofNullable(pageListDownloadTaskDto.getPageSize()).orElse(10);
  129.         QueryWrapper<DownloadTask> queryWrapper = new QueryWrapper<>();
  130.         queryWrapper.eq(DownloadTask.ACCOUNT, account);
  131.         queryWrapper.eq(id != null, DownloadTask.ID, id);
  132.         queryWrapper.between(startTime != null && endTime != null, DownloadTask.CREATE_TIME, startTime, endTime);
  133.         queryWrapper.like(StrUtil.isNotBlank(fileName), DownloadTask.TITLE, "%" + fileName + "%");
  134.         queryWrapper.eq(taskState != null, DownloadTask.STATE, taskState);
  135.         // 最新的在前
  136.         queryWrapper.orderByDesc(DownloadTask.CREATE_TIME);
  137.         return page(new Page<>(pageNo, pageSize), queryWrapper);
  138.     }
  139.     /**
  140.      * 重新执行下载任务
  141.      *
  142.      * @param taskId
  143.      * @return
  144.      */
  145.     @Override
  146.     public Result<String> rerunTask(Integer taskId) {
  147.         DownloadTask downloadTask = getDownloadTaskById(taskId);
  148.         if (downloadTask == null) {
  149.             return Result.fail("未找到该任务,请刷新后重试");
  150.         }
  151.         if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
  152.             return Result.fail("任务正在执行中,请稍后重试");
  153.         }
  154.         eventPublisher.publishEvent(new DownloadTaskPublishEvent(taskId));
  155.         return Result.ok("重新执行中");
  156.     }
  157.     /**
  158.      * 根据报表ID获取报表名称
  159.      *
  160.      * @param reportId
  161.      * @return
  162.      */
  163.     @Override
  164.     public String getReportNameByReportId(String reportId) {
  165.         if (StrUtil.isBlank(reportId)) {
  166.             return "";
  167.         }
  168.         String sql = "select name from report.jimu_report where id = '" + reportId + "'";
  169.         JSONObject jsonObject = reportDataGetService.getOne(sql);
  170.         return Optional.ofNullable(jsonObject.getString("name")).orElse("");
  171.     }
  172.     /**
  173.      * 从请求体中获取报表ID
  174.      *
  175.      * @param requestBody
  176.      * @return
  177.      */
  178.     @Override
  179.     public String getReportIdFromRequestBody(String requestBody) {
  180.         if (StrUtil.isNotBlank(requestBody)) {
  181.             JSONObject jsonObject = JSONObject.parseObject(requestBody);
  182.             return jsonObject.getString("excelConfigId");
  183.         }
  184.         return null;
  185.     }
  186.     /**
  187.      * 根据报表ID获取报表API地址或者SQL
  188.      *
  189.      * @param reportId
  190.      * @return
  191.      */
  192.     @Override
  193.     public JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId) {
  194.         JimuReportDataSourceDTO jimuReportDataSourceDTO = new JimuReportDataSourceDTO();
  195.         if (StrUtil.isNotBlank(reportId)) {
  196.             String sql = "select db_dyn_sql,api_url from report.jimu_report_db where jimu_report_id = '" + reportId + "'";
  197.             JSONObject jsonObject = reportDataGetService.getOne(sql);
  198.             jimuReportDataSourceDTO.setSql(jsonObject.getString("db_dyn_sql"));
  199.             jimuReportDataSourceDTO.setApiUrl(jsonObject.getString("api_url"));
  200.         }
  201.         List<List<String>> head = new ArrayList<>();
  202.         EasyExcel.write(new OutputStream() {
  203.             @Override
  204.             public void write(int b) throws IOException {
  205.             }
  206.         }).head(head).sheet("sheet").doWrite(new ArrayList<>());
  207.         return jimuReportDataSourceDTO;
  208.     }
  209.     /**
  210.      * 获取积木报表的头
  211.      *
  212.      * @param reportId
  213.      * @return
  214.      */
  215.     @Override
  216.     public List<JimuReportDataColumnDTO> getReportHead(String reportId) {
  217.         if (StrUtil.isBlank(reportId)) {
  218.             return Collections.emptyList();
  219.         }
  220.         String sql = "select json_str from report.jimu_report where id = '" + reportId + "'";
  221.         JSONObject jsonObject = reportDataGetService.getOne(sql);
  222.         String jsonStr = jsonObject.getString("json_str");
  223.         JSONObject json = JSONObject.parseObject(jsonStr);
  224.         JSONObject rows = json.getJSONObject("rows");
  225.         JSONObject rows0Cells = rows.getJSONObject("0").getJSONObject("cells");
  226.         JSONObject rows1Cells = rows.getJSONObject("1").getJSONObject("cells");
  227.         Set<String> rows0KeySets = rows0Cells.keySet();
  228.         List<JimuReportDataColumnDTO> heads = rows0KeySets.stream().map(key -> {
  229.             JSONObject keyObject = rows0Cells.getJSONObject(key);
  230.             JSONObject columnObject = rows1Cells.getJSONObject(key);
  231.             if (keyObject == null || columnObject == null) {
  232.                 return null;
  233.             }
  234.             String name = keyObject.getString("text");
  235.             String column = columnObject.getString("text");
  236.             if (StrUtil.isBlank(name) || StrUtil.isBlank(column)) {
  237.                 return null;
  238.             }
  239.             // 处理 #{vpjcgifyua.orderId}
  240.             int indexOf = column.lastIndexOf(".");
  241.             int indexOf2 = column.lastIndexOf("}");
  242.             if (column.startsWith("#") && indexOf >= 0 && indexOf2 >= 0) {
  243.                 column = column.substring(indexOf + 1, indexOf2);
  244.                 if (StrUtil.isBlank(column)) {
  245.                     return null;
  246.                 }
  247.             }
  248.             JimuReportDataColumnDTO jimuReportDataColumnDTO = new JimuReportDataColumnDTO();
  249.             jimuReportDataColumnDTO.setName(name);
  250.             jimuReportDataColumnDTO.setColumn(column);
  251.             jimuReportDataColumnDTO.setIndex(Integer.parseInt(key));
  252.             return jimuReportDataColumnDTO;
  253.         }).filter(Objects::nonNull).sorted(Comparator.comparing(JimuReportDataColumnDTO::getIndex)).collect(Collectors.toList());
  254.         log.info("【下载中心】获取积木报表的头 reportId:{},heads:{}", reportId, heads);
  255.         return heads;
  256.     }
  257.     /**
  258.      * 从积木请求体中获取请求参数
  259.      *
  260.      * @param json
  261.      * @return
  262.      */
  263.     @Override
  264.     public String getRequestParamFromJson(String json) {
  265.         if (StrUtil.isNotBlank(json)) {
  266.             JSONObject jsonObject = JSONObject.parseObject(json);
  267.             if (jsonObject.containsKey("param")) {
  268.                 return jsonObject.getJSONObject("param").toJSONString();
  269.             }
  270.             return "{}";
  271.         }
  272.         return "{}";
  273.     }
  274. }
复制代码
数据跨库获取工具

服务接口
  1. /**
  2. * @author: humorchen
  3. * date: 2023/12/19
  4. * description: 获取数据服务,直接SQL跨库拿数据
  5. **/
  6. @DS("slave_1")
  7. public interface IReportDataGetService<T> {
  8.     /**
  9.      * 执行SQL返回数据
  10.      *
  11.      * @param sql
  12.      * @return
  13.      */
  14.     JSONObject getOne(String sql);
  15.     /**
  16.      * 执行SQL返回数据,数据封装到类cls对象里
  17.      *
  18.      * @param sql
  19.      * @param cls
  20.      * @return
  21.      */
  22.     T getOne(String sql, Class<T> cls);
  23.     /**
  24.      * 执行SQL返回数据
  25.      *
  26.      * @param sql
  27.      * @return
  28.      */
  29.     JSONArray getList(String sql);
  30.     /**
  31.      * 执行SQL返回数据,数据封装到类cls对象里
  32.      *
  33.      * @param sql
  34.      * @param cls
  35.      * @return
  36.      */
  37.     List<T> getList(String sql, Class<T> cls);
  38.     /**
  39.      * 分页查询
  40.      *
  41.      * @param sql
  42.      * @param page
  43.      * @param pageSize
  44.      * @return
  45.      */
  46.     JSONArray pageGetList(String sql, int page, int pageSize);
  47.     /**
  48.      * 分页查询
  49.      *
  50.      * @param sql
  51.      * @param page
  52.      * @param pageSize
  53.      * @return
  54.      */
  55.     JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize);
  56.     /**
  57.      * 分页查询
  58.      *
  59.      * @param sql
  60.      * @param page
  61.      * @param pageSize
  62.      * @param cls
  63.      * @return
  64.      */
  65.     JimuPageDto<T> pageGetListForJimu(String sql, int page, int pageSize, Class<T> cls);
  66.     /**
  67.      * 计数
  68.      *
  69.      * @param sql
  70.      * @return
  71.      */
  72.     long count(String sql);
  73.     /**
  74.      * 生成in语句
  75.      *
  76.      * @param columnName
  77.      * @param elements
  78.      * @return string
  79.      */
  80.     default String getColumnInSql(String columnName, List<String> elements) {
  81.         StringBuilder sqlBuilder = new StringBuilder();
  82.         sqlBuilder.append(" ");
  83.         sqlBuilder.append(columnName);
  84.         sqlBuilder.append(" in (");
  85.         for (int i = 0; i < elements.size(); i++) {
  86.             String id = elements.get(i);
  87.             if (i > 0) {
  88.                 sqlBuilder.append(",");
  89.             }
  90.             sqlBuilder.append("'");
  91.             sqlBuilder.append(id);
  92.             sqlBuilder.append("'");
  93.         }
  94.         sqlBuilder.append(")");
  95.         return sqlBuilder.toString();
  96.     }
  97. }
复制代码
服务实现
  1. /**
  2. * @author: chenfuxing
  3. * date: 2023/12/19
  4. * description:
  5. **/
  6. @Service
  7. @Slf4j
  8. public class ReportDataGetServiceImpl implements IReportDataGetService {
  9.     @Autowired
  10.     private DataSource dataSource;
  11.     /**
  12.      * 执行SQL返回数据
  13.      *
  14.      * @param sql
  15.      * @return
  16.      */
  17.     @Override
  18.     public JSONObject getOne(String sql) {
  19.         JSONObject ret = null;
  20.         Connection connection = null;
  21.         Statement statement = null;
  22.         ResultSet resultSet = null;
  23.         try {
  24.             connection = dataSource.getConnection();
  25.             statement = connection.createStatement();
  26.             logSql(sql);
  27.             resultSet = statement.executeQuery(sql);
  28.             if (resultSet != null) {
  29.                 while (resultSet.next()) {
  30.                     if (ret != null) {
  31.                         throw new RuntimeException("查询结果不止一条数据");
  32.                     }
  33.                     ret = new JSONObject();
  34.                     ResultSetMetaData metaData = resultSet.getMetaData();
  35.                     int columnCount = metaData.getColumnCount();
  36.                     for (int i = 1; i <= columnCount; i++) {
  37.                         String columnName = metaData.getColumnLabel(i);
  38.                         ret.put(columnName, resultSet.getObject(columnName));
  39.                     }
  40.                 }
  41.             }
  42.         } catch (Exception e) {
  43.             log.error("获取数据报错", e);
  44.         } finally {
  45.             // 释放资源
  46.             IoUtil.close(resultSet);
  47.             IoUtil.close(statement);
  48.             IoUtil.close(connection);
  49.         }
  50.         return ret;
  51.     }
  52.     /**
  53.      * 执行SQL返回数据
  54.      *
  55.      * @param sql
  56.      * @return
  57.      */
  58.     @Override
  59.     public JSONArray getList(String sql) {
  60.         JSONArray ret = new JSONArray();
  61.         Connection connection = null;
  62.         Statement statement = null;
  63.         ResultSet resultSet = null;
  64.         try {
  65.             connection = dataSource.getConnection();
  66.             statement = connection.createStatement();
  67.             logSql(sql);
  68.             resultSet = statement.executeQuery(sql);
  69.             if (resultSet != null) {
  70.                 while (resultSet.next()) {
  71.                     // 组装数据为json 对象
  72.                     JSONObject data = new JSONObject();
  73.                     ResultSetMetaData metaData = resultSet.getMetaData();
  74.                     int columnCount = metaData.getColumnCount();
  75.                     for (int i = 1; i <= columnCount; i++) {
  76.                         String columnName = metaData.getColumnLabel(i);
  77.                         data.put(columnName, resultSet.getObject(columnName));
  78.                     }
  79.                     ret.add(data);
  80.                 }
  81.             }
  82.         } catch (Exception e) {
  83.             log.error("获取数据报错", e);
  84.         } finally {
  85.             // 释放资源
  86.             IoUtil.close(resultSet);
  87.             IoUtil.close(statement);
  88.             IoUtil.close(connection);
  89.         }
  90.         return ret;
  91.     }
  92.     private void logSql(String sql) {
  93.         int len = 5000;
  94.         // 执行SQL
  95.         log.info("执行的SQL:{}", StrUtil.isNotBlank(sql) && sql.length() > len ? sql.substring(0, len) : sql);
  96.     }
  97.     /**
  98.      * 计数
  99.      *
  100.      * @param sql
  101.      * @return
  102.      */
  103.     @Override
  104.     public long count(String sql) {
  105.         String countSQL = getCountSqlFromQuerySql(sql);
  106.         if (StrUtil.isBlank(countSQL)) {
  107.             throw new RuntimeException("计数语句不得为空,SQL为:" + sql);
  108.         }
  109.         long ret = 0;
  110.         Connection connection = null;
  111.         Statement statement = null;
  112.         ResultSet resultSet = null;
  113.         try {
  114.             connection = dataSource.getConnection();
  115.             statement = connection.createStatement();
  116.             logSql(sql);
  117.             resultSet = statement.executeQuery(countSQL);
  118.             if (resultSet != null) {
  119.                 while (resultSet.next()) {
  120.                     ret = resultSet.getLong(1);
  121.                 }
  122.             }
  123.         } catch (Exception e) {
  124.             log.error("获取数据报错", e);
  125.         } finally {
  126.             // 释放资源
  127.             if (resultSet != null) {
  128.                 try {
  129.                     resultSet.close();
  130.                 } catch (Exception ignored) {
  131.                 }
  132.             }
  133.             if (statement != null) {
  134.                 try {
  135.                     statement.close();
  136.                 } catch (Exception ignored) {
  137.                 }
  138.             }
  139.             if (connection != null) {
  140.                 try {
  141.                     connection.close();
  142.                 } catch (Exception ignored) {
  143.                 }
  144.             }
  145.         }
  146.         return ret;
  147.     }
  148.     /**
  149.      * 从查询语句变计数语句
  150.      *
  151.      * @param sql
  152.      * @return
  153.      */
  154.     public String getCountSqlFromQuerySql(String sql) {
  155.         String selectStr = "select";
  156.         int selectIndex = sql.indexOf(selectStr);
  157.         int fromIndex = sql.indexOf("from");
  158.         return sql.replace(sql.substring(selectIndex + selectStr.length(), fromIndex), " count(*) as c ");
  159.     }
  160.     /**
  161.      * 分页查询
  162.      *
  163.      * @param sql
  164.      * @param page
  165.      * @param pageSize
  166.      * @return
  167.      */
  168.     @Override
  169.     public JSONArray pageGetList(String sql, int page, int pageSize) {
  170.         String querySql = getPageSqlFromQuerySql(sql, page, pageSize);
  171.         if (StrUtil.isBlank(querySql)) {
  172.             throw new RuntimeException("分页查询解析失败,SQL:" + sql + " 页号: " + page + " 每页数量:" + pageSize);
  173.         }
  174.         return getList(querySql);
  175.     }
  176.     /**
  177.      * 分页查询
  178.      *
  179.      * @param sql
  180.      * @param page
  181.      * @param pageSize
  182.      * @return
  183.      */
  184.     @Override
  185.     public JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize) {
  186.         JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
  187.         // 查count
  188.         long count = count(sql);
  189.         long total = count / pageSize + (count % pageSize > 0 ? 1 : 0);
  190.         log.info("数据总条数:{} 条,每页:{} 条,总页数:{} 页", count, pageSize, total);
  191.         jimuPageDto.setTotal(total);
  192.         // 查分页数据
  193.         JSONArray data = pageGetList(sql, page, pageSize);
  194.         List<JSONObject> dataList = new ArrayList<>(data.size());
  195.         for (int i = 0; i < data.size(); i++) {
  196.             JSONObject jsonObject = data.getJSONObject(i);
  197.             dataList.add(jsonObject);
  198.         }
  199.         jimuPageDto.setData(dataList);
  200.         jimuPageDto.setCount(count);
  201.         return jimuPageDto;
  202.     }
  203.     /**
  204.      * 分页查询
  205.      *
  206.      * @param sql
  207.      * @param page
  208.      * @param pageSize
  209.      * @return
  210.      */
  211.     public String getPageSqlFromQuerySql(String sql, int page, int pageSize) {
  212.         Assert.isTrue(page >= 1, () -> new IllegalArgumentException("page不得小于1"));
  213.         Assert.isTrue(pageSize >= 1, () -> new IllegalArgumentException("pageSize不得小于1"));
  214.         int skip = (page - 1) * pageSize;
  215.         StringBuilder builder = new StringBuilder(sql);
  216.         builder.append(" limit ");
  217.         if (skip > 0) {
  218.             builder.append(skip);
  219.             builder.append(",");
  220.         }
  221.         builder.append(pageSize);
  222.         String querySql = builder.toString();
  223.         log.info("分页查询原SQL:{}\n分页SQL处理后:{}", sql, querySql);
  224.         return querySql;
  225.     }
  226.     /**
  227.      * 执行SQL返回数据,数据封装到类cls对象里
  228.      *
  229.      * @param sql
  230.      * @param cls
  231.      * @return
  232.      */
  233.     @Override
  234.     public Object getOne(String sql, Class cls) {
  235.         return getOne(sql).toJavaObject(cls);
  236.     }
  237.     /**
  238.      * 执行SQL返回数据,数据封装到类cls对象里
  239.      *
  240.      * @param sql
  241.      * @param cls
  242.      * @return
  243.      */
  244.     @Override
  245.     public List getList(String sql, Class cls) {
  246.         return getList(sql).toJavaList(cls);
  247.     }
  248.     /**
  249.      * 分页查询
  250.      *
  251.      * @param sql
  252.      * @param page
  253.      * @param pageSize
  254.      * @param cls
  255.      * @return
  256.      */
  257.     @Override
  258.     public JimuPageDto pageGetListForJimu(String sql, int page, int pageSize, Class cls) {
  259.         JimuPageDto<JSONObject> jimuPageDto = pageGetListForJimu(sql, page, pageSize);
  260.         JimuPageDto ret = new JimuPageDto<>();
  261.         List list = new ArrayList(jimuPageDto.getData().size());
  262.         for (int i = 0; i < jimuPageDto.getData().size(); i++) {
  263.             list.add(jimuPageDto.getData().get(i).toJavaObject(cls));
  264.         }
  265.         ret.setData(list);
  266.         ret.setTotal(jimuPageDto.getTotal());
  267.         ret.setCount(jimuPageDto.getCount());
  268.         return ret;
  269.     }
  270. }
复制代码
拦截切面

  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/15
  4. * description: 下载任务切面
  5. * 对加上了@UseDownloadTaskCenter注解的方法进行切面,使用下载任务中心代理掉,完成下载任务
  6. **/
  7. @Aspect
  8. @Component
  9. @Slf4j
  10. public class DownloadTaskAspect {
  11.     @Autowired
  12.     private IDownloadTaskService downloadTaskService;
  13.     /**
  14.      * 注入spring 事件发布器
  15.      */
  16.     @Autowired
  17.     private ApplicationEventPublisher eventPublisher;
  18.     /**
  19.      * 环绕通知
  20.      *
  21.      * @return
  22.      */
  23.     @Around("@annotation(cn.sffix.recovery.report.annotations.UseDownloadTaskCenter))")
  24.     @Order(50)
  25.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  26.         log.info("【下载中心】进入下载中心切面");
  27.         // 是下载中心发的请求则直接执行分页数据
  28.         if (DownloadCenterUtil.isDownloadCenterRequest()) {
  29.             log.info("【下载中心】下载中心发的请求,直接执行分页数据");
  30.             return joinPoint.proceed();
  31.         }
  32.         // 识别下载请求
  33.         int pageNo = 1;
  34.         int pageSize = 20;
  35.         Object[] args = joinPoint.getArgs();
  36.         if (args != null && args.length > 0) {
  37.             DownloadCenterBaseParam downloadCenterBaseParam = null;
  38.             // 找到参数
  39.             for (Object arg : args) {
  40.                 if (arg instanceof DownloadCenterBaseParam) {
  41.                     downloadCenterBaseParam = (DownloadCenterBaseParam) arg;
  42.                     break;
  43.                 }
  44.             }
  45.             // 检查参数
  46.             if (downloadCenterBaseParam != null) {
  47.                 pageNo = Optional.ofNullable(downloadCenterBaseParam.getPageNo()).orElse(pageNo);
  48.                 pageSize = Optional.ofNullable(downloadCenterBaseParam.getPageSize()).orElse(pageSize);
  49.             }
  50.             log.info("【下载中心】下载中心切面,downloadCenterBaseParam:{}", downloadCenterBaseParam);
  51.             if (downloadCenterBaseParam != null) {
  52.                 Object target = joinPoint.getTarget();
  53.                 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
  54.                 Class<?> returnType = method.getReturnType();
  55.                 // 返回值类型检查
  56.                 if (returnType.equals(JimuPageDto.class)) {
  57.                     // 如果是导出请求,则使用下载任务中心代理掉
  58.                     if (isExportFirstPageRequest(pageNo, pageSize)) {
  59.                         // 如果是导出第一页请求,则使用下载任务中心代理掉
  60.                         DownloadTask downloadTask = registerTask(downloadCenterBaseParam, target, method, args);
  61.                         if (downloadTask == null || downloadTask.getId() == null) {
  62.                             log.error("【下载中心】注册下载任务失败,任务信息:{}", downloadTask);
  63.                             return joinPoint.proceed();
  64.                         }
  65.                         log.info("【下载中心】注册下载任务成功,任务信息:{}", downloadTask);
  66.                         // 返回积木所需要的数据
  67.                         JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
  68.                         jimuPageDto.setTotal(0);
  69.                         jimuPageDto.setCount(0);
  70.                         JSONObject jsonObject = new JSONObject();
  71.                         String downloadTaskJsonStr = downloadTask.getJson();
  72.                         DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTaskJsonStr, DownloadTaskJson.class);
  73.                         String requestBody = downloadTaskJson.getRequestBody();
  74.                         String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
  75.                         List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
  76.                         log.info("【下载中心】reportHead:{}", reportHead);
  77.                         if (CollectionUtil.isNotEmpty(reportHead) && reportHead.size() > 1) {
  78.                             String column = reportHead.get(1).getColumn();
  79.                             jsonObject.put(column, "请前往报表中台-下载中心查看(任务ID " + downloadTask.getId() + ")");
  80.                             log.info("【下载中心】返回数据:{}", jsonObject);
  81.                         } else {
  82.                             log.info("【下载中心】返回数据为空");
  83.                         }
  84.                         List<JSONObject> list = Collections.singletonList(jsonObject);
  85.                         jimuPageDto.setData(list);
  86.                         eventPublisher.publishEvent(new DownloadTaskPublishEvent(downloadTask.getId()));
  87.                         return jimuPageDto;
  88.                     } else {
  89.                         log.info("【下载中心】不是导出请求,直接执行分页数据");
  90.                     }
  91.                 } else {
  92.                     log.error("【下载中心】返回值类型不是JimuPageDto,无法使用下载任务中心代理掉");
  93.                 }
  94.             }
  95.         }
  96.         return joinPoint.proceed();
  97.     }
  98.     /**
  99.      * 生成下载任务
  100.      *
  101.      * @param downloadTaskParam
  102.      * @return
  103.      */
  104.     private DownloadTask registerTask(DownloadCenterBaseParam downloadTaskParam, Object proxyTarget, Method method, Object[] args) {
  105.         UserInfo loginUser = UserInfoHolder.get();
  106.         String account = loginUser.getAccount();
  107.         HttpServletRequest currentRequest = RequestUtil.getCurrentRequest();
  108.         String requestBody = DownloadCenterUtil.getRequestBodyFromHeader(currentRequest);
  109.         // 防止10秒内重复点击
  110.         if (!downloadTaskService.setSameTaskLock(account, requestBody)) {
  111.             log.error("【下载中心】10秒内重复点击,不给再次注册下载任务");
  112.             return null;
  113.         }
  114.         String title = "导出-" + DateUtil.now().replace(" ", "_") + ".xlsx";
  115.         try {
  116.             title = downloadTaskService.getReportNameByReportId(downloadTaskService.getReportIdFromRequestBody(requestBody)) + title;
  117.         } catch (Exception e) {
  118.             log.error("【下载中心】获取报表名称失败", e);
  119.         }
  120.         String url = generateFileUrl();
  121.         DownloadTask downloadTask = new DownloadTask();
  122.         downloadTask.setAccount(account);
  123.         downloadTask.setTitle(title);
  124.         downloadTask.setIcon("");
  125.         downloadTask.setUrl(url);
  126.         downloadTask.setFileSize("");
  127.         downloadTask.setPercent("0%");
  128.         downloadTask.setState(DownloadTaskStateEnum.WAIT.getState());
  129.         DownloadTaskJson downloadTaskJson = new DownloadTaskJson();
  130.         // 拷贝最开始请求积木的token和requestBody,执行下载任务时需要
  131.         downloadTaskJson.setRequestToken(DownloadCenterUtil.getRequestTokenFromHeader(currentRequest));
  132.         downloadTaskJson.setRequestBody(requestBody);
  133.         downloadTaskJson.setProxyMethod(method.getName());
  134.         if (args != null) {
  135.             for (Object arg : args) {
  136.                 if (arg instanceof DownloadCenterBaseParam) {
  137.                     downloadTaskJson.setParam((DownloadCenterBaseParam) arg);
  138.                     break;
  139.                 }
  140.             }
  141.         }
  142.         downloadTask.setJson(JSONObject.toJSONString(downloadTaskJson));
  143.         downloadTask = downloadTaskService.registerTask(downloadTask);
  144.         return downloadTask;
  145.     }
  146.     /**
  147.      * 生成文件url地址
  148.      *
  149.      * @return
  150.      */
  151.     private String generateFileUrl() {
  152.         // todo 生成文件url地址
  153.         return "";
  154.     }
  155.     /**
  156.      * 注入jm report分页大小
  157.      */
  158.     @Value("${jeecg.jmreport.page-size-number:5000}")
  159.     private int jmReportPageSizeNumber;
  160.     /**
  161.      * 判断是否为导出请求
  162.      *
  163.      * @param pageNo
  164.      * @param pageSize
  165.      * @return
  166.      */
  167.     private boolean isExportPageRequest(int pageNo, int pageSize) {
  168.         return pageSize == jmReportPageSizeNumber;
  169.     }
  170.     /**
  171.      * 判断是否为导出请求
  172.      *
  173.      * @param pageNo
  174.      * @param pageSize
  175.      * @return
  176.      */
  177.     private boolean isExportFirstPageRequest(int pageNo, int pageSize) {
  178.         log.info("【下载中心】判断是否为导出请求 isExportFirstPageRequest pageNo:{},pageSize:{},积木报表导出size:{}", pageNo, pageSize, jmReportPageSizeNumber);
  179.         return pageNo == 1 && isExportPageRequest(pageNo, pageSize);
  180.     }
  181. }
复制代码
报表下载使命处理事件

   暂时由于导出项目只部署一个实例,导出使命使用的是spring 本地event来异步处理的,你可以使用MQ、调理来执行掉使命,可以参考我这个
上传华为云OSS的部分你更换为你的OSS上传操作,这个地方我是复用项目里已有的存储上传服务
内里的异步线程池、环境工具类、积木导出每页大小要设置本身的
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/16
  4. * description: 下载任务发布事件
  5. **/
  6. @Getter
  7. public class DownloadTaskPublishEvent extends ApplicationEvent {
  8.     /**
  9.      * 任务ID
  10.      */
  11.     private final Integer taskId;
  12.     public DownloadTaskPublishEvent(Integer taskId) {
  13.         super(taskId);
  14.         this.taskId = taskId;
  15.     }
  16.    
  17. }
复制代码
处理器
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/16
  4. * description: 处理下载任务发布事件
  5. **/
  6. @Component
  7. @Slf4j
  8. public class DownloadTaskPublishEventHandler implements ApplicationListener<DownloadTaskPublishEvent> {
  9.     @Autowired
  10.     private IDownloadTaskService downloadTaskService;
  11.     @Autowired
  12.     private IReportService reportService;
  13.     @Autowired
  14.     @Qualifier(AsyncConfig.ASYNC_THREAD_POOL)
  15.     private Executor executor;
  16.     @Autowired
  17.     private HwProperties hwProperties;
  18.     @Autowired
  19.     private EnvUtil envUtil;
  20.     /**
  21.      * 注入jm report分页大小
  22.      */
  23.     @Value("${jeecg.jmreport.page-size-number:5000}")
  24.     private int jmReportPageSizeNumber;
  25.     @Autowired
  26.     private RedissonClient redissonClient;
  27.     /**
  28.      * 超时时间
  29.      */
  30.     private static final int TIMEOUT_MILLS = 1000 * 60 * 20;
  31.     @Autowired
  32.     private HwObjectStorageService hwObjectStorageService;
  33.     /**
  34.      * 上传文件到OSS的路径
  35.      *
  36.      * @param account
  37.      * @param fileName
  38.      * @return
  39.      */
  40.     private final StoragePath getStoragePath(String account, String fileName) {
  41.         return StoragePath.path().addFolder("downloadTask").addFolder(account).fileName(fileName);
  42.     }
  43.     /**
  44.      * Handle an application event.
  45.      *
  46.      * @param event the event to respond to
  47.      */
  48.     @Override
  49.     @Async(AsyncConfig.ASYNC_THREAD_POOL)
  50.     public void onApplicationEvent(DownloadTaskPublishEvent event) {
  51.         Integer taskId = event.getTaskId();
  52.         log.info("【下载中心】执行下载任务 taskId:{}", taskId);
  53.         DownloadTask downloadTask = downloadTaskService.getById(taskId);
  54.         if (downloadTask == null) {
  55.             log.error("【下载中心】下载任务不存在,taskId:{}", taskId);
  56.             return;
  57.         }
  58.         if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
  59.             log.error("【下载中心】下载任务正在执行中,taskId:{}", taskId);
  60.             return;
  61.         }
  62.         try {
  63.             log.info("【下载中心】下载任务开始执行,taskId:{}", taskId);
  64.             // 改状态到执行中
  65.             DownloadTaskStateEnum downloadTaskStateEnum = Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT);
  66.             int compareAndSwapTaskState = downloadTaskService.compareAndSwapTaskState(taskId, downloadTaskStateEnum, DownloadTaskStateEnum.RUNNING);
  67.             if (compareAndSwapTaskState < 1) {
  68.                 log.info("【下载中心】下载任务状态不对,taskId:{}, state:{}", taskId, downloadTaskStateEnum);
  69.                 return;
  70.             }
  71.             DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTask.getJson(), DownloadTaskJson.class);
  72.             // 获取数据
  73.             String requestBody = downloadTaskJson.getRequestBody();
  74.             String requestToken = downloadTaskJson.getRequestToken();
  75.             String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
  76.             String reportName = downloadTaskService.getReportNameByReportId(reportId);
  77.             String requestParam = downloadTaskService.getRequestParamFromJson(downloadTask.getJson());
  78.             JimuReportDataSourceDTO dataSourceDTO = downloadTaskService.getReportApiOrSqlByReportId(reportId);
  79.             List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
  80.             // 打印上面拿到的数据
  81.             log.info("reportId :{} \n reportName:{} \n requestParam:{} \n requestBody:{}  \n dataSourceDTO:{} \n reportHead:{}", reportId, reportName, requestParam, requestBody, dataSourceDTO, reportHead);
  82.             JimuReportDynamicEasyExcelImpl jimuReportDynamicEasyExcel = new JimuReportDynamicEasyExcelImpl(reportId, reportName, taskId, downloadTaskService, requestParam, requestToken, dataSourceDTO, reportHead);
  83.             // 生成excel文件
  84.             List<List<String>> head = reportHead.stream().map(d -> Collections.singletonList(d.getName())).collect(Collectors.toList());
  85.             // 分页写数据
  86.             InputStream inputStream = DynamicColumnEasyExcelUtil.writePageData(head, jimuReportDynamicEasyExcel, jmReportPageSizeNumber);
  87.             // 上传excel文件到oss
  88.             StoragePath storagePath = getStoragePath(downloadTask.getAccount(), downloadTask.getTitle());
  89.             downloadTask.setPercent("100%");
  90.             StopWatch stopWatch = new StopWatch();
  91.             stopWatch.start();
  92.             URI uri = hwObjectStorageService.savePublicFile(inputStream, storagePath);
  93.             stopWatch.stop();
  94.             log.info("【下载中心】上传文件到OSS,耗时:{} ms,uri:{}", stopWatch.getLastTaskTimeMillis(), uri);
  95.             // 更新任务信息
  96.             String url = getUrlPrefix() + uri.getPath();
  97.             downloadTask.setUrl(url);
  98.             downloadTask.setState(DownloadTaskStateEnum.SUCCESS.getState());
  99.             log.info("【下载中心】下载任务成功,taskId:{},task:{}", taskId, downloadTask);
  100.             boolean updated = downloadTaskService.updateById(downloadTask);
  101.             log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
  102.         } catch (Exception e) {
  103.             log.error("【下载中心】下载任务执行失败", e);
  104.             // 更新任务信息
  105.             downloadTask.setState(DownloadTaskStateEnum.FAILED.getState());
  106.             downloadTask.setError("【下载中心】执行失败(" + e.getMessage() + ")");
  107.             boolean updated = downloadTaskService.updateById(downloadTask);
  108.             log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
  109.         } finally {
  110.             log.info("【下载中心】下载任务 {} 执行完毕", taskId);
  111.         }
  112.     }
  113.     /**
  114.      * 根据环境获取文件url前缀
  115.      *
  116.      * @return
  117.      */
  118.     private String getUrlPrefix() {
  119.         String envCode = envUtil.isPreOrProdEnv() ? "pro" : "test";
  120.         String prefix = "https://test-obs.xxxxx.com";
  121.         for (String key : hwProperties.getUrlMap().keySet()) {
  122.             if (key.contains(envCode)) {
  123.                 prefix = hwProperties.getUrlMap().get(key);
  124.             }
  125.         }
  126.         return prefix;
  127.     }
  128. }
复制代码
透传信息

紧张是在这个customApiHeader() 方法里DownloadCenterUtil.copyDownloadCenterHeader(request, header);自定义获取数据哀求的header这,读取这次积木导出哀求的参数信息,写到获取数据哀求的header里去
  1. /**
  2. * 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)
  3. * 1.自定义获取登录token
  4. * 2.自定义获取登录用户
  5. */
  6. @Slf4j
  7. @Component
  8. public class JimuReportTokenService implements JmReportTokenServiceI {
  9.     @Autowired
  10.     @Lazy
  11.     private UserAutenticationFeign userAutenticationFeign;
  12.     @Autowired
  13.     @Lazy
  14.     private UserInfoFeign userInfoFeign;
  15.     /**
  16.      * 通过请求获取Token
  17.      * @param request
  18.      * @return
  19.      */
  20.     @Override
  21.     public String getToken(HttpServletRequest request) {
  22.         String token = request.getParameter("token");
  23.         if (token == null) {
  24.             token = request.getHeader("X-Access-Token");
  25.         }
  26.         if (token == null) {
  27.             token = request.getHeader("token");
  28.         }
  29.         if (token == null) {
  30.             token = request.getHeader("Token");
  31.         }
  32.         return token;
  33.     }
  34.     /**
  35.      * 通过Token获取登录人用户名
  36.      * @param token
  37.      * @return
  38.      */
  39.     @Override
  40.     public String getUsername(String token) {
  41.         UserTokenDTO tokenInfo = new UserTokenDTO();
  42.         tokenInfo.setToken(token);
  43.         Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);
  44.         CustomerInfoDTO data = customerInfoDTOResult.getData();
  45.         if(data != null){
  46.             FxUserInfoDTO userInfo = JSONObject.parseObject(JSONObject.toJSONString(data.getBase()), FxUserInfoDTO.class);
  47.             if(userInfo == null){
  48.                 throw new RuntimeException("找不到相应平台用户信息");
  49.             } else {
  50.                 // 写到上下文
  51.                 UserInfo user = new UserInfo();
  52.                 BeanUtils.copyProperties(userInfo, user);
  53.                 UserInfoHolder.set(user);
  54.                 log.info("成功将用户信息写入上下文");
  55.             }
  56.             if(userInfo.getClientType() != PlatformTypeEnum.fx.name()){
  57.                 return userInfo.getName();
  58.             }else{
  59.                 throw new RuntimeException("平台类型不支持");
  60.             }
  61.         }else {
  62.             throw new RuntimeException("用户不存在");
  63.         }
  64.     }
  65.     /**
  66.      * 自定义用户拥有的角色
  67.      *
  68.      * @param token
  69.      * @return
  70.      */
  71.     @Override
  72.     public String[] getRoles(String token) {
  73.         return new String[]{"admin"};
  74.     }
  75.     /**
  76.      * Token校验
  77.      * @param token
  78.      * @return
  79.      */
  80.     @Override
  81.     public Boolean verifyToken(String token) {
  82.         ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  83.         HttpServletRequest request = requestAttributes.getRequest();
  84.         request.setAttribute(UserAccountConstant.TOKEN,token);
  85.         try {
  86.             UserTokenDTO tokenInfo = new UserTokenDTO();
  87.             tokenInfo.setToken(token);
  88.             Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);
  89.             return customerInfoDTOResult.getData() != null;
  90.         }catch (Exception e){
  91.             log.error("校验Token异常:" + e.getMessage());
  92.             return false;
  93.         }
  94.     }
  95.     /**
  96.      *  自定义请求头
  97.      * @return
  98.      */
  99.     @Override
  100.     public HttpHeaders customApiHeader() {
  101.         ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  102.         HttpHeaders header = new HttpHeaders();
  103.         header.add("custom-header1", "Please set a custom value 1");
  104.         if (requestAttributes != null) {
  105.             HttpServletRequest request = requestAttributes.getRequest();
  106.             header.set("authorization", getToken(request));
  107.             header.set("token", getToken(request));
  108.             // 拷贝请求过去
  109.             DownloadCenterUtil.copyDownloadCenterHeader(request, header);
  110.             // 如果是下载中心发起的请求,设置请求头
  111.             if (DownloadCenterUtil.isDownloadCenterRequest(requestAttributes.getRequest())) {
  112.                 DownloadCenterUtil.setDownloadCenterHeaderRequest(header);
  113.             }
  114.         }
  115.         return header;
  116.     }
  117. }
复制代码
下载中央工具类
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/18
  4. * description: 下载中心工具类
  5. **/
  6. @Slf4j
  7. public class DownloadCenterUtil {
  8.     private static final String DOWNLOAD_CENTER_HEADER_REQUEST = "download-center-request";
  9.     /**
  10.      * 是否为下载中心发起的请求
  11.      *
  12.      * @return
  13.      */
  14.     public static boolean isDownloadCenterRequest() {
  15.         ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  16.         if (requestAttributes == null) {
  17.             log.debug("DownloadCenterUtil#isDownloadCenterRequest requestAttributes is null");
  18.             return false;
  19.         }
  20.         return isDownloadCenterRequest(requestAttributes.getRequest());
  21.     }
  22.     /**
  23.      * 是否为下载中心发起的请求
  24.      *
  25.      * @param servletRequest
  26.      * @return
  27.      */
  28.     public static boolean isDownloadCenterRequest(HttpServletRequest servletRequest) {
  29.         return servletRequest != null && StrUtil.isNotBlank(servletRequest.getHeader(DOWNLOAD_CENTER_HEADER_REQUEST));
  30.     }
  31.     /**
  32.      * 设置下载中心请求头
  33.      *
  34.      * @param headers
  35.      */
  36.     public static void setDownloadCenterHeaderRequest(HttpHeaders headers) {
  37.         if (headers != null) {
  38.             headers.set(DOWNLOAD_CENTER_HEADER_REQUEST, "true");
  39.         }
  40.     }
  41.     /**
  42.      * 复制下载中心请求头
  43.      *
  44.      * @param request
  45.      * @param headers
  46.      */
  47.     public static void copyDownloadCenterHeader(HttpServletRequest request, HttpHeaders headers) {
  48.         if (request == null || headers == null) {
  49.             return;
  50.         }
  51.         // 复制request请求里的Token请求头
  52.         String token = request.getHeader("Token");
  53.         if (StrUtil.isNotBlank(token)) {
  54.             headers.set(DownloadTaskJson.Fields.requestToken, token);
  55.         }
  56.         // 复制request请求里的请求体
  57.         headers.set(DownloadTaskJson.Fields.requestBody, RequestUtil.getRequestBody(request));
  58.     }
  59.     /**
  60.      * 获取下载请求头token
  61.      *
  62.      * @param request
  63.      * @return
  64.      */
  65.     public static String getRequestTokenFromHeader(HttpServletRequest request) {
  66.         if (request == null) {
  67.             return null;
  68.         }
  69.         return request.getHeader(DownloadTaskJson.Fields.requestToken);
  70.     }
  71.     /**
  72.      * 获取下载请求头
  73.      *
  74.      * @param request
  75.      * @return
  76.      */
  77.     public static String getRequestBodyFromHeader(HttpServletRequest request) {
  78.         if (request == null) {
  79.             return null;
  80.         }
  81.         return request.getHeader(DownloadTaskJson.Fields.requestBody);
  82.     }
  83.     /**
  84.      * 设置下载中心请求头
  85.      *
  86.      * @param request
  87.      */
  88.     public static void setDownloadCenterHeaderRequest(HttpRequest request) {
  89.         if (request != null) {
  90.             request.header(DOWNLOAD_CENTER_HEADER_REQUEST, "true");
  91.         }
  92.     }
  93.     /**
  94.      * 获取带参数的url
  95.      *
  96.      * @param url
  97.      * @param params
  98.      * @return
  99.      */
  100.     public static String getUrlWithParams(String url, JSONObject params) {
  101.         if (StrUtil.isBlank(url) || params == null) {
  102.             return url;
  103.         }
  104.         StringBuilder sb = new StringBuilder(url);
  105.         if (url.contains("?")) {
  106.             sb.append("&");
  107.         } else {
  108.             sb.append("?");
  109.         }
  110.         for (String key : params.keySet()) {
  111.             sb.append(key).append("=").append(params.getString(key)).append("&");
  112.         }
  113.         return sb.substring(0, sb.length() - 1);
  114.     }
  115. }
复制代码
异步线程池
  1. /**
  2. * @author: humorchen
  3. * date: 2024/1/16
  4. * description: 异步配置
  5. **/
  6. @Slf4j
  7. @Configuration
  8. public class AsyncConfig {
  9.     public static final String ASYNC_THREAD_POOL = "asyncThreadPool";
  10.     /**
  11.      * 异步线程池
  12.      */
  13.     @Bean(name = ASYNC_THREAD_POOL)
  14.     public Executor asyncExecutor() {
  15.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  16.         executor.setCorePoolSize(8);
  17.         executor.setMaxPoolSize(64);
  18.         executor.setQueueCapacity(128);
  19.         executor.setThreadNamePrefix("asyncThreadPool-");
  20.         executor.initialize();
  21.         return executor;
  22.     }
  23. }
复制代码
给前端袒露接口

  1. /**
  2. * @author humorchen
  3. * date: 2024/2/28
  4. * description: 下载中心API
  5. **/
  6. @RequestMapping("/report/form/downloadCenter")
  7. public interface DownloadCenterApi {
  8.     /**
  9.      * 分页查下载任务
  10.      *
  11.      * @param pageListDownloadTaskDto
  12.      * @return
  13.      */
  14.     @RequestMapping("/pageListDownloadTask")
  15.     Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);
  16.     /**
  17.      * 删除下载任务
  18.      *
  19.      * @param taskId
  20.      * @return
  21.      */
  22.     @RequestMapping("/deleteTask")
  23.     Result<String> deleteTask(Integer taskId);
  24.     /**
  25.      * 重新执行下载任务
  26.      *
  27.      * @param taskId
  28.      * @return
  29.      */
  30.     @RequestMapping("/rerunTask")
  31.     Result<String> rerunTask(Integer taskId);
  32. }
复制代码
  1. /**
  2. * @author humorchen
  3. * date: 2024/2/28
  4. * description: 下载中心
  5. **/
  6. @RestController
  7. public class DownloadCenterController implements DownloadCenterApi {
  8.     @Autowired
  9.     private IDownloadTaskService downloadTaskService;
  10.     /**
  11.      * 分页查下载任务
  12.      *
  13.      * @param pageListDownloadTaskDto
  14.      * @return
  15.      */
  16.     @Override
  17.     public Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
  18.         IPage<DownloadTask> downloadTaskPages = downloadTaskService.pageListDownloadTask(pageListDownloadTaskDto);
  19.         Page<DownloadTaskVo> downloadTaskVoPage = new Page<>();
  20.         downloadTaskVoPage.setCurrent(downloadTaskPages.getCurrent());
  21.         downloadTaskVoPage.setPages(downloadTaskPages.getPages());
  22.         downloadTaskVoPage.setSize(downloadTaskPages.getSize());
  23.         downloadTaskVoPage.setTotal(downloadTaskPages.getTotal());
  24.         List<DownloadTaskVo> downloadTaskVos = downloadTaskPages.getRecords().stream().map(downloadTask -> {
  25.             DownloadTaskVo downloadTaskVo = BeanUtils.convert(downloadTask, DownloadTaskVo.class);
  26.             downloadTaskVo.setStateStr(Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT).getTitle());
  27.             return downloadTaskVo;
  28.         }).collect(Collectors.toList());
  29.         downloadTaskVoPage.setRecords(downloadTaskVos);
  30.         return Result.ok(downloadTaskVoPage);
  31.     }
  32.     /**
  33.      * 删除下载任务
  34.      *
  35.      * @param taskId
  36.      * @return
  37.      */
  38.     @Override
  39.     public Result<String> deleteTask(Integer taskId) {
  40.         boolean removeById = downloadTaskService.removeById(taskId);
  41.         return removeById ? Result.ok("删除成功") : Result.fail("未找到该任务,请刷新后重试)");
  42.     }
  43.     /**
  44.      * 重新执行下载任务
  45.      *
  46.      * @param taskId
  47.      * @return
  48.      */
  49.     @Override
  50.     public Result<String> rerunTask(Integer taskId) {
  51.         return downloadTaskService.rerunTask(taskId);
  52.     }
  53. }
复制代码
DynamicColumnEasyExcelUtil 动态字段EasyExcel工具
  1. /**
  2. * @author humorchen
  3. * date: 2024/3/5
  4. * description: 动态列easyexcel工具类
  5. **/
  6. @Slf4j
  7. public class DynamicColumnEasyExcelUtil {
  8.     public static final String ROW = "=row()";
  9.     public static interface DynamicColumnEasyExcelInterface<T> {
  10.         /**
  11.          * 分页获取数据
  12.          *
  13.          * @param page
  14.          * @param size
  15.          * @return
  16.          */
  17.         JimuPageDto<T> pageGetData(int page, int size);
  18.         /**
  19.          * 数据对象转换为字符串
  20.          *
  21.          * @param t
  22.          * @return
  23.          */
  24.         List<String> mapDataToStringList(T t);
  25.         /**
  26.          * 分页获取数据加载第i页时触发函数,用于实现进度变更
  27.          *
  28.          * @param pageNo
  29.          * @param pageSize
  30.          */
  31.         void onLoadedPage(int pageNo, int pageSize, int pages);
  32.     }
  33. /**
  34.      * 从数据库分页读数据并写入成Excel文件,把文件内容写到输出流
  35.      *
  36.      * @param head
  37.      * @param dynamicColumnEasyExcelInterface
  38.      * @param pageSize
  39.      * @param <T>
  40.      * @return
  41.      */
  42.     public static <T> ByteArrayInputStream writePageData(List<List<String>> head, DynamicColumnEasyExcelInterface<T> dynamicColumnEasyExcelInterface, int pageSize) {
  43.         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  44.         ExcelWriter excelWriter = EasyExcel.write(outputStream).head(head).build();
  45.         // 当前分页
  46.         int currentPage = 1;
  47.         // 总页数
  48.         long pages = 1;
  49.         // 当前写入的sheet页
  50.         int sheetNo = 1;
  51.         // 写入计数(用于自动翻sheet页)
  52.         int writeRowCount = 0;
  53.         // 积木序号编号
  54.         int index = 1;
  55.         StopWatch stopWatch = new StopWatch("报表分页写入Excel");
  56.         WriteSheet sheet = getWriteSheet(sheetNo);
  57.         do {
  58.             // 加载数据
  59.             stopWatch.start("加载第" + currentPage + "页数据");
  60.             JimuPageDto<T> jimuPageDto = dynamicColumnEasyExcelInterface.pageGetData(currentPage, pageSize);
  61.             stopWatch.stop();
  62.             // 数据判空
  63.             List<T> records = jimuPageDto.getData();
  64.             if (CollectionUtil.isEmpty(records)) {
  65.                 break;
  66.             }
  67.             // 转换数据
  68.             stopWatch.start("转换第" + currentPage + "页数据");
  69.             List<List<String>> data = records.stream().map(dynamicColumnEasyExcelInterface::mapDataToStringList).collect(Collectors.toList());
  70.             stopWatch.stop();
  71.             // 处理序号 row()
  72.             if (CollectionUtil.isNotEmpty(data) && CollectionUtil.isNotEmpty(data.get(0)) && ROW.equals(data.get(0).get(0))) {
  73.                 for (List<String> stringList : data) {
  74.                     if (CollectionUtil.isNotEmpty(stringList) && ROW.equals(stringList.get(0))) {
  75.                         stringList.set(0, String.valueOf(index));
  76.                         ++index;
  77.                     }
  78.                 }
  79.             }
  80.             // 自动跳sheet页
  81.             if (writeRowCount + data.size() >= MAX_SIZE_PER_SHEET) {
  82.                 ++sheetNo;
  83.                 writeRowCount = 0;
  84.                 index = 1;
  85.                 sheet = getWriteSheet(sheetNo);
  86.             }
  87.             // 写入数据
  88.             stopWatch.start("写入第" + currentPage + "页数据(" + data.size() + "条数据)");
  89.             excelWriter.write(data, sheet);
  90.             stopWatch.stop();
  91.             pages = jimuPageDto.getTotal();
  92.             // 更新进度
  93.             dynamicColumnEasyExcelInterface.onLoadedPage(currentPage, pageSize, (int) pages);
  94.             log.info("【下载中心】 分页获取数据,第{}页,总页数:{} 第一行数据是:{}", currentPage, pages, data.get(0));
  95.             // 自增
  96.             currentPage++;
  97.             writeRowCount += data.size();
  98.         } while (currentPage <= pages);
  99.         log.info("【下载中心】 耗时打印");
  100.         for (StopWatch.TaskInfo taskInfo : stopWatch.getTaskInfo()) {
  101.             log.info("【下载中心】 {} 耗时:{} ms", taskInfo.getTaskName(), taskInfo.getTimeMillis());
  102.         }
  103.         excelWriter.finish();
  104.         return new ByteArrayInputStream(outputStream.toByteArray());
  105.     }
  106.     /**
  107.      * 生成WriteSheet对象
  108.      *
  109.      * @param sheetNo 1开始
  110.      * @return WriteSheet
  111.      */
  112.     private static WriteSheet getWriteSheet(int sheetNo) {
  113.         WriteSheet sheet = new WriteSheet();
  114.         sheet.setSheetName("sheet" + sheetNo);
  115.         sheet.setSheetNo(sheetNo - 1);
  116.         return sheet;
  117.     }
  118.     /**
  119.      * 获取字段宽度策略
  120.      *
  121.      * @return
  122.      */
  123.     private AbstractHeadColumnWidthStyleStrategy getAbstractColumnWidthStyleStrategy() {
  124.         return new AbstractHeadColumnWidthStyleStrategy() {
  125.             /**
  126.              * Returns the column width corresponding to each column head.
  127.              *
  128.              * <p>
  129.              * if return null, ignore
  130.              *
  131.              * @param head        Nullable.
  132.              * @param columnIndex Not null.
  133.              * @return
  134.              */
  135.             @Override
  136.             protected Integer columnWidth(Head head, Integer columnIndex) {
  137.                 return null;
  138.             }
  139.         };
  140.     }
  141. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表