卖了几年的 BI 模块突然免费:24 种图表,一条 SQL 一张图

「请勿将源码泄漏给他人」

这句话,写在一个叫 erupt-bi 的仓库 README 的第二行。

再往下是一个语雀文档链接,带访问密码 tcrk。最后一行写着:「编译成果 ------ 在项目 Git 标签栏目中有各版本的 jar」。

只发 jar,不发源码,文档要密码。 这是很典型的商业模块分发方式 ------ 这个模块确实卖了几年。

现在最有意思的地方来了:这份 README,今天还原封不动地躺在开源仓库里。

因为随着 erupt 2.1.0 发布,erupt-bi 改名 erupt-report,整体并入主仓库,跟着仓库一起挂上了 Apache License 2.0。作者忘了删那份旧 README。

所以你现在 clone 下来,能看到一个开源模块的 README 上写着"请勿将源码泄漏给他人"。

时间线是这样的:

  • 2026-07-25 ------ 私有仓库的最后一次常规提交
  • 2026-07-27 ------ 三次同名提交 Erupt BI rename to Erupt Report,新旧两个仓库同一天完成改名
  • 2026-07-30 ------ commit 224dcb41dAdd erupt-report BI module with report config, data sources, and charts这就是开源的那一刻
  • 2026-08-17 ------ 目录从 erupt-report/ 移进 erupt-plugin/erupt-report/
  • 2026-08-23 ------ erupt 2.1.0 正式发版,Report 随之开源

从头到尾一个人完成:YuePeng。

下面说说这东西到底值不值得你花 30 分钟。


一、它是什么:纯 SQL 定义报表和图表

一句话:在你现有的 Spring Boot 工程里,用 SQL 定义报表和图表,零前端代码。

它不是一个独立部署的 BI 平台。它是一个 Maven 依赖:

xml 复制代码
<dependency>
    <groupId>xyz.erupt</groupId>
    <artifactId>erupt-report</artifactId>
    <version>2.1.0</version>
</dependency>

引进去,启动,后台侧边栏自动多出一个「报表管理」根菜单,下面挂好 7 个子菜单:数据源管理、报表处理类、组件模板、参照维度、函数管理、分组管理、报表配置。

不用改一行代码,不用建一张表,不用配一个 bean。

图表引擎是 G2Plot,24 种图表类型。


二、30 分钟:从零到一张能看的报表

第一步:新建报表,粘一段 SQL

进「报表配置 → 新增」,填个名称,把你平时在 Navicat 里跑的那条 SQL 粘进去。编码自动生成。

就这样。保存后点「效果预览」,表格已经出来了 ------ 列名、列宽、分页、排序全是自动的。

这里有个细节值得说 :你不需要配置任何列。系统拿结果集第一行的字段名逐列比对,没有对应列配置的列会自动按「文本、可显示、不可排序」生成。想调某一列的宽度、类型(数值 / 时间 / 百分比进度条 / 链接 / 长文本收起)、是否显示,再去「列配置」里加就行 ------ 只配你要改的那几列

第二步:加查询条件

在同一个表单的「查询维度」Tab 里加搜索项。21 种维度类型:文本、标签、数值、数值区间、日期、时间、日期时间、周、月、年、日期区间、日期时间区间、单选参照、多选参照、Radio 参照、Checkbox 参照、单选树参照、多选树参照、级联选择参照、单选表格参照、多选表格参照。

每个维度有个「编码」------ 这个编码就是 SQL 里的变量名

第三步:让 SQL 认识这些条件

这是整个模块设计最漂亮的一环,值得单独讲,见下一节。

第四步:挂图表

在报表行下钻进「图表配置」,选类型 + 写一条图表 SQL。一张报表可以挂多个图表,支持拖拽排序,每个图表能单独设置栅格宽度(24 格制)、高度、缓存时间,甚至可以用和主表不同的数据源

第五步:发布到菜单

报表行点「添加到菜单」,填菜单名和挂载位置。菜单编码 = 报表编码。刷新页面,侧边栏就有了。


三、SQL 里到底能写什么

这是 erupt-report 和"配置化 BI 工具"的分水岭。它没有发明一套查询 DSL,而是在 SQL 上开了三个口子

口子一:6 个上下文变量

bash 复制代码
${__uid__}         当前登录用户 ID
${__request__}     HttpServletRequest
${__response__}    HttpServletResponse
${__pageIndex__}   分页索引
${__pageSize__}    分页大小
${__export__}      布尔值,导出时为 true

${__uid__} 是最常用的 ------ 一句话实现"只看自己的数据":

sql 复制代码
select * from t_order where owner_id = ${__uid__}

${__export__} 有个巧妙用法:导出时才关联那张很慢的明细表,页面浏览时不关联。

多租户等 MetaContext 里的上下文变量也会一起注入,直接在 SQL 里取。

口子二:${} 里跑的是真 JavaScript

${} 内的内容会被交给 JS 引擎求值。你可以写 select '${1 + 1}' 验证一下 ------ 它真的会算。

引擎是 org.openjdk.nashorn:nashorn-core:15.7 ,模块自带,不依赖 JDK 内置的 Nashorn(JDK 15 已经把它移除了)。默认 ECMAScript 5.1,加一个 JVM 参数 -Dnashorn.args=--language=es6 就能用 let/constMap/Setfor..of

因为跑在 JVM 上,JS 里可以直接调 Java API。比如给日期区间维度设默认值:

javascript 复制代码
java.util.Arrays.asList('2026-01-01', '2026-08-01')

口子三:可以自己写函数

「函数管理」菜单里在线写 JS 函数,保存时立即 eval 校验语法,约 1.5 秒后全局生效。

首次启动会自动灌入 4 个内置函数,它们解决的是同一个老问题:查询条件没填时,不要拼进 where

写法 生成的 SQL
${and('status','st')} and status=:st
${like('name','kw')} and name like '%值%'
${In('dept_id','depts')} and dept_id in (:depts)
${range('create_time','dt')} and create_time between 'a' and 'b'

值为空时函数返回空,表达式被替换成空串 ------ 条件没填,SQL 里就干脆没有那一段range() 还支持单边:只填开始就生成 >=,只填结束就生成 <=

于是一条能应付各种筛选组合的 SQL 长这样:

sql 复制代码
select dept_name, count(1) cnt, sum(amount) total
from t_order
where 1=1
  and owner_id = ${__uid__}
  ${like('customer', 'kw')}
  ${In('dept_id', 'deptIds')}
  ${range('create_time', 'dateRange')}
group by dept_name

注意 andIn 生成的是 :st / :depts 这样的命名参数 ------ 值走 JDBC 绑定,不是字符串拼接。 函数只负责拼结构。


四、24 种图表,每种都只是一条 SQL

数值统计、文本提示、折线、阶梯折线、柱状、堆叠柱状、面积、百分比面积、条形、百分比条形、雷达、散点、气泡、饼图、环形、玫瑰、玉珏、漏斗、瀑布、词云、桑基、弦图、数据表、组件模板。

这里有个必须知道的规则 :每种图表对 SQL 返回列有约定,而且是按列的顺序取,不按列名

  • 大部分图(折线 / 柱状 / 面积 / 条形 / 雷达 / 散点):2~3 列 = 名称 / 数值 / 分类
  • 饼图 / 环形 / 玫瑰 / 漏斗 / 瀑布:2 列 = 名称 / 数值
  • 桑基图 / 弦图:3 列 = 名称 / 数值 / 目标名称
  • 气泡图:4 列 = x / y / 系列 / 大小
  • 数值统计:1~2 列 = 数值 / 名称

所以一张环形图就是:

sql 复制代码
select status, count(1) from t_order group by status

列别名叫什么无所谓,select 的顺序就是语义。多出来的列会被丢弃。

如果 24 种都不够 ------ 「组件模板」类型让你用 Freemarker 自己写渲染逻辑,模板里能拿到 ${dataJson}(JSON 串)、data(List 对象)和 request。要接 ECharts 的某个冷门图,从这里进。

自定义 G2Plot 配置项也支持,图表配置里有个 JSON 输入框直通底层。


五、下钻:一列变成一个入口

表格里某一列想点进去看明细?列配置里把「类型」选成下钻,然后写一条下钻 SQL:

sql 复制代码
select * from t_order_item where order_id = :id

:列名 是当前行的字段。当前行的所有列都会作为命名参数传过去 ------ 包括你设置成"不显示"的列。所以主键 id 可以藏起来不占版面,但依然能当钻取键用。


六、多数据源:报表要查的库,通常不是业务库

「数据源管理」里可以配任意多个 JDBC 数据源,报表、图表、参照维度三处各自独立选择用哪个。不选就走 Spring Boot 的默认数据源。

三个实现细节能看出这是个跑过生产的模块:

第一,每个 BI 数据源的 Hikari 连接池都 setReadOnly(true) 报表天然只读,从物理层面挡住误写。

第二,每条发出去的 SQL 都带注释:

sql 复制代码
/* erupt bi query :: 部门销售日报 */ select ...

DBA 在慢查询日志里一眼就能看出是哪张报表在拖数据库。这个细节我很喜欢。

第三,分页方言内置 12 种:MySQL、MariaDB、PostgreSQL、TiDB、Oracle、SQLServer 2012、达梦、人大金仓、ClickHouse、Impala、StarRocks,以及 Other。

选 Other 就自己写分页语句,用 4 个占位符:

sql 复制代码
select * from (@sql) _t @sort limit @size offset @skip

Apache Druid、Presto 这类都能这么接进来。(顺带一提,Impala 被单独特判了 ------ 它的 limit/offset 必须配 order by,模块会在没有排序字段时自动补 order by null。这种坑一般是踩过才知道。)

新建数据源的表单里有个**「测试连接」按钮**,保存前就能验通不通。驱动列表是从 DriverManager 里动态列出你 classpath 里真实注册的驱动 ------ 不会让你选一个根本没有的驱动。


七、真正的杀手锏是权限

如果只看图表能力,erupt-report 打不过专业 BI 工具。它的优势在别处。

一张报表发布后,就是一个普通的后台菜单。 于是:

  • 谁能看哪张报表 ------ 给角色勾菜单,和你系统里其他页面一模一样
  • 不需要在 BI 系统里再建一套用户,也不需要把组织架构同步过去
  • 报表里的行级数据权限,用 ${__uid__} 或处理类直接接你现有的逻辑
  • 操作日志、单点登录、多租户 ------ 全都自动生效

后端还有一道校验挺讲究:请求 header 里的 erupt 标识、URL 参数里的编码、路径里的报表 code,三者必须一致,否则直接抛无权限异常。防的是"用 A 报表的权限去查 B 报表的数据"。

这就是那张对比表想说的事:自研 ECharts 组件,权限要重写一遍;上 DataEase / Superset,要单独部署一套服务 + 一套用户体系还要做同步;erupt-report 这两项成本都是 0。


八、SQL 表达不了的时候

总有 SQL 搞不定的需求。模块留了一个口子:报表处理类

实现 EruptReportHandler,注册成 Spring Bean:

java 复制代码
@Service
public class SalesReportHandler implements EruptReportHandler {

    // SQL 执行前改写(拿到的是已经编译好占位符的 SQL)
    @Override
    public String exprHandler(String param, Map<String, Object> condition, String expr) {
        return expr + " and dept_id in (" + currentUserDepts() + ")";
    }

    // SQL 执行后加工结果集
    @Override
    public void resultHandler(String param, Map<String, Object> condition,
                              List<Map<String, Object>> result) { ... }

    // 导出 Excel 时定制 POI Workbook
    @Override
    public void exportHandler(String param, Map<String, Object> condition,
                              Workbook workbook) { ... }
}

三个方法都是 default,只实现你需要的那个。第一个参数 param 是在界面上配的,所以同一个处理类可以被多张报表复用,靠参数区分行为

处理类能绑在三个位置:报表、图表、参照维度。典型用途就是数据权限过滤 ------ 这也是为什么 exprHandler 会同时作用于查询 SQL 和统计 SQL,保证 count 和 list 的过滤条件永远一致。


九、还有一些顺手的东西

  • 修改记录:每次改查询 SQL 都自动存一条历史,记录改前改后的 SQL、操作人、时间。报表数据突然不对了,能立刻查是谁什么时候动了 SQL。这张表是全只读的,删不掉也改不了。
  • 缓存:默认 1 秒。别小看这 1 秒 ------ 一个页面上挂 6 个图表,切换筛选条件时的重复查询全被吃掉了。缓存 key 是「编译后的 SQL + 查询参数」,改条件就自然失效。
  • 自动刷新:配一个秒数,报表就变成了大屏。
  • 树型分组:报表多了以后,列表页左侧自动出分组树。
  • Excel 导出:默认开启,可以按报表关掉,关掉后后端接口直接拦截 ------ 不是只藏了按钮。

十、去试试

Maven 坐标就一行:

xml 复制代码
<dependency>
    <groupId>xyz.erupt</groupId>
    <artifactId>erupt-report</artifactId>
    <version>2.1.0</version>
</dependency>

依赖很轻:spring-boot-starter-jdbc + nashorn-core + freemarker + erupt 自己的 upms / tpl。整个模块 41 个 Java 文件。

不想装环境就直接看

  • 在线演示:demo.erupt.xyz ,账号 bi 密码 bi
  • 一条命令跑全套:docker run -d -p 8080:8080 erupts/erupt:2.1.0
  • 文档:docs.erupt.xyz → 模块 → Erupt Report
  • 源码:github.com/erupts/erupterupt-plugin/erupt-report

最后

开源一个卖过钱的模块,是个不太容易的决定。

对作者来说,它意味着一条收入线没了;对你来说,它意味着下次产品说"能不能加个销售趋势图"的时候,你不用再评估两周工时,也不用再申请一台机器部署 DataEase。

如果它帮你省了这两周,去 GitHub 点个 Star ------ 对一个一个人在维护的开源项目来说,Star 数直接决定了它能不能被下一个正在选型的团队看见,也决定了作者还有没有动力把下一个商业模块也开源。

相关推荐
dong_junshuai19 分钟前
每天一个开源项目#82 9.2K星God's Eye View:公开信号3D地球
开源·github
简创AIGC陶先生1 小时前
第1章:项目全景与架构总览
github
怕浪猫1 小时前
ZCode 周末送额度活动开启:3 亿 Token 免费领取
github
P1Browser2 小时前
指纹浏览器安全吗?从浏览器环境隔离、数据存储到安全风险解析
网络·tcp/ip·安全·网络安全·github·php
todoitbo5 小时前
用蓝耘元生代做 GitHub 热榜解读:Dify Chatflow 接入和真实项目分析
ai·github·api·dify·蓝耘
猫头虎5 小时前
GitHub 入门教程:如何加入并为开源项目贡献代码
gitee·开源·gitlab·github·开放原子·开源协议·gitcode
樱花落木兰5 小时前
Git 超全零基础教程|三区原理、全套命令、分支协作、冲突解决、IDEA 集成(面试必备)
ide·git·json·github
TunerT_TQ7 小时前
IBM |css-gridish 源码分析:4 个 JavaScript 文件背后的 CSS Grid 可视化工具
开源·github·资讯
TunerT_TQ7 小时前
Hugging Face| Candle 源码分析:702 个文件背后的 Rust 深度学习框架架构
开源·github·资讯