# BI / KPI / 图表引擎

xly **没有**内置通用 OLAP / cube 引擎（没有 Mondrian、Saiku、MDX；随包里的 `olap4j-1.2.0.jar` 没有任何 Java import，属于 classpath 上的历史包袱，见[技术栈](tech-stack.md)里的 OLAP4J 说明）。

xly 真正提供的是一套**自研的元数据驱动看板 + KPI 层**。它和框架其他部分使用同一组原语：`gds*` 元数据行指向 `Sp_*` 存储过程，再由通用 Java service 渲染。本页按端到端路径说明它。

## 三个部分

| 部分 | 入口 | 支撑表 | Service |
|---|---|---|---|
| **图表**（卡片、柱线饼仪表盘组件、看板） | FROUNT 的 `/indexPage/commonChar` 模块；BACK 的图表配置管理页 | `gdsconfigcharmaster`（3,006 行）、`gdsconfigcharslave`（1,951 行） | `CharServiceImpl`（2,219 行，是 xlyBusinessService 里最重的类之一） |
| **KPI**（按员工 / 模块做绩效评分） | BACK / FROUNT 的 KPI 页面，例如 `行为KPI项目设置`、`5.经营KPI分析`、`异常清除KPI任务表` | `kpimaster`（124,524 行）、`kpidetail`（1,308）、`kpimodule`（44）、`kpimoduleuser`、`kpimoduleuserday`、`kpislavel` | `KpiServiceImpl`（833 行，位于 `xlyBusinessService/.../KPIService/`）+ `BusinessModelKpiServiceImpl`（901 行）+ `FlushModleKpiThread`（后台刷新） |
| **预置聚合过程** | 由图表元数据行调用 | n/a | 20 个 `Sp_chart_*` 过程 + 2 个 `Sp_KPI_*` 过程 + `spKPImodule` |

## 图表：看板如何渲染

xly 中的一个图表就是 `gdsconfigcharmaster` 中的一行，关键列如下：

| 列 | 作用 |
|---|---|
| `sId` | 图表 ID |
| `sParentId` | 所属模块（`gdsmodule.sId`） |
| `sChinese` / `sEnglish` / `sBig5` | 图表标题 |
| `sCharType` | 组件类型，见下方分布 |
| `sProcedureName` | 产出图表数据的存储过程 |
| `sProcedureParam` | 存储过程参数 JSON 规格 |
| `iWidth` | 布局跨度（24 列栅格） |

实时 dev DB 中的 `sCharType` 分布：

| `sCharType` | 数量 | 渲染内容 |
|---|---:|---|
| `Div` | 1558 | 容器 / 纯布局块 |
| `sLabel` | 1143 | 单值文本卡片，例如“今日销售额：¥X” |
| `Progress` | 137 | 进度条 |
| `sPie` | 52 | 饼图 |
| `commonList` | 45 | 内嵌数据表格（复用通用 grid） |
| `sColumnarGroup` | 30 | 分组柱状图 |
| `sColumnar` | 28 | 单系列柱状图 |
| `sBrokenLine` | 5 | 折线图 |
| `sBar` | 3 | 横向条形图 |
| `ColorBlock` | 3 | 类热力块的彩色块 |
| `sGauge` | 2 | 仪表盘组件 |

从表行（`gdsconfigcharslave`）保存多系列 / 多列图表所需的系列或列拆分。

运行时路径：

```text
SPA 打开 /indexPage/commonChar?sModelsId=<看板模块 id>
    │
    ▼
GET /xlyEntry/business/getModelBysId/<id>
    → 返回看板的元数据复合结果
      （formData 中包含来自 gdsconfigcharmaster + slave 行的图表布局）
    │
    ▼
对每个图表：
  POST /xlyEntry/business/getXxx（CharServiceImpl 方法）
       携带图表的 sProcedureName + sProcedureParam
    → CharServiceImpl 通过通用存储过程分发调用对应的 Sp_chart_* 过程
    │
    ▼
SPA 用前端 ECharts 渲染每张卡片；一行图表元数据对应一张卡片
```

## 20 个 `Sp_chart_*` 过程

| 过程 | 计算内容 |
|---|---|
| `Sp_chart_home_11`、`Sp_chart_home_13` | 首页看板卡片 |
| `Sp_chart_TodayOrder`、`Sp_chart_TodayOrder_hm`、`Sp_chart_ThisMonthQty`、`Sp_chart_MonthOrder`、`Sp_chart_MonthTeamQty` | 当天 / 当月订单数、班组产量 |
| `Sp_chart_TodayProfit`、`Sp_chart_MonthProfit`、`Sp_chart_TodayReceivables`、`Sp_chart_TodayReceive`、`Sp_chart_expenses` | 财务：利润、应收、收款、费用汇总 |
| `Sp_chart_EquipmentLoad`、`Sp_chart_EquipmentLoad1`、`Sp_chart_EquipmentLod1`、`Sp_chart_EquipmentLast`、`Sp_chart_sMachine_speed`、`Sp_chart_Bottleneck` | 车间：设备负载、最后运行状态、当前瓶颈 |
| `Sp_chart_OrderProcess`、`Sp_chart_WorkOrderProcess` | 订单 / 工单进度时间线 |

每个过程都遵循标准的 `(IN sLoginId, IN sBrId, IN sSuId, ...) → result-set` 形状，因此通用分发器可以直接调用。[多租户作用域](../../concepts/multi-tenancy.md)也会自然传入：每张图表都会自动按租户过滤。

## dev DB 中的预置看板模块

`/indexPage/commonChar` 是共享路由。dev DB 中有 6 个模块映射到它：

| Module sId | 中文名称 |
|---|---|
| `19211681019715464089035510` | 销售图表分析 |
| `19211681019715481435115760` | 财务图表分析 |
| `19211681019715481435298200` | 生产图表分析 |
| `19211681019715708435449190` | 销售大数据分析 |
| `19211681019715708471874620` | 采购大数据分析 |
| `101251240115015889205266000` | 采购价格分析查询 |

六个模块全部由 `gdsconfigcharmaster` 行驱动；新增或修改它们不需要改 Java 代码。

## KPI 子系统

> **先消歧。** FROUNT 首页也有一个标题为“**KPI监控**”的卡片，但它**不是**本页记录的 KPI。首页卡片是 `BusinessModelCenterController.getModelCenter` 提供的未清任务计数器，读取 `gdsmodule.bUnTask` / `sUnType`，没有目标值、评分和图表，只是名字容易误导。见运行时页的 [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard)。下面的 `kpi*` 表族才是真正的员工绩效评分层。

`kpi*` 是独立于图表渲染的**按员工绩效评分**层。形状如下：

| 表 | 作用 | 实时行数 |
|---|---|---:|
| `kpimaster` | 按员工、按期间的 KPI 汇总。它是容量最大的表：每个员工的每次评分事件一行。 | 124,524 |
| `kpidetail` | 支撑每条 `kpimaster` 汇总的明细行。 | 1,308 |
| `kpimodule` | KPI 定义：哪些模块 / 指标参与评分。 | 44 |
| `kpimoduleuser` | 每用户 KPI 分配。 | 0（dev DB 未分配） |
| `kpimoduleuserday` | 每用户每日 KPI 桶。 | 1 |
| `kpislavel` | KPI 等级 / 档位定义。 | 0 |

Java 侧：

- `KpiServiceImpl.java`（833 行，位于 `xlyBusinessService/.../KPIService/`）：KPI 事件的读写 API。
- `BusinessModelKpiServiceImpl.java`（901 行）：把业务事件数据计算成 KPI 行的计算层。
- `FlushModleKpiThread.java`：后台重算线程。
- `KpimasterCloum.java` enum（xlyPersist）：列名常量。

存储过程：

- `Sp_KPI_DetailByEmployee`：按员工的明细报表。
- `Sp_KPI_SumByEmployee`：按员工的汇总报表。
- `spKPImodule`：按模块重算 KPI。

`script/客户/` 下也有较大的客户覆盖，例如 `script/标版/30100101/spKPImodule.sql` 以及多个 `Sp_SalesOrder_Kpi*` 过程。这与[每客户 SQL 覆盖通道](../../slices/05-customer-sql-override.md)一致：需要不同 KPI 规则的客户会交付自己的过程。

## 自研方案的代价

元数据 + 每图表一个过程的设计与 xly 的数据驱动论点一致，也避免了携带重型 OLAP 引擎。但代价很明确：

1. **每张新图表都需要 SQL 作者。** “PM 添加一行元数据”只在工程师已经写好配套 `Sp_chart_*` 过程之后才成立。这里没有聚合构建器、字段选择器或自动生成查询；每个指标都是工程团队手写、评审和维护的存储过程。20 个过程和 11 种图表类型就是当前系统能渲染的全部形状。
2. **图表在 OLTP DB 上跑重 SQL。** 没有数仓、没有预聚合、没有增量汇总。“今日利润”图表就是在实时交易 schema 上做 SELECT。大客户加载图表时会和录单负载竞争同一个 MySQL 实例。缓存有帮助，但只对命中有效；元数据变更后的第一次加载仍要付完整成本。
3. **图表之间没有语义一致性保证。** 每个 `Sp_chart_*` 过程自行决定如何计算“月利润”“今日销售额”等指标。两个看似展示同一指标的图表可能因为过程体不同而悄悄不一致。真正的语义层可以避免这个问题，自研模型不能。
4. **不能钻取，也不能自由切片分析。** 每张图表都是固定查询形状。用户不能自由切换维度，也不能从汇总卡片钻取到底层交易，除非工程师为每条路径再写一个过程。
5. **KPI 逻辑会按客户分歧。** `script/客户/` 下的客户会交付自己的 `spKPImodule` 和 `Sp_SalesOrder_Kpi*` 覆盖；不同客户的 KPI 算法不同，而且代码只存在于该客户 DB 中。这会让“这个 KPI 到底是什么意思”取决于当前连接的是哪个 schema。

这种简单设计足够支撑“展示 xly 一直展示的那 20 张卡片”。如果目标是即席分析或自助报表，它就不够了；那需要一套 xly 当前没有的独立语义层 / 数仓层。

## 它不是什么

- **不是自助 BI 工具。** 客户不能随便指向一张表并拖拽生成图表；新图表需要一个 SQL 存储过程，以及懂得注册元数据行的管理员。
- **不是实时分析基础设施。** 图表在缓存 miss 时会直接在 OLTP MySQL schema 上运行过程。没有独立数仓、没有增量聚合管道、没有流式层。大客户的大图表会在实时 DB 上执行重 SQL。
- **不是列存 / OLAP 引擎支撑的分析。** `xlyPersist/build.gradle` 中的 `olap4j` jar 没有任何 Java import，只是 classpath 上的历史包袱。xly 通过 MyBatis 和通用存储过程分发使用 MySQL 的普通行存。
