
简介这是一套面向企业级数据分析场景的开源BI平台源码适用于大数据开发、后端工程师及BI报表二次开发者解决轻量级Web端多维分析、自助SQL查询与拖拽式大屏可视化等核心需求。资源含1955个文件以591个.exsElixir服务端逻辑、581个.exElixir模块、112个.tsxTypeScriptReact前端组件及91个.csv电商、用户行为等示例数据集为主辅以部署脚本、配置文件与文档整体压缩包仅3.6MB结构精简且开箱即用。已有22人学习下载适合快速验证OLAP分析、报表设计器、大屏联动等工业级功能。读者可直接获取完整前后端源码、MySQL初始化脚本、多类型数据源接入示例、含权限控制的RESTful API设计范式以及基于Spring BootVue 3的技术栈落地实践代码覆盖策略模式、工厂模式等设计思想具备高可维护性与二次扩展能力。1. 开源BI数据分析平台源码不是“又一个Dashboard模板”而是能跑通从MySQL接入→ETL清洗→多维钻取→大屏自动轮播的完整Web工程你手头有一堆MySQL里的销售、用户、订单表想快速搭个内部用的分析看板——但试过Metabase发现权限太粗、Superset部署卡在Python依赖、Redash导出PDF总糊、轻量级工具又不支持下钻联动。这个开源BI项目不是UI组件库也不是配置型SaaS前端而是一套可编译、可调试、可嵌入、可二次开发的全栈Web项目后端用Spring Boot MyBatis-Plus做数据服务层前端用Vue 3 TypeScript ECharts 5 DataV非商业版构建响应式报表与大屏内置完整的RBAC权限模型、SQL执行沙箱、定时任务调度器XXL-JOB集成、以及关键的「维度建模元数据管理」模块。它解决的不是“怎么画柱状图”而是“如何让业务人员在不写SQL的前提下通过拖拽字段生成合法、可审计、带缓存策略的聚合查询”。适合中小团队技术负责人、BI工程师、或需要交付定制化数据看板的外包开发者——你得愿意改Java Service层逻辑、能看懂Vue Composition API里的useDataQuery组合式函数、并接受它不提供一键安装包必须本地编译部署。2. 搭建前必读为什么选这套技术栈Spring Boot Vue 3 ECharts 5 的组合不是凑数而是为解决三个硬伤2.1 后端选型Spring Boot 2.7.x MyBatis-Plus 3.5.x —— 不为“新”而为“稳”和“可控”这套BI平台后端没上Spring Boot 3.x或Quarkus核心原因就两条一是生产环境MySQL驱动兼容性尤其老版本5.1.47Spring Boot 2.7.x对JDBC 4.2支持更成熟二是MyBatis-Plus 3.5.x的LambdaQueryWrapper在动态拼接多维过滤条件时比MyBatis-Plus 4.x的泛型推导更少报错。项目里所有数据查询都走BaseMapperT封装的selectPage()selectList()双路径避免直接写XML导致SQL注入风险——但注意它不拦截Select(SELECT * FROM ${table})这种硬编码表名这是你二次开发时必须加白名单校验的点。// src/main/java/com/bi/core/service/impl/ReportServiceImpl.java public IPageReportData queryReportData(PageReportData page, ReportQueryDTO queryDTO) { // 关键queryDTO中dimensionList、metricList、filterList全部经ValidateUtils校验 // 表名、字段名强制映射到元数据字典sys_dim_table / sys_dim_field QueryWrapperReportData wrapper new QueryWrapper(); wrapper.apply(11); // 防止空wrapper被忽略 if (CollectionUtils.isNotEmpty(queryDTO.getFilterList())) { queryDTO.getFilterList().forEach(filter - { // 这里会查sys_dim_field表确认filter.fieldCode是否属于当前reportId关联的维度表 String realField dimFieldService.getRealFieldName(filter.getFieldCode()); wrapper.and(i - i.apply({0} {1} {2}, realField, filter.getOperator(), filter.getValue())); }); } return reportDataMapper.selectPage(page, wrapper); }提示realField不是直接拼接而是通过dimFieldService.getRealFieldName()查元数据表获取物理字段名比如逻辑字段“城市”对应物理表user_info.city_name。这是防止业务方乱填字段名的核心防线。2.2 前端架构Vue 3 Composition API Pinia —— 解耦报表逻辑与渲染逻辑报表编辑器和大屏设计器是两个独立路由模块但共享同一套useReportEngine()组合式函数。它把“指标定义”、“维度分组”、“筛选器状态”、“图表配置项”全部抽成响应式ref而非Vuex全局store——因为大屏要支持多实例如同时开销售大屏库存大屏每个实例需独立状态。ECharts配置不是JSON Schema硬编码而是通过echartsOptionBuilder.ts动态生成传入{ type: bar, xField: province, yField: order_amount }自动补全tooltip、legend、grid等基础项并注入主题色从themeConfig.json读取。// src/composables/useReportEngine.ts export function useReportEngine() { const reportConfig refReportConfig({ id: , name: , dimensions: [], // [{ code: province, name: 省份, type: string }] metrics: [], // [{ code: order_amount, name: 订单金额, type: number, agg: sum }] filters: [] // [{ field: date, operator: , value: 2024-01-01 }] }) const buildEchartsOption computed(() { const option: EChartsOption { tooltip: { trigger: axis }, legend: { data: reportConfig.value.metrics.map(m m.name) }, xAxis: { type: category, data: [] }, yAxis: { type: value }, series: [] } // 根据dimensions.length自动判断xAxis类型1维category2维cartesianN维treeMap... if (reportConfig.value.dimensions.length 1) { option.xAxis.data getDimensionValues(reportConfig.value.dimensions[0].code) } return option }) return { reportConfig, buildEchartsOption, loadReportData: async () { /* 调用/api/report/data接口 */ } } }2.3 可视化引擎ECharts 5 DataV 3.0 —— 为什么不用AntV或Chart.jsECharts 5的dataset机制天然适配多维分析场景dataset.source可直接绑定二维数组或对象数组encode字段声明维度/指标映射关系无需手动map()转换DataV 3.0则专攻大屏——它的dv-border-box-12、dv-scroll-board组件自带抗锯齿、滚动性能优化且支持v-show控制显隐比v-if更适合大屏轮播。项目里所有大屏组件都继承自BaseScreenComponent强制实现onResize()和onRefresh()生命周期钩子确保浏览器窗口缩放或定时刷新时图表重绘不卡顿。注意DataV 3.0的dv-decorate-1装饰框在高DPI屏幕如Mac Retina下有1px描边模糊问题已在src/assets/styles/dv-fix.css中用transform: scale(0.999)临时修复——这不是hack是ECharts官方推荐的DPI适配方案。3. 本地编译部署从源码到可访问的http://localhost:8080绕不开的五个实操步骤3.1 环境准备JDK 11 Node.js 16.15.0 MySQL 5.7 —— 版本锁死是刚需项目pom.xml明确指定java.version11/java.version若用JDK 17会触发MyBatis-PlusLambdaQueryWrapper泛型擦除异常前端package.json中engines: {node: 16.15.0}Node.js 18会导致vue-cli-service build生成的index.html中script标签缺少typemodule进而使Pinia初始化失败。MySQL必须5.7非8.0因项目使用GROUP_CONCAT函数且未设置group_concat_max_lenMySQL 8.0默认值为1024而大屏元数据导出常超此限。# 验证JDK版本必须11 java -version # 输出应为openjdk version 11.0.20 2023-07-18 # 验证Node版本必须16.15.0 node -v # 输出应为v16.15.0 # MySQL检查登录后执行 SELECT VERSION(); SHOW VARIABLES LIKE group_concat_max_len; # 若返回1024执行SET GLOBAL group_concat_max_len 1000000;3.2 数据库初始化四张核心表不能少sys_dim_table是元数据心脏项目不提供一键建库脚本需手动执行sql/init.sql。重点不是建表而是填充四张系统表表名关键字段必填说明sys_dim_tabletable_code,table_name,db_type,db_urltable_code必须与后端application.yml中bi.datasource.table-mapping一致如sales_order对应sales_order表sys_dim_fieldfield_code,table_code,physical_name,data_type,is_dimensionis_dimension1表示该字段可拖拽为X轴is_dimension0且agg_type IN (sum,avg)才允许作为指标sys_reportreport_code,name,typechart/screentypescreen的大屏会自动加载dv-*组件typechart走ECharts渲染sys_user_roleuser_id,role_code默认管理员账号admin/admin123角色ROLE_ADMIN需在此表关联提示sys_dim_field.physical_name必须是真实数据库字段名如order_amount但field_code可自定义如amt前端拖拽时显示field_code后端查询时转为physical_name。3.3 后端编译跳过测试、排除HikariCP冲突、指定profile项目含单元测试但无Mock数据mvn clean package -Dmaven.test.skiptrue是安全选择。关键参数是-Pprod激活生产profile并通过-Dspring.profiles.activeprod覆盖application.yml中的spring.profiles.active否则会加载dev配置导致连接本地H2数据库。cd bi-backend mvn clean package -Dmaven.test.skiptrue -Pprod \ -Dspring.profiles.activeprod \ -Dmaven.compiler.source11 \ -Dmaven.compiler.target11 # 生成target/bi-backend-1.0.0.jar3.4 前端构建public目录下的config.js决定API地址前端不走环境变量而是读取public/config.js// public/config.js window.GLOBAL_CONFIG { API_BASE_URL: http://localhost:8080/api, WS_BASE_URL: ws://localhost:8080/ws, THEME: dark // light | dark }修改后执行cd bi-frontend npm install npm run build # 生成dist/目录需将dist/内容复制到后端resources/static/3.5 启动与验证java -jar后必须检查三个端点java -jar target/bi-backend-1.0.0.jar启动成功后验证http://localhost:8080/api/health返回{status:UP}http://localhost:8080/api/sys/user/loginPOST{username:admin,password:admin123}返回tokenhttp://localhost:8080/加载首页F12查看Network →config.js是否加载Console无Uncaught ReferenceError: GLOBAL_CONFIG is not defined注意若首页空白90%是public/config.js未被正确复制到后端resources/static/目录——Spring Boot静态资源路径为classpath:/static/不是/public/。4. 报表开发实战从零创建一张“各省份销售额TOP10”柱状图看清数据流转全链路4.1 元数据注册在sys_dim_table和sys_dim_field中定义销售事实表假设你的MySQL有表sales_fact含字段province字符串、order_amount数字、order_date日期。需插入-- 插入维度表 INSERT INTO sys_dim_table (table_code, table_name, db_type, db_url) VALUES (sales_fact, sales_fact, mysql, jdbc:mysql://localhost:3306/bi_db?useSSLfalse); -- 插入维度字段province INSERT INTO sys_dim_field (field_code, table_code, physical_name, data_type, is_dimension, sort_order) VALUES (province, sales_fact, province, string, 1, 1); -- 插入指标字段order_amount INSERT INTO sys_dim_field (field_code, table_code, physical_name, data_type, is_dimension, agg_type, sort_order) VALUES (amt, sales_fact, order_amount, number, 0, sum, 2);提示field_code是前端显示名physical_name是数据库真实字段。agg_type必须为sum/avg/count/max/min之一否则前端指标下拉框不显示该字段。4.2 创建报表POST/api/report提交JSON定义调用接口前先用Postman或curl提交{ reportCode: province_sales_top10, name: 各省份销售额TOP10, type: chart, dataSource: sales_fact, dimensions: [province], metrics: [{fieldCode: amt, aggType: sum}], filters: [{fieldCode: order_date, operator: , value: 2024-01-01}], chartType: bar, sort: {fieldCode: amt, direction: desc}, limit: 10 }后端会校验dataSource是否在sys_dim_table中存在dimensions中每个fieldCode是否关联is_dimension1的字段metrics中fieldCode是否关联is_dimension0且agg_type匹配校验通过后生成report_config记录并返回reportId。4.3 查询数据GET/api/report/data?id{reportId}触发动态SQL生成请求该接口时后端执行根据reportId查sys_report和sys_report_config拼接SQLSELECT province AS x, SUM(order_amount) AS y FROM sales_fact WHERE order_date 2024-01-01 GROUP BY province ORDER BY y DESC LIMIT 10执行查询结果转为[{x: 广东, y: 1250000}, ...]格式返回前端useReportEngine.loadReportData()拿到数据后自动注入buildEchartsOption的series.data。4.4 大屏嵌入用dv-chart组件复用同一份报表配置在大屏页面如/screen/sales-dashboard中template dv-chart :configchartConfig stylewidth:100%;height:400px; / /template script setup import { ref, onMounted } from vue const chartConfig ref({}) onMounted(async () { // 复用已创建的报表ID const res await fetch(/api/report/config?idprovince_sales_top10) chartConfig.value await res.json() // 此时chartConfig包含dimensions/metrics/filters供dv-chart内部调用/api/report/data }) /script关键dv-chart组件内部会监听chartConfig变化自动触发数据拉取无需手动调用loadReportData——这是DataV与ECharts的融合设计。5. 避坑指南这五个血泪问题90%的人第一次部署都会翻车5.1 现象登录成功但首页空白Network里config.js404原因前端npm run build生成的dist/目录未复制到后端src/main/resources/static/而是放在了错误路径如target/classes/static/解决确认bi-backend/src/main/resources/static/config.js存在且内容为window.GLOBAL_CONFIG {...}若用IDEA检查Project Structure → Modules → bi-backend → Resources路径是否指向src/main/resources5.2 现象报表编辑器里维度字段下拉为空原因sys_dim_field.table_code与sys_dim_table.table_code不一致或sys_dim_field.is_dimension0解决执行SELECT f.field_code, f.table_code, t.table_code FROM sys_dim_field f LEFT JOIN sys_dim_table t ON f.table_code t.table_code WHERE t.table_code IS NULL查孤儿字段再查SELECT * FROM sys_dim_field WHERE table_codeyour_table AND is_dimension15.3 现象大屏图表显示“数据加载中...”但Network无请求原因dv-chart组件未正确接收config常见于chartConfig响应式失效如用let config {}未用ref解决确保chartConfig为ref({})且onMounted中赋值后触发更新检查dv-chart.vue源码确认其watch监听的是props.config而非内部变量5.4 现象导出PDF时中文乱码柱状图X轴文字显示为方块原因后端ECharts SVG导出未嵌入中文字体且application.yml中spring.servlet.context-path非/时字体URL路径错误解决在bi-backend/src/main/resources/application-prod.yml中添加echarts: font-path: classpath:static/fonts/simhei.ttf # 提前放入resources/static/fonts/并在EChartsExporter.java中option.setRenderer(svg)前设置option.setTextStyle(new TextStyle().setFont(14px simhei))5.5 现象定时任务XXL-JOB执行报表快照失败日志报No DataSource set原因xxl-job-executor模块未正确注入DataSourceBean因MapperScan扫描路径未覆盖bi-core模块的Mapper解决在XxlJobConfig.java中MapperScan的basePackages必须包含com.bi.core.mapper且XxlJobExecutor类上加EnableTransactionManagement提示XXL-JOB执行器默认每5秒心跳一次若网络延迟高需在xxl-job-admin后台将执行器-注册方式改为自动注册并检查bi-backend的application-prod.yml中xxl.job.executor.appname与后台配置一致。6. 进阶技巧让大屏真正“活”起来——基于WebSocket的实时数据推送与跨屏联动6.1 实时数据流用WebSocket替代轮询降低后端压力项目内置/ws/report/{reportId}端点但默认关闭。启用需三步在application-prod.yml中开启websocket: enabled: true path-pattern: /ws/**前端在报表页建立连接// src/views/report/ReportView.vue onMounted(() { const ws new WebSocket(ws://localhost:8080/ws/report/${route.params.id}) ws.onmessage (e) { const data JSON.parse(e.data) if (data.type refresh) { // 触发图表重绘而非整页reload chartRef.value?.refresh(data.payload) } } })后端在ReportService中注入SimpMessagingTemplate当sales_fact表有新记录插入时通过MySQL Binlog监听或应用层事件发送simpMessagingTemplate.convertAndSend( /topic/report/ reportId, Map.of(type, refresh, payload, latestData) );注意WebSocket消息体必须为JSON字符串convertAndSend会自动序列化但payload字段建议只传增量数据如[{x:广东,y:12000}]而非全量避免网络拥塞。6.2 跨屏联动点击大屏某省份联动其他大屏筛选该省份数据核心是BroadcastChannel API——现代浏览器原生支持同源页面间通信无需后端中转// 大屏A省份地图点击事件 document.getElementById(map).addEventListener(click, (e) { const channel new BroadcastChannel(bi-screen-link) channel.postMessage({ type: province-select, payload: { province: e.target.dataset.province } }) }) // 大屏B监听事件并刷新图表 const channel new BroadcastChannel(bi-screen-link) channel.addEventListener(message, (e) { if (e.data.type province-select) { // 更新本地filters触发useReportEngine重新拉取 reportConfig.value.filters [ { fieldCode: province, operator: , value: e.data.payload.province } ] } })提示BroadcastChannel在Chrome/Firefox/Edge中支持良好Safari 15.4支持。若需兼容旧版Safari降级为localStorage事件window.addEventListener(storage, ...)但会有100ms延迟。6.3 权限精细化按数据行级控制报表可见性项目RBAC只到菜单/按钮级行级权限需扩展。例如销售总监只能看自己部门数据。做法是在ReportServiceImpl.queryReportData()中插入// 获取当前用户部门ID Long deptId SecurityUtils.getCurrentUserDeptId() if (deptId ! null !queryDTO.getFilters().stream() .anyMatch(f - dept_id.equals(f.getFieldCode()))) { // 自动追加部门过滤条件 queryDTO.getFilters().add(new ReportFilter() .setFieldCode(dept_id) .setOperator() .setValue(String.valueOf(deptId)) ) }然后在sys_dim_field中为dept_id字段标记is_security_sensitive1前端报表编辑器隐藏该字段——业务方无法手动添加但系统自动注入。从那以后我每次交付BI项目都强制走一遍「元数据注册→报表创建→大屏嵌入→WebSocket压测→行级权限验证」五步 checklist哪怕客户说“先看看效果”也绝不跳过元数据表填充——因为90%的后续问题根子都在sys_dim_field.is_dimension填错了。希望帮到你。本文还有配套的精品资源点击获取