大屏动态数据接入:从静态 Mock 到真实业务 API

0 阅读10分钟

一、问题场景

大屏做出来之后,最常被问到的一个问题是:

"这个数字是写死的吗?"

C06 里 AI 生成的大屏,数据来自静态 Mock。演示时足够好看,但交给业务方使用,必须接入真实数据。这个"最后一公里"往往比生成大屏本身更棘手:

  • 数据源种类多:MySQL、PostgreSQL、Oracle、HTTP API……
  • 同一张报表,不同部门看到的数据范围不同(行级权限)
  • 手机号、身份证号不能直接显示(字段脱敏)
  • 大屏上的"部门"需要显示"研发部"而不是 dept_code=IT(维度翻译)
  • 查询参数要支持"最近 7 天"这种动态值,还要支持组件之间的联动传参

把这些需求逐一硬编码进每个图表组件,维护成本会迅速失控。需要一个统一的数据接入层,让大屏组件只关心"我要什么数据",而不关心"数据从哪来、怎么算出来的"。


二、解决方案:数据源 + 数据集双模块

Forge 的设计思路是把"数据从哪来"和"我要什么数据"拆成两个独立模块

数据连接(Data Connection)
    ↓  定义"能连到哪个数据库"
数据集(Data Dataset)
    ↓  定义"我要哪些字段、用什么条件过滤"
大屏组件
    ↓  绑定数据集 ID + 参数,运行时自动查询

2.1 数据连接:解决"能连到哪"

ai_report_data_connection 表存储数据库连接信息:

// 核心字段
connectionCode      // 连接编码
connectionName      // 连接名称
dbType              // 数据库类型(MySQL/PostgreSQL/Oracle...)
driverClassName     // JDBC 驱动类
jdbcUrl             // 连接地址
username            // 用户名
passwordCipher      // 密码(加密存储,非明文)
schemaName          // 默认 schema
testSql             // 测试 SQL
poolConfigJson      // 连接池配置(JSON)
status              // 0=禁用, 1=启用

管理后台提供完整的 CRUD 和"测试连接"功能,密码编辑时留空表示沿用原密码——这个细节很实用,避免每次修改连接都要重新填密码。

为什么选择 JDBC 直连而不是加一层数据中台?
中小型项目里,JDBC 直连的延迟最低、部署最简单。引入中间件意味着多一个故障点,也多一份运维成本。Forge 的定位是"能跑起来的数据大屏工具",而不是企业级数据中台,这个取舍是故意的。

2.2 数据集:解决"我要什么数据"

数据集是连接大屏组件和数据库之间的语义层。它不关心连接细节,只关心"查什么、怎么过滤、返回什么格式"。

系统支持两种数据集类型(DatasetTypeEnum):

类型说明适用场景
TABLE选表 + 选字段,系统自动生成 SQL快速配置,不需要写 SQL
SQL手写 SQL 查询语句复杂关联、子查询、多表 JOIN

ai_report_data_dataset 表核心字段:

datasetCode        // 数据集编码(唯一标识,前端绑定用这个)
datasetName        // 数据集名称
connectionId       // 关联的数据连接 ID
datasetType        // TABLE / SQL
tableName          // 表名(TABLE 类型时使用)
sqlText            // SQL 文本(SQL 类型时使用)
paramSchemaJson    // 查询参数定义(JSON 数组,核心!)
defaultOrderJson   // 默认排序(JSON)
maxRows            // 最大返回行数(默认 1000,防止全表扫)
timeoutSeconds     // 查询超时时间(秒)
cacheEnabled       // 是否启用缓存
cacheTtl           // 缓存 TTL(秒)
publishStatus      // DRAFT / PUBLISHED / OFFLINE

三、数据结构:三张核心表

3.1 数据集字段定义表(ai_report_data_dataset_field

数据集的字段不是随便 SELECT * 就完事,每个字段可以单独配置显示名、数据类型、角色(维度/指标)、聚合方式、脱敏规则等:

fieldName          // 字段名(对应数据库列名)
fieldLabel         // 字段标签(中文显示名,大屏上显示这个)
sourceColumn       // 源列名
dataType           // 前端数据类型(string/number/date/...)
fieldRole          // DIMENSION(维度)/ MEASURE(指标)
defaultAgg         // 默认聚合方式(SUM/AVG/COUNT/...)
queryEnabled       // 是否允许作为查询条件
displayEnabled     // 是否允许在结果中显示
sensitiveLevel     // 敏感等级(HIDDEN / MASK / CLEAR)
maskRule           // 脱敏规则(正则表达式)
dictType           // 字典类型(用于下拉筛选器的选项)
dateFormat         // 日期格式
dataUnit           // 数据单位(万元/个/%)
dimensionId        // 关联维度 ID(用于维度翻译)

fieldRole 的设计值得单独说一下:维度字段用于分组和筛选,指标字段用于聚合计算。这个区分在大屏前端的数据适配层会自动生效——维度字段默认出现在 X 轴或图例,指标字段默认出现在 Y 轴或数值显示。AI 生成大屏时也会参考这个角色信息来决策用哪种图表类型。

3.2 动态参数结构(paramSchemaJson

这是整个动态数据接入里最灵活的部分。数据集可以声明一组参数,大屏组件在请求数据时传入实际值,后端自动绑定到 SQL 中。

[
  {
    "paramName": "startDate",
    "label": "开始日期",
    "fieldName": "order_date",
    "operator": ">=",
    "defaultValue": "T-7",
    "required": true
  },
  {
    "paramName": "deptCode",
    "label": "部门",
    "fieldName": "dept_code",
    "operator": "=",
    "defaultValue": "",
    "required": false
  }
]

defaultValue: "T-7" 表示默认取 7 天前,后端会自动计算实际日期。支持的运算符:=, !=, >, >=, <, <=, LIKE


四、实现链路:一次运行时查询的完整流程

这是最核心的部分。当用户打开一个大屏页面,每个组件会发起一次数据请求,完整的调用链如下:

大屏组件(Vue 组件)
  ↓ useChartDataFetch hook
  ↓ requestDataType == DATASET(3) ?
  ↓ 是 → datasetRequest()
  ↓
customizeHttp() — 统一请求入口
  ↓
POST /data/dataset/runtime/query
  ↓
DataDatasetRuntimeController
  ↓
DataQueryExecutor.execute()
  ↓
  1. 加载数据集定义(含 paramSchemaJson)
  2. 绑定动态参数(请求参数 → SQL 命名参数)
  3. 应用行级权限条件(自动注入)
  4. 执行 JDBC 查询(PreparedStatement,防 SQL 注入)
  5. 维度翻译(dimensionId 关联字典,code → name)
  6. 字段脱敏(maskRule 正则替换)
  ↓
DataDatasetQueryResultVO
  { dimensions, source, total, fields }
  ↓
datasetAdapter.adaptDatasetForComponent()
  ↓  根据组件类型自动选择适配模式:
  ↓  ECharts 组件 → echartsDataset 格式
  ↓  TableScrollBoard → arrayRows 格式
  ↓  KpiCard → singleValue 格式
  ↓
渲染图表/表格

4.1 运行时查询接口

前端调用后端的核心接口:

// POST /data/dataset/runtime/query
{
  "datasetId": 123,
  "params": {
    "startDate": "2024-01-01",
    "endDate": "2024-12-31",
    "deptCode": "IT"
  },
  "fields": ["dept_name", "amount"],
  "pageNum": 1,
  "pageSize": 50,
  "maxRows": 1000,
  "outputMode": "ECHARTS_DATASET"
}

outputMode 控制返回格式,ECHARTS_DATASET 对应 ECharts 的 dataset 格式(dimensions + source),前端几乎可以原样传给 ECharts 实例。

4.2 SQL 安全:为什么不用字符串拼接

参数绑定通过 SqlParameterBinder 转换为 PreparedStatement 参数,而不是字符串拼接。这意味着:

-- 安全的做法(使用命名参数,PreparedStatement 预编译)
SELECT dept_name, SUM(amount)
FROM t_order
WHERE order_date >= :startDate
  AND dept_code = :deptCode
GROUP BY dept_name

-- 而不是:
SELECT ... WHERE order_date >= '${startDate}'   ← 这会被拦截

SqlSafetyValidator 会在执行前做 SQL 安全检查,防止明显的注入尝试。这不是银弹,但作为纵深防御的一层是有意义的。

4.3 前端动态参数:四种来源

大屏组件在请求数据时,参数值可以从四个来源自动获取(requestDynamicParams.ts):

来源说明示例
context用户上下文(userId、username、deptCode 等 18 个字段){ source: 'context', sourceKey: 'userId' }
pageContext页面上下文(区域、对象等信息){ source: 'pageContext', sourceKey: 'regionCode' }
component其他组件的当前值(组件联动){ source: 'component', componentId: 'xxx', componentField: 'value' }
preset预设值(T-N 日期偏移){ source: 'preset', presetType: 'tn-day-start', offsetDays: 7 }

preset 类型特别实用:大屏上"最近 7 天销售额"这种需求,不需要前端算日期,直接配 T-7 即可,后端自动算出 CURDATE() - INTERVAL 7 DAY

4.4 维度翻译

数据库里存的是 dept_code = 'IT',但大屏上要显示"研发部"。这个过程叫维度翻译,由 dimensionId 字段驱动:

数据集字段 dept_code,dimensionId = 5
  ↓ 查询维度表
  ↓ code=IT → name=研发部
  ↓ code=MK → name=市场部
  ↓
返回给前端的是翻译后的值

这个翻译在后端查询结果出来之后、返回前端之前完成,前端不需要关心 code 和 name 的映射关系。

4.5 字段脱敏

sensitiveLevel + maskRule 实现字段级脱敏:

// 示例:手机号脱敏
// maskRule: (\d{3})\d{4}(\d{4})
// 输入:13812345678
// 输出:138****5678

HIDDEN 级别直接返回空,MASK 级别按正则规则脱敏,CLEAR 级别原样返回。这个逻辑在查询结果映射阶段执行,确保脱敏后的数据不会泄漏到前端。


五、设计取舍

5.1 为什么选择 JDBC 直连而非中间件

方案优点缺点
JDBC 直连(当前方案)延迟低、部署简单、调试方便数据库连接数受限、无统一数据治理
数据中台/API 网关统一治理、权限集中管控运维成本高、引入额外故障点

结论:Forge 的定位是"能快速跑起来的数据大屏工具",目标用户是中小团队。JDBC 直连在这个场景下是最务实的选择。如果未来有企业级需求,可以扩展 forge-plugin-external 插件,通过外部 API 代理方式接入数据中台。

5.2 缓存策略:缓存什么、不缓存什么

cacheEnabled + cacheTtl 字段已定义,但缓存的实现需要权衡:

  • 可以缓存的:聚合结果、字典数据、维度翻译结果
  • 不应该缓存的:实时交易数据、行级权限过滤后的结果(权限可能变化)

当前实现中,缓存层的具体方案(Caffeine 本地缓存 vs Redis 分布式缓存)取决于部署形态。单机部署用 Caffeine 足够,集群部署需要引入 Redis。

5.3 数据集发布流程

数据集有 publishStatus 字段(DRAFT / PUBLISHED / OFFLINE),这个设计参考了 BI 工具的"发布"概念:

  • DRAFT:编辑中,大屏无法引用
  • PUBLISHED:已发布,大屏可以绑定,但仍可编辑(编辑后自动变回 DRAFT)
  • OFFLINE:已下线,大屏引用了会报错

这个流程在多人协作时有用——数据集的修改不会影响正在运行的大屏,只有重新发布后新打开的大屏才会用到新版本。


六、二开指南

6.1 如何扩展新的数据源类型

当前支持 JDBC 兼容的数据库。DbTypeEnum 枚举定义了支持的数据库类型,扩展新数据库需要:

  1. DbTypeEnum 中新增枚举值
  2. 确认 JDBC 驱动是否在依赖中(Maven pom.xml
  3. 在管理后台"新增连接"时,driverClassName 填对应数据库的驱动类全限定名

对于非 JDBC 数据源(如 Elasticsearch、InfluxDB),需要:

  1. DataQueryExecutor 中新增执行分支
  2. 实现对应的 DatasetQueryResultVO 构建逻辑
  3. 在前端 datasetAdapter.ts 中确认返回格式兼容

6.2 如何自定义参数绑定逻辑

DatasetParamSchemaParser 解析 paramSchemaJsonSqlParameterBinder 负责绑定。如果需要支持新的运算符(如 INBETWEEN),需要:

  1. DatasetParamSchemaItem.operator 中新增枚举值
  2. SqlParameterBinder 中增加对应的 SQL 片段生成逻辑
  3. 前端 requestDynamicParams.ts 中增加对应的参数构造逻辑

6.3 如何优化查询性能

几个实用建议:

  • 加索引:确保 paramSchemaJson 中用作过滤条件的字段有索引
  • 限制返回行数maxRows 默认 1000,大屏不需要全量数据
  • 使用缓存:对变化不频繁的数据集启用 cacheEnabled
  • 避免 SELECT ***** :用 fields 参数指定需要的字段,减少数据传输
  • 分页查询:大屏表格组件使用 pageNum + pageSize 分页,不要一次拉全量

七、体验预告:C05 → C06 → C07 完整链路

这三篇博客覆盖了一条完整的产品链路:

C05AI 供应商接入
  ↓ 配置好 AI 供应商和模型
C06 AI 生成数据大屏
  ↓ AI 根据自然语言描述生成大屏 JSON(含数据集绑定配置)
C07 大屏动态数据接入(本文)
  ↓ 数据集绑定真实数据库,大屏显示实时业务数据

从"能对话的 AI"到"能生成大屏的 AI"再到"大屏能显示真实数据",这条链路已经可以跑通。下一步的方向是:

  • 大屏模板市场:预设行业模板,用户选模板 → AI 微调 → 绑定自己的数据集
  • 数据源插件生态:让社区可以贡献新的数据源类型(Elasticsearch、Prometheus...)
  • AI 辅助数据集配置:根据数据库表结构,AI 自动生成数据集的字段定义和参数配置

项目入口


本文基于 forge-plugin-data 模块源码和 datasource-analysis.md 分析报告撰写,代码细节以实际仓库为准。