积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)

文章目录

  • 积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)
    • 优化结果
    • 需求背景和解决方案的思考
    • 解决方案
      • 流程描述:
      • 关键代码
        • 引入easy excel
        • 新建数据库表
        • 识别所需注解、基类
        • 下载中心服务(报表下载的导出任务)
        • 数据跨库获取工具
        • 拦截切面
        • 报表下载任务处理事件
        • 透传信息
        • 给前端暴露接口

积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)

优化结果

原积木导出有两种导出,直接导出和大数据导出(大数据导出是做了优化去掉了一些样式之类的,性能更好)
实测中发现
原积木大数据导出性能:1万条数据导出耗时30秒,1.5万条耗时1.5分钟导出失败,数据超过一万条后经常导出失败,还会导致容器实例探活失败/内存撑爆重启
异步+自实现导出功能优化方案导出性能:34.6万条数据一次导出耗时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文件的二进制流)

解决方案

流程描述:

识别并拦截积木导出操作,透传导出时参数生成自己设计的导出任务,异步执行导出任务(自行实现积木导出的逻辑),并使用高性能的阿里巴巴开源的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='下载中心的任务';

下载任务


/*** <p>* 下载任务* </p>** @author humorchen* @since 2024-01-05*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_download_task")
public class DownloadTask implements Serializable {private static final long serialVersionUID = 1L;/*** ID*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 创建下载任务的账号*/@TableField("account")private String account;/*** 下载任务标题*/@TableField("title")private String title;/*** 图标*/@TableField("icon")private String icon;/*** 文件URL*/@TableField("url")private String url;/*** 文件大小*/@TableField("file_size")private String fileSize;/*** 进度(例如50%)*/@TableField("percent")private String percent;/*** 任务状态(0 等待执行,1执行中,2执行成功,3执行失败)*/@TableField("state")private Integer state;/*** 执行报错信息(有则填)*/@TableField("error")private String error;/*** 预留的json扩展字段*/@TableField("json")private String json;/*** 创建时间*/@TableField("create_time")private Date createTime;/*** 更新时间*/@TableField("update_time")private Date updateTime;public static final String ID = "id";public static final String ACCOUNT = "account";public static final String TITLE = "title";public static final String ICON = "icon";public static final String URL = "url";public static final String FILE_SIZE = "file_size";public static final String PERCENT = "percent";public static final String STATE = "state";public static final String ERROR = "error";public static final String JSON = "json";public static final String CREATE_TIME = "create_time";public static final String UPDATE_TIME = "update_time";}

使用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@UseDownloadTaskCenterpublic 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*/
public interface IDownloadTaskService extends IService<DownloadTask> {/*** 注册任务** @param downloadTask* @return*/DownloadTask registerTask(@NonNull DownloadTask downloadTask);/*** 10秒内是否有相同任务未完成,不给再次注册下载任务** @param account* @param requestBody* @return*/boolean setSameTaskLock(String account, String requestBody);/*** 更新任务** @param downloadTask* @return*/int updateTaskById(@NonNull DownloadTask downloadTask);/*** 更新任务进度** @param id* @param percent* @return*/int changeTaskPercent(int id, @NonNull String percent);/*** 更新任务状态** @param id* @param state* @return*/int changeTaskState(int id, @NonNull DownloadTaskStateEnum state);/*** 更新任务状态** @param id* @param expectState* @param targetState* @return*/int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState);/*** 根据任务ID获取任务** @param id* @return*/DownloadTask getDownloadTaskById(int id);/*** 分页查下载任务** @param pageListDownloadTaskDto* @return*/IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);/*** 重新执行下载任务** @param taskId* @return*/@RequestMapping("/rerunTask")Result<String> rerunTask(Integer taskId);/*** 根据报表ID获取报表名称** @param reportId* @return*/String getReportNameByReportId(String reportId);/*** 从请求体中获取报表ID** @param requestBody* @return*/String getReportIdFromRequestBody(String requestBody);/*** 根据报表ID获取报表API地址或者SQL** @param reportId* @return*/JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId);/*** 获取积木报表的头** @param reportId* @return*/List<JimuReportDataColumnDTO> getReportHead(String reportId);/*** 从积木请求体中获取请求参数** @param json* @return*/String getRequestParamFromJson(String json);
}

实现类

/*** <p>* 下载任务 服务实现类* </p>** @author humorchen* @since 2024-01-05*/
@Service
@Slf4j
public class DownloadTaskServiceImpl extends ServiceImpl<DownloadTaskMapper, DownloadTask> implements IDownloadTaskService {@Autowiredprivate DownloadTaskMapper downloadTaskMapper;@Autowiredprivate IReportDataGetService reportDataGetService;@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 注入spring 事件发布器*/@Autowiredprivate ApplicationEventPublisher eventPublisher;/*** 注册任务** @param downloadTask* @return*/@Overridepublic DownloadTask registerTask(@NonNull DownloadTask downloadTask) {downloadTaskMapper.insert(downloadTask);return downloadTask;}/*** 10秒内是否有相同任务未完成,不给再次注册下载任务** @param account* @param requestBody* @return*/@Overridepublic 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*/@Overridepublic int updateTaskById(@NonNull DownloadTask downloadTask) {return downloadTaskMapper.updateById(downloadTask);}/*** 更新任务进度** @param id* @param percent* @return*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic DownloadTask getDownloadTaskById(int id) {return downloadTaskMapper.selectById(id);}/*** 查下载任务** @param pageListDownloadTaskDto* @return*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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() {@Overridepublic void write(int b) throws IOException {}}).head(head).sheet("sheet").doWrite(new ArrayList<>());return jimuReportDataSourceDTO;}/*** 获取积木报表的头** @param reportId* @return*/@Overridepublic 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*/@Overridepublic 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 {@Autowiredprivate DataSource dataSource;/*** 执行SQL返回数据** @param sql* @return*/@Overridepublic 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*/@Overridepublic 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;// 执行SQLlog.info("执行的SQL:{}", StrUtil.isNotBlank(sql) && sql.length() > len ? sql.substring(0, len) : sql);}/*** 计数** @param sql* @return*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize) {JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();// 查countlong 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*/@Overridepublic Object getOne(String sql, Class cls) {return getOne(sql).toJavaObject(cls);}/*** 执行SQL返回数据,数据封装到类cls对象里** @param sql* @param cls* @return*/@Overridepublic List getList(String sql, Class cls) {return getList(sql).toJavaList(cls);}/*** 分页查询** @param sql* @param page* @param pageSize* @param cls* @return*/@Overridepublic 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 {@Autowiredprivate IDownloadTaskService downloadTaskService;/*** 注入spring 事件发布器*/@Autowiredprivate 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> {@Autowiredprivate IDownloadTaskService downloadTaskService;@Autowiredprivate IReportService reportService;@Autowired@Qualifier(AsyncConfig.ASYNC_THREAD_POOL)private Executor executor;@Autowiredprivate HwProperties hwProperties;@Autowiredprivate EnvUtil envUtil;/*** 注入jm report分页大小*/@Value("${jeecg.jmreport.page-size-number:5000}")private int jmReportPageSizeNumber;@Autowiredprivate RedissonClient redissonClient;/*** 超时时间*/private static final int TIMEOUT_MILLS = 1000 * 60 * 20;@Autowiredprivate 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文件到ossStoragePath 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里去

/*** 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)* 1.自定义获取登录token* 2.自定义获取登录用户*/
@Slf4j
@Component
public class JimuReportTokenService implements JmReportTokenServiceI {@Autowired@Lazyprivate UserAutenticationFeign userAutenticationFeign;@Autowired@Lazyprivate UserInfoFeign userInfoFeign;/*** 通过请求获取Token* @param request* @return*/@Overridepublic String getToken(HttpServletRequest request) {String token = request.getParameter("token");if (token == null) {token = request.getHeader("X-Access-Token");}if (token == null) {token = request.getHeader("token");}if (token == null) {token = request.getHeader("Token");}return token;}/*** 通过Token获取登录人用户名* @param token* @return*/@Overridepublic String getUsername(String token) {UserTokenDTO tokenInfo = new UserTokenDTO();tokenInfo.setToken(token);Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);CustomerInfoDTO data = customerInfoDTOResult.getData();if(data != null){FxUserInfoDTO userInfo = JSONObject.parseObject(JSONObject.toJSONString(data.getBase()), FxUserInfoDTO.class);if(userInfo == null){throw new RuntimeException("找不到相应平台用户信息");} else {// 写到上下文UserInfo user = new UserInfo();BeanUtils.copyProperties(userInfo, user);UserInfoHolder.set(user);log.info("成功将用户信息写入上下文");}if(userInfo.getClientType() != PlatformTypeEnum.fx.name()){return userInfo.getName();}else{throw new RuntimeException("平台类型不支持");}}else {throw new RuntimeException("用户不存在");}}/*** 自定义用户拥有的角色** @param token* @return*/@Overridepublic String[] getRoles(String token) {return new String[]{"admin"};}/*** Token校验* @param token* @return*/@Overridepublic Boolean verifyToken(String token) {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = requestAttributes.getRequest();request.setAttribute(UserAccountConstant.TOKEN,token);try {UserTokenDTO tokenInfo = new UserTokenDTO();tokenInfo.setToken(token);Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);return customerInfoDTOResult.getData() != null;}catch (Exception e){log.error("校验Token异常:" + e.getMessage());return false;}}/***  自定义请求头* @return*/@Overridepublic HttpHeaders customApiHeader() {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpHeaders header = new HttpHeaders();header.add("custom-header1", "Please set a custom value 1");if (requestAttributes != null) {HttpServletRequest request = requestAttributes.getRequest();header.set("authorization", getToken(request));header.set("token", getToken(request));// 拷贝请求过去DownloadCenterUtil.copyDownloadCenterHeader(request, header);// 如果是下载中心发起的请求,设置请求头if (DownloadCenterUtil.isDownloadCenterRequest(requestAttributes.getRequest())) {DownloadCenterUtil.setDownloadCenterHeaderRequest(header);}}return header;}
}

下载中心工具类


/*** @author: humorchen* date: 2024/1/18* description: 下载中心工具类**/
@Slf4j
public class DownloadCenterUtil {private static final String DOWNLOAD_CENTER_HEADER_REQUEST = "download-center-request";/*** 是否为下载中心发起的请求** @return*/public static boolean isDownloadCenterRequest() {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes == null) {log.debug("DownloadCenterUtil#isDownloadCenterRequest requestAttributes is null");return false;}return isDownloadCenterRequest(requestAttributes.getRequest());}/*** 是否为下载中心发起的请求** @param servletRequest* @return*/public static boolean isDownloadCenterRequest(HttpServletRequest servletRequest) {return servletRequest != null && StrUtil.isNotBlank(servletRequest.getHeader(DOWNLOAD_CENTER_HEADER_REQUEST));}/*** 设置下载中心请求头** @param headers*/public static void setDownloadCenterHeaderRequest(HttpHeaders headers) {if (headers != null) {headers.set(DOWNLOAD_CENTER_HEADER_REQUEST, "true");}}/*** 复制下载中心请求头** @param request* @param headers*/public static void copyDownloadCenterHeader(HttpServletRequest request, HttpHeaders headers) {if (request == null || headers == null) {return;}// 复制request请求里的Token请求头String token = request.getHeader("Token");if (StrUtil.isNotBlank(token)) {headers.set(DownloadTaskJson.Fields.requestToken, token);}// 复制request请求里的请求体headers.set(DownloadTaskJson.Fields.requestBody, RequestUtil.getRequestBody(request));}/*** 获取下载请求头token** @param request* @return*/public static String getRequestTokenFromHeader(HttpServletRequest request) {if (request == null) {return null;}return request.getHeader(DownloadTaskJson.Fields.requestToken);}/*** 获取下载请求头** @param request* @return*/public static String getRequestBodyFromHeader(HttpServletRequest request) {if (request == null) {return null;}return request.getHeader(DownloadTaskJson.Fields.requestBody);}/*** 设置下载中心请求头** @param request*/public static void setDownloadCenterHeaderRequest(HttpRequest request) {if (request != null) {request.header(DOWNLOAD_CENTER_HEADER_REQUEST, "true");}}/*** 获取带参数的url** @param url* @param params* @return*/public static String getUrlWithParams(String url, JSONObject params) {if (StrUtil.isBlank(url) || params == null) {return url;}StringBuilder sb = new StringBuilder(url);if (url.contains("?")) {sb.append("&");} else {sb.append("?");}for (String key : params.keySet()) {sb.append(key).append("=").append(params.getString(key)).append("&");}return sb.substring(0, sb.length() - 1);}}

异步线程池

/*** @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 {@Autowiredprivate IDownloadTaskService downloadTaskService;/*** 分页查下载任务** @param pageListDownloadTaskDto* @return*/@Overridepublic 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*/@Overridepublic Result<String> deleteTask(Integer taskId) {boolean removeById = downloadTaskService.removeById(taskId);return removeById ? Result.ok("删除成功") : Result.fail("未找到该任务,请刷新后重试)");}/*** 重新执行下载任务** @param taskId* @return*/@Overridepublic 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;int index = 1;StopWatch stopWatch = new StopWatch("报表分页写入Excel");WriteSheet sheet = EasyExcel.writerSheet("sheet" + currentPage).build();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;}}}// 写入数据stopWatch.start("写入第" + currentPage + "页数据");excelWriter.write(data, sheet);stopWatch.stop();pages = jimuPageDto.getTotal();// 更新进度dynamicColumnEasyExcelInterface.onLoadedPage(currentPage, pageSize, (int) pages);log.info("【下载中心】 分页获取数据,第{}页,总页数:{} 第一行数据是:{}", currentPage, pages, data.get(0));// 自增currentPage++;} 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());}/*** 获取字段宽度策略** @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*/@Overrideprotected Integer columnWidth(Head head, Integer columnIndex) {return null;}};}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799238.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux云计算之Linux基础3——Linux系统基础2

1、终端 终端(terminal)&#xff1a;人和系统交互的必要设备&#xff0c;人机交互最后一个界面&#xff08;包含独立的输入输出设备&#xff09; 物理终端(console)&#xff1a;直接接入本机器的键盘设备和显示器虚拟终端(tty)&#xff1a;通过软件方式虚拟实现的终端。它可以…

Docker 引擎离线安装包采集脚本

文章目录 一、场景说明二、脚本职责三、参数说明四、操作示例五、注意事项 一、场景说明 本自动化脚本旨在为提高研发、测试、运维快速部署应用环境而编写。 脚本遵循拿来即用的原则快速完成 CentOS 系统各应用环境部署工作。 统一研发、测试、生产环境的部署模式、部署结构、…

基于SSM的宠物管理系统

点击以下链接获取源码: https://download.csdn.net/download/qq_64505944/89076676?spm=1001.2014.3001.5503 技术:SSM(Spring+SpringMVC+MyBatis)+LayUI+Echarts技术栈,分页采用pagehelper插件,EasyExcel进行Excel文件的导入导出。 宠物管理系统 1 CHINER-宠物管理系…

【MYSQL之进阶篇】视图、存储过程、存储函数以及触发器

&#x1f525;作者主页&#xff1a;小林同学的学习笔录 &#x1f525;mysql专栏&#xff1a;小林同学的专栏 1.视图 1.1 定义 视图是MySQL数据库中的虚拟表&#xff0c;它基于一个或多个实际表的查询结果。视图提供了一种简单的 方法来封装和重用复杂的查询&#xff0c;同时…

MySQL的基本查询

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;MySQL &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容介绍了mysql的基本查询部分的知识&#xff0c;包括Crea…

YOLOv9改进 | 一文带你了解全新的SOTA模型YOLOv9(论文阅读笔记,效果完爆YOLOv8)

官方论文地址&#xff1a; 官方论文地址点击即可跳转 官方代码地址&#xff1a; 官方代码地址点击即可跳转 图1. 在MS COCO数据集上实时对象检测器的比较。基于GELAN和PGI的对象检测方法在对象检测性能方面超越了所有以前的从头开始训练的方法。在准确性方面&#xff0c;新方法…

Go语言实现Redis分布式锁2

项目地址: https://github.com/liwook/Redislock 1.支持阻塞式等待获取锁 之前的是只尝试获取一次锁&#xff0c;要是获取失败就不再尝试了。现在修改为支持阻塞式等待获取锁。 添加LockOptions结构体 添加option.go文件。 在LockOptions中 isBlock表示是否是阻塞模式blo…

配置vscode用于STM32编译,Debug,github上传拉取

配置环境参考&#xff1a; Docs 用cubemx配置工程文件&#xff0c;用VScode打开工程文件。 编译的时候会有如下报错&#xff1a; vscode出现process_begin :CreateProcess failed 系统找不到指定文件 解决方案&#xff1a;在你的makefile中加上SHELLcmd.exe就可以了 参考…

java发送请求-cookie有关代码

在初始化后添加cookie的代码 用这个httpclients类调custom方法&#xff0c;进行代码定制化 找和cookie有关的方法&#xff0c;设置默认的cookie存储信息 入参是接口 将入参粘贴后找方法&#xff0c;用new实现这个接口 这个方法是无参空构造&#xff0c;可以使用 设置了cookie …

【C++】RapidJSON 设置支持 std::string,防止编译报错

问题 rapidjson 创建 json 数据&#xff0c;使用 std::string 字符串进行赋值&#xff0c;编译时&#xff0c;抱一堆错误 .... rapidjson/include/rapidjson/document.h:690:5: note: candidate expects 0 arguments, 1 provided [build] make[2]: *** [main/CMakeFiles/ma…

Betaflight 4.5RC3 AT32F435遇到的一些“怪”现象

Betaflight 4.5RC3 AT32F435遇到的一些“怪”现象 1. 源由2. “怪”现象2.1 电机#4没有RPM转速2.2 遥控器通道10接收机测试失败2.3 OSD 异常2.4 磁力计数据无法获取 3. 参考资料 1. 源由 升级下固件&#xff0c;追下“时髦”&#xff0c;赶下“潮流”&#xff0c;本着“活着就…

MySQL学习路线一条龙

引言 在当前的IT行业&#xff0c;无论是校园招聘还是社会招聘&#xff0c;MySQL的重要性不言而喻。 面试过程中&#xff0c;MySQL相关的问题经常出现&#xff0c;这不仅因为它是最流行的关系型数据库之一&#xff0c;而且在日常的软件开发中&#xff0c;MySQL的应用广泛&#…

蚁剑修改特征性信息

前言 我们首先得知道蚁剑的流量特征&#xff1a; 编码器和解码器的特征&#xff1a;蚁剑自带的编码器和解码器具有明显的特点&#xff0c;可以通过更改配置文件来达到流量加密的目的1。例如&#xff0c;蚁剑支持多种编码方式&#xff0c;如base64、chr、rot13等&#xff0c;这…

LLM 构建Data Multi-Agents 赋能数据分析平台的实践之②:数据治理之二(自动处理)

前述 在前文的multi Agents for Data Analysis的设计说起&#xff0c;本文将继续探索和测试借助llm实现基于私有知识库的数据治理全自动化及智能化。整体设计如下&#xff1a; 整个体系设计了3个Agent以及一个Planer&Execute Agent&#xff0c;第一个Agent用于从企业数据…

【Linux系列】“dev-node1“ 运行的操作系统分析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Vue3与TypeScript中动态加载图片资源的解决之道

在前端开发中&#xff0c;Vue.js已成为一个备受欢迎的框架&#xff0c;尤其是在构建单页面应用时。Vue3的发布更是带来了许多性能优化和新特性&#xff0c;而TypeScript的加入则进一步提升了代码的可维护性和健壮性。然而&#xff0c;在实际的项目开发中&#xff0c;我们有时会…

牛客NC93 设计LRU缓存结构【hard 链表,Map Java】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/5dfded165916435d9defb053c63f1e84 思路 双向链表map最新的数据放头结点&#xff0c;尾节点放最老的数据&#xff0c;没次移除尾巴节点本地考察链表的新增&#xff0c;删除&#xff0c;移动节点参考答案Java im…

第六篇: 3.5 性能效果 (Performance)- IAB/MRC及《增强现实广告效果测量指南1.0》

​​​​​​​ 翻译计划 第一篇概述—IAB与MRC及《增强现实广告效果测量指南》之目录、适用范围及术语第二篇 广告效果测量定义和其他矩阵之- 3.1 广告印象&#xff08;AD Impression&#xff09;第三篇 广告效果测量定义和其他矩阵之- 3.2 可见性 &#xff08;Viewability…

正确使用@Autowired

目录 一、前言二、跟着官方文档&#xff0c;学习正确使用Autowired0、实验环境1、通过构造方法进行注入1.1 问题1&#xff1a;那万一没有这个CustomerPreferenceDao对象&#xff0c;会报错吗&#xff1f; 2、通过setter方法注入3、通过方法注入&#xff08;这个方法可以是任意名…

【Android】apk安装报错:包含病毒: a.gray.BulimiaTGen.f

​ 有时候apk安装或者更新时&#xff0c;显示&#xff1a;[高风险]包含病毒: a.gray.BulimiaTGen.f这种bug&#xff1b; 原因&#xff1a;这是手机管家误报病毒。 处理方法&#xff1a;我看网上其他资料可以进行申诉&#xff0c;也可以进行apk加固&#xff0c;我这边尝试用360…