0
点赞
收藏
分享

微信扫一扫

安卓APP的开发:为了安全的设计

文章目录

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

优化结果

原积木导出有两种导出,直接导出和大数据导出(大数据导出是做了优化去掉了一些样式之类的,性能更好)
实测中发现
原积木大数据导出性能:1万条数据导出耗时30秒,1.5万条耗时1.5分钟导出失败,数据超过一万条后经常导出失败,还会导致容器实例探活失败/内存撑爆重启
异步+自实现导出功能优化方案导出性能:34.6万条数据一次导出耗时2.5分钟,导出时快速返回空数据文件,避免页面死等不可用,导出后的表格是在线OSS的文件URL直接每个人都可以快速下载,可随时多次下载。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

需求背景和解决方案的思考

解决方案

流程描述:

识别并拦截积木导出操作,透传导出时参数生成自己设计的导出任务,异步执行导出任务(自行实现积木导出的逻辑),并使用高性能的阿里巴巴开源的EasyExcel工具,最后将EasyExcel导出的表格文件上传到OSS变为表格URL(记得做安全防护,避免表格泄漏)

积木的逻辑
积木前端导出按钮->积木导出接口->分页调取你的数据接口拉数据->生成excel返回

关键代码

引入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);
    }
}

报表下载任务处理事件
/**
 * @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;
            }
        };
    }

}


举报

相关推荐

0 条评论