积木报表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
    @UseDownloadTaskCenter
    public JimuPageDto<DashBoardDataDto> dashboardNewVersion(DashBoardQueryDto dashBoardQueryDto) {
    //  你的数据接口逻辑
    }

参数基类

/**
 * @author: humorchen
 * date: 2024/1/15
 * description:
 **/
@Data
public class DownloadCenterBaseParam {

    /**
     * 分页数据页号和页大小
     */
    private Integer pageNo;
    /**
     * 分页数据页号和页大小
     */
    private Integer pageSize;
}

结果基类

/**
 * @author: humorchen
 * date: 2023/12/19
 * description:
 **/
@Data
@FieldNameConstants
@Accessors(chain = true)
public class JimuPageDto<T> {
    /**
     * 数据
     */
    private List<T> data;
    /**
     * 积木的count是总数据条数,不是当前页多少条!!!
     */
    private long count;
    /**
     * 积木的total是总页数,不是总数据条数!!!
     */
    private long total;

    public static final JimuPageDto EMPTY = new JimuPageDto().setData(Collections.emptyList()).setTotal(0).setCount(0);
}

/**
 * @author: humorchen
 * date: 2024/1/5
 * description: 下载任务状态
 **/
@Getter
public enum DownloadTaskStateEnum {
    WAIT(0, "等待执行"),
    RUNNING(1, "执行中"),
    SUCCESS(2, "执行成功"),
    FAILED(3, "执行失败"),

    ;

    private final int state;
    private final String title;

    DownloadTaskStateEnum(int state, String title) {
        this.state = state;
        this.title = title;
    }

    /**
     * 根据状态获取枚举
     *
     * @param state
     * @return
     */
    public static DownloadTaskStateEnum of(int state) {
        for (DownloadTaskStateEnum value : values()) {
            if (value.state == state) {
                return value;
            }
        }
        return null;
    }
}

下载中心服务(报表下载的导出任务)
/**
 * <p>
 * 下载任务 服务类
 * </p>
 *
 * @author humorchen
 * @since 2024-01-05
 */
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 {
    @Autowired
    private DownloadTaskMapper downloadTaskMapper;
    @Autowired
    private IReportDataGetService reportDataGetService;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 注入spring 事件发布器
     */
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    /**
     * 注册任务
     *
     * @param downloadTask
     * @return
     */
    @Override
    public DownloadTask registerTask(@NonNull DownloadTask downloadTask) {
        downloadTaskMapper.insert(downloadTask);
        return downloadTask;
    }

    /**
     * 10秒内是否有相同任务未完成,不给再次注册下载任务
     *
     * @param account
     * @param requestBody
     * @return
     */
    @Override
    public boolean setSameTaskLock(String account, String requestBody) {
        DownloadTaskSubmitLimitCacheKey limitCacheKey = new DownloadTaskSubmitLimitCacheKey(account, MD5.create().digestHex(requestBody));
        Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(limitCacheKey.getKey(), DateUtil.now(), limitCacheKey.getExpire(), limitCacheKey.getTimeUnit());
        return Boolean.TRUE.equals(setIfAbsent);
    }

    /**
     * 更新任务
     *
     * @param downloadTask
     * @return
     */
    @Override
    public int updateTaskById(@NonNull DownloadTask downloadTask) {
        return downloadTaskMapper.updateById(downloadTask);
    }

    /**
     * 更新任务进度
     *
     * @param id
     * @param percent
     * @return
     */
    @Override
    public int changeTaskPercent(int id, @NonNull String percent) {
        UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq(DownloadTask.ID, id);
        updateWrapper.set(DownloadTask.PERCENT, percent);
        log.info("【下载中心】更新任务进度 id:{} percent:{}", id, percent);
        return downloadTaskMapper.update(null, updateWrapper);
    }

    /**
     * 更新任务状态
     *
     * @param id
     * @param state
     * @return
     */
    @Override
    public int changeTaskState(int id, @NonNull DownloadTaskStateEnum state) {
        UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq(DownloadTask.ID, id);
        updateWrapper.set(DownloadTask.STATE, state.getState());
        return downloadTaskMapper.update(null, updateWrapper);
    }

    /**
     * 更新任务状态
     *
     * @param id
     * @param expectState
     * @param targetState
     * @return
     */
    @Override
    public int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState) {
        UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq(DownloadTask.ID, id);
        updateWrapper.eq(DownloadTask.STATE, expectState.getState());
        updateWrapper.set(DownloadTask.STATE, targetState.getState());
        return downloadTaskMapper.update(null, updateWrapper);
    }

    /**
     * 根据任务ID获取任务
     *
     * @param id
     * @return
     */
    @Override
    public DownloadTask getDownloadTaskById(int id) {
        return downloadTaskMapper.selectById(id);
    }

    /**
     * 查下载任务
     *
     * @param pageListDownloadTaskDto
     * @return
     */
    @Override
    public IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
        Integer id = pageListDownloadTaskDto.getId();
        String startTime = pageListDownloadTaskDto.getStartTime();
        String endTime = pageListDownloadTaskDto.getEndTime();
        String fileName = pageListDownloadTaskDto.getFileName();
        Integer taskState = pageListDownloadTaskDto.getTaskState();
        UserInfo userInfo = UserInfoHolder.get();
        String account = userInfo.getAccount();
        int pageNo = Optional.ofNullable(pageListDownloadTaskDto.getPageNo()).orElse(1);
        int pageSize = Optional.ofNullable(pageListDownloadTaskDto.getPageSize()).orElse(10);

        QueryWrapper<DownloadTask> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq(DownloadTask.ACCOUNT, account);
        queryWrapper.eq(id != null, DownloadTask.ID, id);
        queryWrapper.between(startTime != null && endTime != null, DownloadTask.CREATE_TIME, startTime, endTime);
        queryWrapper.like(StrUtil.isNotBlank(fileName), DownloadTask.TITLE, "%" + fileName + "%");
        queryWrapper.eq(taskState != null, DownloadTask.STATE, taskState);
        // 最新的在前
        queryWrapper.orderByDesc(DownloadTask.CREATE_TIME);
        return page(new Page<>(pageNo, pageSize), queryWrapper);
    }

    /**
     * 重新执行下载任务
     *
     * @param taskId
     * @return
     */
    @Override
    public Result<String> rerunTask(Integer taskId) {
        DownloadTask downloadTask = getDownloadTaskById(taskId);
        if (downloadTask == null) {
            return Result.fail("未找到该任务,请刷新后重试");
        }
        if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
            return Result.fail("任务正在执行中,请稍后重试");
        }
        eventPublisher.publishEvent(new DownloadTaskPublishEvent(taskId));
        return Result.ok("重新执行中");
    }

    /**
     * 根据报表ID获取报表名称
     *
     * @param reportId
     * @return
     */
    @Override
    public String getReportNameByReportId(String reportId) {
        if (StrUtil.isBlank(reportId)) {
            return "";
        }
        String sql = "select name from report.jimu_report where id = '" + reportId + "'";
        JSONObject jsonObject = reportDataGetService.getOne(sql);
        return Optional.ofNullable(jsonObject.getString("name")).orElse("");
    }

    /**
     * 从请求体中获取报表ID
     *
     * @param requestBody
     * @return
     */
    @Override
    public String getReportIdFromRequestBody(String requestBody) {
        if (StrUtil.isNotBlank(requestBody)) {
            JSONObject jsonObject = JSONObject.parseObject(requestBody);
            return jsonObject.getString("excelConfigId");
        }
        return null;
    }

    /**
     * 根据报表ID获取报表API地址或者SQL
     *
     * @param reportId
     * @return
     */
    @Override
    public JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId) {
        JimuReportDataSourceDTO jimuReportDataSourceDTO = new JimuReportDataSourceDTO();
        if (StrUtil.isNotBlank(reportId)) {
            String sql = "select db_dyn_sql,api_url from report.jimu_report_db where jimu_report_id = '" + reportId + "'";
            JSONObject jsonObject = reportDataGetService.getOne(sql);
            jimuReportDataSourceDTO.setSql(jsonObject.getString("db_dyn_sql"));
            jimuReportDataSourceDTO.setApiUrl(jsonObject.getString("api_url"));
        }
        List<List<String>> head = new ArrayList<>();
        EasyExcel.write(new OutputStream() {
            @Override
            public void write(int b) throws IOException {

            }
        }).head(head).sheet("sheet").doWrite(new ArrayList<>());

        return jimuReportDataSourceDTO;
    }

    /**
     * 获取积木报表的头
     *
     * @param reportId
     * @return
     */
    @Override
    public List<JimuReportDataColumnDTO> getReportHead(String reportId) {
        if (StrUtil.isBlank(reportId)) {
            return Collections.emptyList();
        }
        String sql = "select json_str from report.jimu_report where id = '" + reportId + "'";
        JSONObject jsonObject = reportDataGetService.getOne(sql);
        String jsonStr = jsonObject.getString("json_str");
        JSONObject json = JSONObject.parseObject(jsonStr);
        JSONObject rows = json.getJSONObject("rows");
        JSONObject rows0Cells = rows.getJSONObject("0").getJSONObject("cells");
        JSONObject rows1Cells = rows.getJSONObject("1").getJSONObject("cells");

        Set<String> rows0KeySets = rows0Cells.keySet();
        List<JimuReportDataColumnDTO> heads = rows0KeySets.stream().map(key -> {
            JSONObject keyObject = rows0Cells.getJSONObject(key);
            JSONObject columnObject = rows1Cells.getJSONObject(key);
            if (keyObject == null || columnObject == null) {
                return null;
            }
            String name = keyObject.getString("text");
            String column = columnObject.getString("text");
            if (StrUtil.isBlank(name) || StrUtil.isBlank(column)) {
                return null;
            }
            // 处理 #{vpjcgifyua.orderId}
            int indexOf = column.lastIndexOf(".");
            int indexOf2 = column.lastIndexOf("}");
            if (column.startsWith("#") && indexOf >= 0 && indexOf2 >= 0) {
                column = column.substring(indexOf + 1, indexOf2);
                if (StrUtil.isBlank(column)) {
                    return null;
                }
            }
            JimuReportDataColumnDTO jimuReportDataColumnDTO = new JimuReportDataColumnDTO();
            jimuReportDataColumnDTO.setName(name);
            jimuReportDataColumnDTO.setColumn(column);
            jimuReportDataColumnDTO.setIndex(Integer.parseInt(key));
            return jimuReportDataColumnDTO;
        }).filter(Objects::nonNull).sorted(Comparator.comparing(JimuReportDataColumnDTO::getIndex)).collect(Collectors.toList());
        log.info("【下载中心】获取积木报表的头 reportId:{},heads:{}", reportId, heads);
        return heads;
    }

    /**
     * 从积木请求体中获取请求参数
     *
     * @param json
     * @return
     */
    @Override
    public String getRequestParamFromJson(String json) {
        if (StrUtil.isNotBlank(json)) {
            JSONObject jsonObject = JSONObject.parseObject(json);
            if (jsonObject.containsKey("param")) {
                return jsonObject.getJSONObject("param").toJSONString();
            }
            return "{}";
        }
        return "{}";
    }


}

数据跨库获取工具

服务接口


/**
 * @author: humorchen
 * date: 2023/12/19
 * description: 获取数据服务,直接SQL跨库拿数据
 **/
@DS("slave_1")
public interface IReportDataGetService<T> {
    /**
     * 执行SQL返回数据
     *
     * @param sql
     * @return
     */
    JSONObject getOne(String sql);

    /**
     * 执行SQL返回数据,数据封装到类cls对象里
     *
     * @param sql
     * @param cls
     * @return
     */
    T getOne(String sql, Class<T> cls);

    /**
     * 执行SQL返回数据
     *
     * @param sql
     * @return
     */
    JSONArray getList(String sql);

    /**
     * 执行SQL返回数据,数据封装到类cls对象里
     *
     * @param sql
     * @param cls
     * @return
     */
    List<T> getList(String sql, Class<T> cls);

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @return
     */
    JSONArray pageGetList(String sql, int page, int pageSize);

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @return
     */
    JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize);

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @param cls
     * @return
     */
    JimuPageDto<T> pageGetListForJimu(String sql, int page, int pageSize, Class<T> cls);

    /**
     * 计数
     *
     * @param sql
     * @return
     */
    long count(String sql);


    /**
     * 生成in语句
     *
     * @param columnName
     * @param elements
     * @return string
     */
    default String getColumnInSql(String columnName, List<String> elements) {
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append(" ");
        sqlBuilder.append(columnName);
        sqlBuilder.append(" in (");
        for (int i = 0; i < elements.size(); i++) {
            String id = elements.get(i);
            if (i > 0) {
                sqlBuilder.append(",");
            }
            sqlBuilder.append("'");
            sqlBuilder.append(id);
            sqlBuilder.append("'");
        }
        sqlBuilder.append(")");
        return sqlBuilder.toString();
    }

}

服务实现


/**
 * @author: chenfuxing
 * date: 2023/12/19
 * description:
 **/
@Service
@Slf4j
public class ReportDataGetServiceImpl implements IReportDataGetService {
    @Autowired
    private DataSource dataSource;


    /**
     * 执行SQL返回数据
     *
     * @param sql
     * @return
     */
    @Override
    public JSONObject getOne(String sql) {
        JSONObject ret = null;
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = dataSource.getConnection();
            statement = connection.createStatement();
            logSql(sql);
            resultSet = statement.executeQuery(sql);
            if (resultSet != null) {
                while (resultSet.next()) {
                    if (ret != null) {
                        throw new RuntimeException("查询结果不止一条数据");
                    }
                    ret = new JSONObject();
                    ResultSetMetaData metaData = resultSet.getMetaData();
                    int columnCount = metaData.getColumnCount();
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = metaData.getColumnLabel(i);
                        ret.put(columnName, resultSet.getObject(columnName));
                    }
                }
            }
        } catch (Exception e) {
            log.error("获取数据报错", e);
        } finally {
            // 释放资源
            IoUtil.close(resultSet);
            IoUtil.close(statement);
            IoUtil.close(connection);
        }
        return ret;
    }

    /**
     * 执行SQL返回数据
     *
     * @param sql
     * @return
     */
    @Override
    public JSONArray getList(String sql) {
        JSONArray ret = new JSONArray();
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = dataSource.getConnection();
            statement = connection.createStatement();
            logSql(sql);
            resultSet = statement.executeQuery(sql);
            if (resultSet != null) {
                while (resultSet.next()) {
                    // 组装数据为json 对象
                    JSONObject data = new JSONObject();
                    ResultSetMetaData metaData = resultSet.getMetaData();
                    int columnCount = metaData.getColumnCount();
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = metaData.getColumnLabel(i);
                        data.put(columnName, resultSet.getObject(columnName));
                    }
                    ret.add(data);
                }
            }
        } catch (Exception e) {
            log.error("获取数据报错", e);
        } finally {
            // 释放资源
            IoUtil.close(resultSet);
            IoUtil.close(statement);
            IoUtil.close(connection);
        }
        return ret;
    }

    private void logSql(String sql) {
        int len = 5000;
        // 执行SQL
        log.info("执行的SQL:{}", StrUtil.isNotBlank(sql) && sql.length() > len ? sql.substring(0, len) : sql);
    }

    /**
     * 计数
     *
     * @param sql
     * @return
     */
    @Override
    public long count(String sql) {
        String countSQL = getCountSqlFromQuerySql(sql);
        if (StrUtil.isBlank(countSQL)) {
            throw new RuntimeException("计数语句不得为空,SQL为:" + sql);
        }
        long ret = 0;
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = dataSource.getConnection();
            statement = connection.createStatement();
            logSql(sql);
            resultSet = statement.executeQuery(countSQL);
            if (resultSet != null) {
                while (resultSet.next()) {
                    ret = resultSet.getLong(1);
                }
            }
        } catch (Exception e) {
            log.error("获取数据报错", e);
        } finally {
            // 释放资源
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (Exception ignored) {
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (Exception ignored) {
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ignored) {
                }
            }
        }
        return ret;
    }

    /**
     * 从查询语句变计数语句
     *
     * @param sql
     * @return
     */
    public String getCountSqlFromQuerySql(String sql) {
        String selectStr = "select";
        int selectIndex = sql.indexOf(selectStr);
        int fromIndex = sql.indexOf("from");
        return sql.replace(sql.substring(selectIndex + selectStr.length(), fromIndex), " count(*) as c ");
    }

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    public JSONArray pageGetList(String sql, int page, int pageSize) {
        String querySql = getPageSqlFromQuerySql(sql, page, pageSize);
        if (StrUtil.isBlank(querySql)) {
            throw new RuntimeException("分页查询解析失败,SQL:" + sql + " 页号: " + page + " 每页数量:" + pageSize);
        }
        return getList(querySql);
    }

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    public JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize) {
        JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
        // 查count
        long count = count(sql);
        long total = count / pageSize + (count % pageSize > 0 ? 1 : 0);
        log.info("数据总条数:{} 条,每页:{} 条,总页数:{} 页", count, pageSize, total);
        jimuPageDto.setTotal(total);
        // 查分页数据
        JSONArray data = pageGetList(sql, page, pageSize);
        List<JSONObject> dataList = new ArrayList<>(data.size());
        for (int i = 0; i < data.size(); i++) {
            JSONObject jsonObject = data.getJSONObject(i);
            dataList.add(jsonObject);
        }
        jimuPageDto.setData(dataList);
        jimuPageDto.setCount(count);
        return jimuPageDto;
    }

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @return
     */
    public String getPageSqlFromQuerySql(String sql, int page, int pageSize) {
        Assert.isTrue(page >= 1, () -> new IllegalArgumentException("page不得小于1"));
        Assert.isTrue(pageSize >= 1, () -> new IllegalArgumentException("pageSize不得小于1"));
        int skip = (page - 1) * pageSize;
        StringBuilder builder = new StringBuilder(sql);
        builder.append(" limit ");
        if (skip > 0) {
            builder.append(skip);
            builder.append(",");
        }
        builder.append(pageSize);
        String querySql = builder.toString();
        log.info("分页查询原SQL:{}\n分页SQL处理后:{}", sql, querySql);
        return querySql;
    }

    /**
     * 执行SQL返回数据,数据封装到类cls对象里
     *
     * @param sql
     * @param cls
     * @return
     */
    @Override
    public Object getOne(String sql, Class cls) {
        return getOne(sql).toJavaObject(cls);
    }

    /**
     * 执行SQL返回数据,数据封装到类cls对象里
     *
     * @param sql
     * @param cls
     * @return
     */
    @Override
    public List getList(String sql, Class cls) {
        return getList(sql).toJavaList(cls);
    }

    /**
     * 分页查询
     *
     * @param sql
     * @param page
     * @param pageSize
     * @param cls
     * @return
     */
    @Override
    public JimuPageDto pageGetListForJimu(String sql, int page, int pageSize, Class cls) {
        JimuPageDto<JSONObject> jimuPageDto = pageGetListForJimu(sql, page, pageSize);
        JimuPageDto ret = new JimuPageDto<>();
        List list = new ArrayList(jimuPageDto.getData().size());
        for (int i = 0; i < jimuPageDto.getData().size(); i++) {
            list.add(jimuPageDto.getData().get(i).toJavaObject(cls));
        }
        ret.setData(list);
        ret.setTotal(jimuPageDto.getTotal());
        ret.setCount(jimuPageDto.getCount());
        return ret;
    }


}

拦截切面

/**
 * @author: humorchen
 * date: 2024/1/15
 * description: 下载任务切面
 * 对加上了@UseDownloadTaskCenter注解的方法进行切面,使用下载任务中心代理掉,完成下载任务
 **/
@Aspect
@Component
@Slf4j
public class DownloadTaskAspect {
    @Autowired
    private IDownloadTaskService downloadTaskService;
    /**
     * 注入spring 事件发布器
     */
    @Autowired
    private ApplicationEventPublisher eventPublisher;



    /**
     * 环绕通知
     *
     * @return
     */
    @Around("@annotation(cn.sffix.recovery.report.annotations.UseDownloadTaskCenter))")
    @Order(50)
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("【下载中心】进入下载中心切面");
        // 是下载中心发的请求则直接执行分页数据
        if (DownloadCenterUtil.isDownloadCenterRequest()) {
            log.info("【下载中心】下载中心发的请求,直接执行分页数据");
            return joinPoint.proceed();
        }

        // 识别下载请求
        int pageNo = 1;
        int pageSize = 20;
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            DownloadCenterBaseParam downloadCenterBaseParam = null;
            // 找到参数
            for (Object arg : args) {
                if (arg instanceof DownloadCenterBaseParam) {
                    downloadCenterBaseParam = (DownloadCenterBaseParam) arg;
                    break;
                }
            }
            // 检查参数
            if (downloadCenterBaseParam != null) {
                pageNo = Optional.ofNullable(downloadCenterBaseParam.getPageNo()).orElse(pageNo);
                pageSize = Optional.ofNullable(downloadCenterBaseParam.getPageSize()).orElse(pageSize);
            }
            log.info("【下载中心】下载中心切面,downloadCenterBaseParam:{}", downloadCenterBaseParam);
            if (downloadCenterBaseParam != null) {
                Object target = joinPoint.getTarget();
                Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
                Class<?> returnType = method.getReturnType();
                // 返回值类型检查
                if (returnType.equals(JimuPageDto.class)) {
                    // 如果是导出请求,则使用下载任务中心代理掉
                    if (isExportFirstPageRequest(pageNo, pageSize)) {
                        // 如果是导出第一页请求,则使用下载任务中心代理掉
                        DownloadTask downloadTask = registerTask(downloadCenterBaseParam, target, method, args);

                        if (downloadTask == null || downloadTask.getId() == null) {
                            log.error("【下载中心】注册下载任务失败,任务信息:{}", downloadTask);
                            return joinPoint.proceed();
                        }
                        log.info("【下载中心】注册下载任务成功,任务信息:{}", downloadTask);
                        // 返回积木所需要的数据
                        JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();
                        jimuPageDto.setTotal(0);
                        jimuPageDto.setCount(0);
                        JSONObject jsonObject = new JSONObject();
                        String downloadTaskJsonStr = downloadTask.getJson();
                        DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTaskJsonStr, DownloadTaskJson.class);
                        String requestBody = downloadTaskJson.getRequestBody();
                        String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
                        List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
                        log.info("【下载中心】reportHead:{}", reportHead);
                        if (CollectionUtil.isNotEmpty(reportHead) && reportHead.size() > 1) {
                            String column = reportHead.get(1).getColumn();
                            jsonObject.put(column, "请前往报表中台-下载中心查看(任务ID " + downloadTask.getId() + ")");
                            log.info("【下载中心】返回数据:{}", jsonObject);
                        } else {
                            log.info("【下载中心】返回数据为空");
                        }
                        List<JSONObject> list = Collections.singletonList(jsonObject);
                        jimuPageDto.setData(list);
                        eventPublisher.publishEvent(new DownloadTaskPublishEvent(downloadTask.getId()));
                        return jimuPageDto;
                    } else {
                        log.info("【下载中心】不是导出请求,直接执行分页数据");
                    }
                } else {
                    log.error("【下载中心】返回值类型不是JimuPageDto,无法使用下载任务中心代理掉");
                }

            }

        }

        return joinPoint.proceed();
    }

    /**
     * 生成下载任务
     *
     * @param downloadTaskParam
     * @return
     */
    private DownloadTask registerTask(DownloadCenterBaseParam downloadTaskParam, Object proxyTarget, Method method, Object[] args) {
        UserInfo loginUser = UserInfoHolder.get();
        String account = loginUser.getAccount();
        HttpServletRequest currentRequest = RequestUtil.getCurrentRequest();
        String requestBody = DownloadCenterUtil.getRequestBodyFromHeader(currentRequest);

        // 防止10秒内重复点击
        if (!downloadTaskService.setSameTaskLock(account, requestBody)) {
            log.error("【下载中心】10秒内重复点击,不给再次注册下载任务");
            return null;
        }

        String title = "导出-" + DateUtil.now().replace(" ", "_") + ".xlsx";
        try {
            title = downloadTaskService.getReportNameByReportId(downloadTaskService.getReportIdFromRequestBody(requestBody)) + title;
        } catch (Exception e) {
            log.error("【下载中心】获取报表名称失败", e);
        }
        String url = generateFileUrl();
        DownloadTask downloadTask = new DownloadTask();
        downloadTask.setAccount(account);
        downloadTask.setTitle(title);
        downloadTask.setIcon("");
        downloadTask.setUrl(url);
        downloadTask.setFileSize("");
        downloadTask.setPercent("0%");
        downloadTask.setState(DownloadTaskStateEnum.WAIT.getState());
        DownloadTaskJson downloadTaskJson = new DownloadTaskJson();
        // 拷贝最开始请求积木的token和requestBody,执行下载任务时需要
        downloadTaskJson.setRequestToken(DownloadCenterUtil.getRequestTokenFromHeader(currentRequest));
        downloadTaskJson.setRequestBody(requestBody);
        downloadTaskJson.setProxyMethod(method.getName());
        if (args != null) {
            for (Object arg : args) {
                if (arg instanceof DownloadCenterBaseParam) {
                    downloadTaskJson.setParam((DownloadCenterBaseParam) arg);
                    break;
                }
            }
        }
        downloadTask.setJson(JSONObject.toJSONString(downloadTaskJson));

        downloadTask = downloadTaskService.registerTask(downloadTask);
        return downloadTask;
    }

    /**
     * 生成文件url地址
     *
     * @return
     */
    private String generateFileUrl() {
        // todo 生成文件url地址
        return "";
    }

    /**
     * 注入jm report分页大小
     */
    @Value("${jeecg.jmreport.page-size-number:5000}")
    private int jmReportPageSizeNumber;

    /**
     * 判断是否为导出请求
     *
     * @param pageNo
     * @param pageSize
     * @return
     */
    private boolean isExportPageRequest(int pageNo, int pageSize) {
        return pageSize == jmReportPageSizeNumber;
    }

    /**
     * 判断是否为导出请求
     *
     * @param pageNo
     * @param pageSize
     * @return
     */
    private boolean isExportFirstPageRequest(int pageNo, int pageSize) {
        log.info("【下载中心】判断是否为导出请求 isExportFirstPageRequest pageNo:{},pageSize:{},积木报表导出size:{}", pageNo, pageSize, jmReportPageSizeNumber);
        return pageNo == 1 && isExportPageRequest(pageNo, pageSize);
    }
}

报表下载任务处理事件

暂时由于导出项目只部署一个实例,导出任务使用的是spring 本地event来异步处理的,你可以使用MQ、调度来执行掉任务,可以参考我这个
上传华为云OSS的部分你替换为你的OSS上传操作,这个地方我是复用项目里已有的存储上传服务
里面的异步线程池、环境工具类、积木导出每页大小要设置自己的

/**
 * @author: humorchen
 * date: 2024/1/16
 * description: 下载任务发布事件
 **/
@Getter
public class DownloadTaskPublishEvent extends ApplicationEvent {
    /**
     * 任务ID
     */
    private final Integer taskId;

    public DownloadTaskPublishEvent(Integer taskId) {
        super(taskId);
        this.taskId = taskId;
    }
    
}

处理器


/**
 * @author: humorchen
 * date: 2024/1/16
 * description: 处理下载任务发布事件
 **/
@Component
@Slf4j
public class DownloadTaskPublishEventHandler implements ApplicationListener<DownloadTaskPublishEvent> {
    @Autowired
    private IDownloadTaskService downloadTaskService;
    @Autowired
    private IReportService reportService;
    @Autowired
    @Qualifier(AsyncConfig.ASYNC_THREAD_POOL)
    private Executor executor;

    @Autowired
    private HwProperties hwProperties;
    @Autowired
    private EnvUtil envUtil;

    /**
     * 注入jm report分页大小
     */
    @Value("${jeecg.jmreport.page-size-number:5000}")
    private int jmReportPageSizeNumber;

    @Autowired
    private RedissonClient redissonClient;


    /**
     * 超时时间
     */
    private static final int TIMEOUT_MILLS = 1000 * 60 * 20;

    @Autowired
    private HwObjectStorageService hwObjectStorageService;


    /**
     * 上传文件到OSS的路径
     *
     * @param account
     * @param fileName
     * @return
     */
    private final StoragePath getStoragePath(String account, String fileName) {
        return StoragePath.path().addFolder("downloadTask").addFolder(account).fileName(fileName);
    }

    /**
     * Handle an application event.
     *
     * @param event the event to respond to
     */
    @Override
    @Async(AsyncConfig.ASYNC_THREAD_POOL)
    public void onApplicationEvent(DownloadTaskPublishEvent event) {
        Integer taskId = event.getTaskId();

        log.info("【下载中心】执行下载任务 taskId:{}", taskId);
        DownloadTask downloadTask = downloadTaskService.getById(taskId);
        if (downloadTask == null) {
            log.error("【下载中心】下载任务不存在,taskId:{}", taskId);
            return;
        }
        if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {
            log.error("【下载中心】下载任务正在执行中,taskId:{}", taskId);
            return;
        }

        try {
            log.info("【下载中心】下载任务开始执行,taskId:{}", taskId);
            // 改状态到执行中
            DownloadTaskStateEnum downloadTaskStateEnum = Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT);
            int compareAndSwapTaskState = downloadTaskService.compareAndSwapTaskState(taskId, downloadTaskStateEnum, DownloadTaskStateEnum.RUNNING);
            if (compareAndSwapTaskState < 1) {
                log.info("【下载中心】下载任务状态不对,taskId:{}, state:{}", taskId, downloadTaskStateEnum);
                return;
            }
            DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTask.getJson(), DownloadTaskJson.class);

            // 获取数据
            String requestBody = downloadTaskJson.getRequestBody();
            String requestToken = downloadTaskJson.getRequestToken();
            String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);
            String reportName = downloadTaskService.getReportNameByReportId(reportId);
            String requestParam = downloadTaskService.getRequestParamFromJson(downloadTask.getJson());
            JimuReportDataSourceDTO dataSourceDTO = downloadTaskService.getReportApiOrSqlByReportId(reportId);
            List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);
            // 打印上面拿到的数据
            log.info("reportId :{} \n reportName:{} \n requestParam:{} \n requestBody:{}  \n dataSourceDTO:{} \n reportHead:{}", reportId, reportName, requestParam, requestBody, dataSourceDTO, reportHead);
            JimuReportDynamicEasyExcelImpl jimuReportDynamicEasyExcel = new JimuReportDynamicEasyExcelImpl(reportId, reportName, taskId, downloadTaskService, requestParam, requestToken, dataSourceDTO, reportHead);
            // 生成excel文件
            List<List<String>> head = reportHead.stream().map(d -> Collections.singletonList(d.getName())).collect(Collectors.toList());
            // 分页写数据
            InputStream inputStream = DynamicColumnEasyExcelUtil.writePageData(head, jimuReportDynamicEasyExcel, jmReportPageSizeNumber);



            // 上传excel文件到oss
            StoragePath storagePath = getStoragePath(downloadTask.getAccount(), downloadTask.getTitle());
            downloadTask.setPercent("100%");
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            URI uri = hwObjectStorageService.savePublicFile(inputStream, storagePath);
            stopWatch.stop();
            log.info("【下载中心】上传文件到OSS,耗时:{} ms,uri:{}", stopWatch.getLastTaskTimeMillis(), uri);
            // 更新任务信息
            String url = getUrlPrefix() + uri.getPath();
            downloadTask.setUrl(url);
            downloadTask.setState(DownloadTaskStateEnum.SUCCESS.getState());
            log.info("【下载中心】下载任务成功,taskId:{},task:{}", taskId, downloadTask);
            boolean updated = downloadTaskService.updateById(downloadTask);
            log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
        } catch (Exception e) {
            log.error("【下载中心】下载任务执行失败", e);
            // 更新任务信息
            downloadTask.setState(DownloadTaskStateEnum.FAILED.getState());
            downloadTask.setError("【下载中心】执行失败(" + e.getMessage() + ")");
            boolean updated = downloadTaskService.updateById(downloadTask);
            log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);
        } finally {
            log.info("【下载中心】下载任务 {} 执行完毕", taskId);
        }


    }

    /**
     * 根据环境获取文件url前缀
     *
     * @return
     */
    private String getUrlPrefix() {
        String envCode = envUtil.isPreOrProdEnv() ? "pro" : "test";
        String prefix = "https://test-obs.xxxxx.com";
        for (String key : hwProperties.getUrlMap().keySet()) {
            if (key.contains(envCode)) {
                prefix = hwProperties.getUrlMap().get(key);
            }
        }
        return prefix;
    }

}

透传信息

主要是在这个customApiHeader() 方法里DownloadCenterUtil.copyDownloadCenterHeader(request, header);自定义获取数据请求的header这,读取这次积木导出请求的参数信息,写到获取数据请求的header里去

/**
 * 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)
 * 1.自定义获取登录token
 * 2.自定义获取登录用户
 */
@Slf4j
@Component
public class JimuReportTokenService implements JmReportTokenServiceI {

    @Autowired
    @Lazy
    private UserAutenticationFeign userAutenticationFeign;

    @Autowired
    @Lazy
    private UserInfoFeign userInfoFeign;

    /**
     * 通过请求获取Token
     * @param request
     * @return
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public String[] getRoles(String token) {
        return new String[]{"admin"};
    }

    /**
     * Token校验
     * @param token
     * @return
     */
    @Override
    public 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
     */
    @Override
    public 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 {
    @Autowired
    private IDownloadTaskService downloadTaskService;


    /**
     * 分页查下载任务
     *
     * @param pageListDownloadTaskDto
     * @return
     */
    @Override
    public Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {
        IPage<DownloadTask> downloadTaskPages = downloadTaskService.pageListDownloadTask(pageListDownloadTaskDto);
        Page<DownloadTaskVo> downloadTaskVoPage = new Page<>();
        downloadTaskVoPage.setCurrent(downloadTaskPages.getCurrent());
        downloadTaskVoPage.setPages(downloadTaskPages.getPages());
        downloadTaskVoPage.setSize(downloadTaskPages.getSize());
        downloadTaskVoPage.setTotal(downloadTaskPages.getTotal());
        List<DownloadTaskVo> downloadTaskVos = downloadTaskPages.getRecords().stream().map(downloadTask -> {
            DownloadTaskVo downloadTaskVo = BeanUtils.convert(downloadTask, DownloadTaskVo.class);
            downloadTaskVo.setStateStr(Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT).getTitle());
            return downloadTaskVo;
        }).collect(Collectors.toList());
        downloadTaskVoPage.setRecords(downloadTaskVos);

        return Result.ok(downloadTaskVoPage);
    }

    /**
     * 删除下载任务
     *
     * @param taskId
     * @return
     */
    @Override
    public Result<String> deleteTask(Integer taskId) {
        boolean removeById = downloadTaskService.removeById(taskId);

        return removeById ? Result.ok("删除成功") : Result.fail("未找到该任务,请刷新后重试)");
    }

    /**
     * 重新执行下载任务
     *
     * @param taskId
     * @return
     */
    @Override
    public Result<String> rerunTask(Integer taskId) {
        return downloadTaskService.rerunTask(taskId);
    }
}

DynamicColumnEasyExcelUtil 动态字段EasyExcel工具


/**
 * @author humorchen
 * date: 2024/3/5
 * description: 动态列easyexcel工具类
 **/
@Slf4j
public class DynamicColumnEasyExcelUtil {
    public static final String ROW = "=row()";
    public static interface DynamicColumnEasyExcelInterface<T> {
        /**
         * 分页获取数据
         *
         * @param page
         * @param size
         * @return
         */
        JimuPageDto<T> pageGetData(int page, int size);

        /**
         * 数据对象转换为字符串
         *
         * @param t
         * @return
         */
        List<String> mapDataToStringList(T t);

        /**
         * 分页获取数据加载第i页时触发函数,用于实现进度变更
         *
         * @param pageNo
         * @param pageSize
         */
        void onLoadedPage(int pageNo, int pageSize, int pages);
    }

    /**
     * 从数据库分页读数据并写入成Excel文件,把文件内容写到输出流
     *
     * @param head
     * @param dynamicColumnEasyExcelInterface
     * @param pageSize
     * @param <T>
     * @return
     */
    public static <T> ByteArrayInputStream writePageData(List<List<String>> head, DynamicColumnEasyExcelInterface<T> dynamicColumnEasyExcelInterface, int pageSize) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ExcelWriter excelWriter = EasyExcel.write(outputStream).head(head).build();
        int currentPage = 1;
        long pages = 1;
        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
             */
            @Override
            protected Integer columnWidth(Head head, Integer columnIndex) {
                return null;
            }
        };
    }

}


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CentOS7.9.2009安装elasticsearch7.11.1(单节点)

本文章使用CentOS7.9.2009服务器安装elasticsearch7.11.1软件 1.服务器信息 [root@elasticsearch ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [root@elasticsearch ~]# [root@elasticsearch ~]# cat /etc/hosts | grep elasticsearch 192.168.10.24…

如何理解图像处理领域的病态问题(ill-posed problem)

ill-posed problem&#xff0c;我们可以理解为病态问题或者不适定问题。在本文中&#xff0c;统一成为不适定问题。 在讨论不适定问题&#xff08;ill-posed problem&#xff09;之前&#xff0c;我们先来看一下什么叫适定性问题&#xff08;well-posed problem&#xff09;。…

14届蓝桥杯 C/C++ B组 T7 子串简写 (字符串)

采用存储目标字符下标的方法&#xff0c;此题的想法比较新奇&#xff0c;故予以记录。 存好下标之后&#xff0c;可以先定位好启始的字符&#xff0c;然后去搜结尾字符符合长度k并且最靠近启始字符的下标&#xff0c;找到之后可以直接取到这个下标之后的所有下标&#xff0c;因…

3d怎么在一块模型上开个孔---模大狮模型网

在进行3D建模时&#xff0c;有时候需要在模型上创建孔&#xff0c;以实现特定的设计需求或功能。无论是为了添加细节&#xff0c;还是为了实现功能性的要求&#xff0c;创建孔都是常见的操作之一。本文将介绍在3D模型上创建孔的几种常用方法&#xff0c;帮助您轻松实现这一目标…

免费全开源,功能强大的多连接数据库管理工具:DbGate

DbGate&#xff1a;您的全能数据库指挥中心&#xff0c;一站式免费开源解决方案&#xff0c;无缝连接并管理多款主流数据库&#xff0c;让复杂的数据世界变得轻松易控! - 精选真开源&#xff0c;释放新价值。 概览 DbGate 是跨平台的数据库管理器。支持 MySQL、PostgreSQL、SQ…

新零售SaaS架构:客户管理系统架构设计(万字图文总结)

什么是客户管理系统&#xff1f; 客户管理系统&#xff0c;也称为CRM&#xff08;Customer Relationship Management&#xff09;&#xff0c;主要目标是建立、发展和维护好客户关系。 CRM系统围绕客户全生命周期的管理&#xff0c;吸引和留存客户&#xff0c;实现缩短销售周…

chrome 浏览器 有自带的自动字幕功能,支持英文,控制您的音乐、视频等媒体内容

chrome 浏览器 有自带的自动字幕功能&#xff0c;支持英文&#xff0c;控制您的音乐、视频等媒体内容

Android Studio学习15——多页面情况下再看Activity生命周期

按返回键退出APP时&#xff1a; 走正常页面的退出流程&#xff1a;onPause–>onStop–>onDestroy(会Destroy,因为它从任务栈中退出了) 再点击图标回来时&#xff1a; 走正常页面的创建流程&#xff1a;onCreate–>onStart–>onResume 按Home键退出App时&#xff1a…

【C#】yield使用

&#x1f4bb;代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace ConsoleApp15 {internal class Program{static void Main(string[] args){IEnumerable<int&…

深度比较Vue 3.0中的computed和watch属性用法与最佳实践

摘要&#xff1a;在Vue 3.0中&#xff0c;computed和watch属性是用于处理数据逻辑的重要工具。本文将详细对比这两个属性的工作原理、适用场景以及使用时的注意事项&#xff0c;旨在帮助开发者更有效地选择和使用它们。 一、computed属性 computed属性是Vue 3.0中用于计算数据…

【小白学机器学习10】假设检验之1:F检验,F检验量的构造,F分布,F分布查表求P值等

目录 1 什么是F检验 F-test 1.1 F-test的定义 1.1.1 维基百科对F检验的定义 1.1.2 百度百科的定义 1.2 F检验的别名 1.3 F检验的判断手段 / 要达成的目标 / 适用范围 1.3.1 判断手段 1.3.2 对H0原假设的理解 1.3.3 判断目标/目的 1.3.4 适用的范围&#xff0c;场合 …

C++【组合模式】

简单介绍 组合模式是一种结构型设计模式&#xff0c; 只有在可以将对象拆分为【树状结构】的情况下使用。并且像使用独立对象一样使用它们。 常用于表示与图形打交道的用户界面组件或代码的层次结构。 基础理解 Q&#xff1a;为什么要用组合模式 &#xff1f; A&#xff1a;在…

每天学习一个Linux命令之curl

每天学习一个Linux命令之curl 在Linux系统中&#xff0c;有很多有用的命令可以帮助我们与网络进行交互。一个非常常用的命令是curl&#xff0c;它是一个功能强大的工具&#xff0c;可用于发送、接收和处理各种网络请求。本文将详细介绍在Linux下使用curl命令的各种选项及其用法…

011_C标准库函数之<time.h>

头文件<time.h>中说明了一些用于处理日期和时间的类型和函数。其中的一部分函数用于处理当地时间&#xff0c;因为时区等原因&#xff0c;当地时间与日历时间可能不相同。clock_t和time_t是两个用于表示时间的算术类型&#xff0c;而struct tm则用于存放日历时间的各个成…

QT学习day1

#include "mywidget.h"myWidget::myWidget(QWidget *parent): QWidget(parent) {this->resize(645,455);//设置窗口大小this->setWindowTitle("QQ");//设置窗口标题this->setWindowIcon(QIcon("D:\\QQ\\1579398717\\FileRecv\\pictrue\\qq.p…

Linux——线程互斥与互斥锁的使用

目录 前言 一、进程线程间的互斥相关背景概念 二、互斥量&#xff08;互斥锁&#xff09; 三、互斥锁的使用 1.互斥锁的初始化 2.加锁与解锁 3.锁的使用 4.锁的封装 四、线程饥饿 五、互斥锁的原理 六、死锁 前言 我们学习过线程概念与线程控制&#xff0c;知道了线…

Django项目定时任务django-crontab

首先定义一个定时任务函数tasks.py&#xff08;见文章末尾示例&#xff09;&#xff0c;编写函数&#xff0c;然后在setting.py中配置定时任务 1、首先安装django-crontab pip install django-crontab 2、在setting.py中添加应用 (在所有自定义注册app之上) INSTALLED_APPS …

第四百四十六回

文章目录 1. 概念介绍2. 使用方法3. 示例代码4. 经验与总结4.1 经验分享4.2 内容总结 我们在上一章回中介绍了"overlay_tooltip简介"相关的内容&#xff0c;本章回中将再谈flutter_launcher_icons包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

Advanced RAG 02:揭开 PDF 文档解析的神秘面纱

编者按&#xff1a; 自 2023 年以来&#xff0c;RAG 已成为基于 LLM 的人工智能系统中应用最为广泛的架构之一。由于诸多产品的关键功能&#xff08;如&#xff1a;领域智能问答、知识库构建等&#xff09;严重依赖RAG&#xff0c;优化其性能、提高检索效率和准确性迫在眉睫&am…

Android详细介绍POI进行Word操作(小白可进)

poi-tl是一个基于Apache POI的Word模板引擎&#xff0c;也是一个免费开源的Java类库&#xff0c;你可以非常方便的加入到你的项目中&#xff0c;并且拥有着让人喜悦的特性。 一、使用poi前准备 1.导入依赖&#xff1a; 亲手测过下面Android导入POI依赖的方法可用 放入这个 …