大宗商品销采运储系统・六层架构连载(基础服务篇)
上篇我们完成报表服务整体架构与职责边界定义,知道报表服务核心目标是交易与统计解耦。本篇聚焦内部核心模块落地:报表元数据模型、多数据源查询引擎、缓存策略,以及行级数据权限的工程实现。
先给结论:
一套可落地的报表服务 = 元数据驱动 + 查询引擎 + 多级缓存 + 权限模型 + 治理闭环。
一、报表元数据模型:从"写接口"到"配报表"
报表服务所有报表都采用配置化驱动,尽量减少硬编码。一张报表由以下元数据组成:
-
基础信息:报表编码、报表名称、业务分类(采购/销售/仓储/运输)、报表描述、创建人、状态启用/禁用;
-
参数定义:页面查询参数(时间范围、客户、仓库、物料编码等),参数类型、是否必填、下拉数据源;
-
数据源配置:绑定对应的数据源连接(业务只读库/数仓/数据湖/湖仓一体);
-
查询脚本:聚合 SQL 或者查询脚本,支持参数变量替换;
-
指标与维度定义:维度(日期、物料、客户),指标(数量、金额、重量);
-
输出配置:页面列定义、列名称、列类型、是否支持导出、列权限;
-
任务配置:是否开启定时预计算、预计算执行周期、过期时间。
核心表设计建议
| 表 | 作用 | 核心字段 |
|---|---|---|
| report_definition | 报表主定义 | id, code, name, category, datasource_id, query_sql, status, version |
| report_param | 参数定义 | report_id, param_name, param_type, required, default_value, option_source |
| report_column | 列定义 | report_id, column_key, title, data_type, exportable, sensitive_level |
| report_data_scope | 行级权限规则 | report_id, scope_type, scope_field, user_attr |
| report_task | 预计算任务 | report_id, cron, precompute_sql, result_table, status |
| report_query_log | 查询日志 | report_id, user_id, params, cost_ms, rows, datasource, cache_hit, slow_flag |
元数据配置示例
{
"reportCode": "INV_MONTHLY_SUMMARY",
"name": "月度进销存汇总",
"category": "仓储",
"datasource": "dw_readonly",
"params": [
{"name": "startDate", "type": "date", "required": true},
{"name": "warehouseId", "type": "select", "source": "warehouse_api", "required": false}
],
"query": "select warehouse_id, material_id, sum(qty) qty, sum(amount) amount from dw_inv_monthly where stat_date >= :startDate and (:warehouseId is null or warehouse_id = :warehouseId) group by warehouse_id, material_id",
"columns": [
{"key": "warehouse_id", "title": "仓库", "type": "string"},
{"key": "material_id", "title": "物料", "type": "string"},
{"key": "qty", "title": "数量", "type": "decimal"},
{"key": "amount", "title": "金额", "type": "decimal", "sensitive": true}
],
"cache": {"level": "redis", "ttl": 300},
"task": {"enabled": true, "cron": "0 10 2 * * ?", "expireDays": 90}
}
通过这套元数据,新增报表可以做到大部分场景不用开发新接口,后台配置即可上线报表,大幅降低报表迭代成本。
但要注意:配置化不等于随便配。 元数据必须有版本、有状态、有发布流程:草稿 → 测试 → 发布 → 下线。否则报表会变成新的"配置垃圾场"。
二、多数据源查询引擎设计
大宗商品销采运储业务数据分散在采购库、销售库、仓储库、运输库,还有数仓汇总数据。查询引擎核心能力:
-
数据源管理:统一管理多数据库连接池,支持达梦、MySQL 等,隔离不同数据源连接;
-
参数解析与安全校验:对传入参数做 SQL 注入防护,参数预编译,禁止直接字符串拼接 SQL;
-
跨数据源查询:轻量场景在报表服务内存做结果合并;复杂跨库统计,推荐下沉到数仓预聚合;
-
查询熔断与超时控制:每个报表查询设置最大超时时间,超过时间自动终止,防止慢 SQL 拖垮数据源。
查询引擎执行流程
用户请求 → 网关鉴权/限流 → 报表元数据加载 → 报表访问权限校验 → 参数解析与校验 → 行级数据权限注入 → 缓存 key 生成与查询 → 数据源路由 → SQL 预编译与绑定 → 超时/最大行数/熔断控制 → 执行查询 → 结果缓存 → 列级权限过滤/脱敏 → 返回或导出
关键工程规范
禁止报表引擎直接对业务交易大表做无索引全表聚合,大统计必须走预计算。
具体落地要加几道硬约束:
-
只读账号:报表服务连接业务库必须使用只读账号;
-
最大行数:同步查询默认最多返回 1 万行,超过强制分页或转异步导出;
-
超时时间:单报表查询默认 10 秒,超过自动终止;
-
SQL 白名单:禁止
delete、update、drop、多语句、注释注入; -
强制参数化:所有用户输入走
PreparedStatement,禁止${}拼接; -
慢 SQL 熔断:同一数据源慢查询比例过高,自动熔断一段时间。
查询引擎伪代码
ReportResult query(ReportRequest req) {
ReportMeta meta = metaService.get(req.reportCode);
authService.checkAccess(currentUser, meta);
Map<String, Object> params = paramParser.parse(req, meta);
DataScope scope = userService.getDataScope(currentUser, meta);
String cacheKey = cacheKeyBuilder.build(meta, params, scope, tenantId);
ReportResult cached = cache.get(cacheKey);
if (cached != null) return cached;
String sql = queryBuilder.build(meta, params, scope);
sqlValidator.validate(sql, meta);
try (Connection conn = datasourceRouter.route(meta.datasource)) {
PreparedStatement ps = conn.prepareStatement(sql);
bind(ps, params, scope);
ps.setQueryTimeout(meta.timeoutSeconds);
ps.setMaxRows(meta.maxRows);
ReportResult result = execute(ps);
cache.put(cacheKey, result, meta.cacheTtl);
return result;
} catch (TimeoutException e) {
logService.slowQuery(meta, params, e);
throw new ReportTimeoutException("报表查询超时,请缩小范围或使用离线导出");
}
}
跨数据源查询要克制:
-
小结果集:内存合并,但必须限制两边返回行数;
-
大结果集:不要跨源 join,直接下沉到数仓/数据湖预聚合;
-
跨源统计:通过调度服务预计算,结果写入报表结果表。
三、多级缓存策略设计
报表数据访问有明显热点:高频查看的月度、日汇总报表,不需要每次都查询数据库。设计三级缓存:
-
一级缓存:本地内存缓存,短时效,热点报表快速返回;
-
二级缓存:Redis 分布式缓存,存放预计算报表结果,设置过期时间,集群共享;
-
三级缓存:预计算结果表,定时任务生成的报表结果持久化存储。
缓存 key 必须带权限维度
这是最容易踩坑的地方。缓存 key 不能只包含报表编码和参数,必须包含:
report:{reportCode}:tenant:{tenantId}:params:{md5(params)}:scope:{md5(dataScope)}:version:{metaVersion}
否则同一张报表,A 用户查完写入缓存,B 用户可能直接读到 A 的数据,造成越权。
缓存失效策略
-
定时预计算任务执行完成,自动刷新缓存;
-
支持手动清除单张报表缓存;
-
区分公共报表缓存、用户私有报表缓存;
-
数据源版本号变化时,批量失效相关缓存。
防穿透、防击穿、防雪崩
-
空结果短 TTL 缓存,防止穿透;
-
热点 key 加互斥锁,防止击穿;
-
TTL 加随机值,防止雪崩;
-
大报表结果放 Redis 或对象存储,不塞本地内存。
四、报表权限模型
权限分为两层:报表访问权限 + 行级数据权限,再往下还有列级权限和导出权限。
-
报表访问权限:角色是否可以打开这张报表;
-
行级数据权限:同一个报表,不同用户只能看到有权限的数据。例如:仓储管理员只能看自己负责仓库的数据,销售只能看自己客户的单据;
-
列级权限:成本价、毛利、供应商等敏感列,无权限不返回或脱敏;
-
导出权限:是否允许导出、导出行数上限、是否加水印、是否走审批。
行级权限落地思路
报表执行查询时,报表服务调用用户服务获取当前用户的数据权限范围,自动拼接数据过滤条件。
{
"userId": "10086",
"warehouseIds": ["WH001", "WH002"],
"customerIds": ["C001", "C002"],
"orgIds": ["ORG01"]
}
报表元数据中定义:
{
"dataScope": [
{"scopeType": "warehouse", "scopeField": "warehouse_id", "userAttr": "warehouseIds"},
{"scopeType": "customer", "scopeField": "customer_id", "userAttr": "customerIds"}
]
}
查询引擎自动追加:
AND warehouse_id IN (?, ?)
AND customer_id IN (?, ?)
注意:IN 列表不能直接拼字符串,必须用 MyBatis foreach 或动态占位符参数化绑定。权限范围来自用户服务,也不能当成"可信 SQL"直接拼接。
权限缓存
用户权限变更后,相关报表缓存必须失效。否则会出现"权限已收回,缓存还能看"的严重问题。建议:
-
用户服务发权限变更事件;
-
报表服务监听事件,清除该用户相关缓存;
-
缓存 key 中的
scopehash 变化,自然命中新缓存。
五、慢查询治理与监控
报表是慢查询高发场景,需要内置监控:
-
记录每次报表查询耗时、返回行数、数据源;
-
超过阈值标记为慢查询,写入日志服务告警;
-
对单用户、单报表接口限流,防止批量导出并发冲击。
监控指标
| 指标 | 说明 |
|---|---|
| report_query_cost_ms | 查询耗时 |
| report_query_rows | 返回行数 |
| report_cache_hit | 缓存命中率 |
| report_slow_count | 慢查询次数 |
| report_export_count | 导出次数 |
| datasource_error_rate | 数据源错误率 |
| report_concurrent | 报表并发数 |
限流与熔断
-
单用户:每分钟最多 30 次报表查询;
-
单报表:并发查询最多 5 个;
-
导出任务:并发最多 2 个,大导出走调度服务;
-
数据源:错误率超过 50% 或平均耗时超过 5 秒,熔断 30 秒。
大查询治理
-
同步导出默认最多 5万行;
-
超过 5 万行,强制转异步导出;
-
百万级导出生成 CSV/Excel 到对象存储,用户下载;
-
禁止在业务高峰期跑全量预计算。
六、和调度服务联动:预计算落地
离线预计算报表通过调度服务触发定时任务,提前跑统计逻辑,计算结果存入缓存/数据库,用户查看直接读取已经生成好的结果。
任务配置建议:
{
"reportCode": "INV_MONTHLY_SUMMARY",
"cron": "0 10 2 * * ?",
"precomputeSql": "insert into rpt_inv_monthly_summary select ... from dw_inv_monthly where stat_date >= :startDate",
"resultTable": "rpt_inv_monthly_summary",
"timeoutSeconds": 1800,
"retry": 2,
"expireDays": 90
}
执行完成后:
-
写入结果表;
-
刷新 Redis 缓存;
-
记录任务日志;
-
失败告警到日志服务;
-
支持手动重跑。
七、落地优先级与避坑清单
如果你们正在建报表服务,建议按这个优先级落地:
P0:必须做
-
业务库只读账号;
-
元数据表 + 配置化查询;
-
SQL 预编译,禁止拼接;
-
超时、最大行数、限流;
-
行级权限过滤;
-
查询日志。
P1:尽快做
-
三级缓存;
-
慢查询告警;
-
导出异步化;
-
列级权限与脱敏;
-
预计算任务。
P2:进阶做
-
跨源查询优化;
-
缓存命中率运营;
-
SQL 指纹分析;
-
AI 辅助报表生成。
避坑清单
-
不要让报表服务直连业务主库;
-
不要用字符串拼接 SQL;
-
不要忽略权限缓存 key;
-
不要同步导出百万行;
-
不要所有报表都实时查;
-
不要没有元数据版本管理;
-
不要没有最大行数限制;
-
不要跨源 join 大表;
-
不要权限变更后不失效缓存。
八、中篇小结
元数据驱动是报表服务配置化的核心,查询引擎、多级缓存、行级权限共同支撑报表稳定运行。
配置化可以极大降低报表开发成本,同时通过超时、熔断、限流机制保护底层数据源。
一句话:
报表服务不是查得快,而是查得稳、控得住、可追溯。
下篇预告:报表服务 AI 集成篇,介绍 AI 在报表场景落地方式、适用场景、模型选型、代码集成、效果验证。AI 仅作为增强能力,不替换原有报表核心能力