# 指标模型动态配置方案 > 最后更新:2026-04-12 | 状态:Phase 1–4 已完成并部署 --- ## 1. 背景与目标 ### 1.1 原始问题 系统原有两套独立的指标管理体系: | 体系 | 表 | 范围 | 编码格式 | |------|----|------|---------| | MetricCatalog | `ado_smart_ops_metric_catalog` | 仅 S4,12 条 | `S4_CYCLE_L1`、`S4_L2_CYCLE` 等 | | KpiMaster | `ado_smart_ops_kpi_master` | S1~S9,115 条 | `S4_L1_001`、`S4_L2_001` 等 | 两套编码互不关联、名称不同步、字段丰富度不一致,导致"运营指标建模"(S0)和"运营指标主数据"页面各管各的。 ### 1.2 目标架构 **KpiMaster 作为唯一指标主数据源**,废弃 MetricCatalog: ``` ┌──────────────────────────────────────────────────┐ │ KpiMaster(115 条,S1~S9) │ │ 统一编码 S{n}_L{m}_NNN │ 父子关系 │ 公式/规则 │ └──────────┬───────────────────────────┬────────────┘ │ │ ┌─────▼─────┐ ┌───────▼───────┐ │ LayoutItem │ │ kpi_value 表 │ │ 租户级展示 │ │ 日值/目标值 │ │ 配置偏好 │ │ metric_code │ └────────────┘ └───────────────┘ ``` ### 1.3 双向联动 - **KpiMaster 页面 → 建模页面**:建模页面每次打开从 KpiMaster 实时读取,天然同步 - **建模页面 → KpiMaster**:保存布局时,如果 DisplayName 与 MetricName 不同,自动同步回写 KpiMaster --- ## 2. 已完成工作 ### Phase 1:SQL 数据迁移 ✅ 将 LayoutItem、kpi_value 表中的旧 MetricCatalog 编码全部迁移为 KpiMaster 编码。 **编码映射表(旧 → 新):** | 旧编码 | 新编码 | 名称 | 层级变化 | |--------|--------|------|---------| | `S4_CYCLE_L1` | `S4_L1_001` | 物料交货周期 | L1→L1 | | `S4_SAT_L1` | `S4_L1_002` | 物料交货满足率 | L1→L1 | | `S4_EFF_L1` | `S4_L1_003` | 物料采购人效 | L1→L1 | | `S4_TRANSIT_L1` | `S4_L1_004` | 采购在途周转 | L1→L1 | | `S4_L2_CYCLE` | `S4_L2_001` | 品类物料交货周期 | L2→L2 | | `S4_L2_SAT` | `S4_L2_002` | 品类物料交货满足率 | L2→L2 | | `S4_L2_EFF` | `S4_L2_003` | 品类物料采购人效 | L2→L2 | | `S4_L2_TRANSIT` | `S4_L2_004` | 品类在途物料周转 | L2→L2 | | `S4_L2_RCYCLE` | `S4_L3_001` | 供应商物料交货周期 | **L2→L3** | | `S4_L2_RSAT` | `S4_L3_002` | 供应商物料交货满足率 | **L2→L3** | | `S4_L2_REFF` | `S4_L3_003` | 供应商物料采购人效 | **L2→L3** | | `S4_L2_RTRANSIT` | `S4_L3_004` | 供应商在途物料周转 | **L2→L3** | **同步更新的表:** - `ado_smart_ops_layout_item`:MetricCode、RowId、ParentRowId、MetricLevel - `ado_s9_kpi_value_l1_day`:metric_code - `ado_s9_kpi_value_l2_day`:metric_code ### Phase 2:后端重构 ✅ **改动文件:** | 文件 | 变更 | |------|------| | `AidopKanbanController.S4.cs` | 5 个端点全部改为查 `AdoSmartOpsKpiMaster`;`PutS4OperationLayout` 增加双向同步逻辑;API 返回增加 `id` 字段 | | `Startup.cs` | 移除 MetricCatalog 的 `InitTables` 和 Seed 调用 | | `AdoSmartOpsMetricCatalog.cs` | **已删除** | | `AidopS4MetricModelSeed.cs` | **已删除** | **API 端点现状(`/api/AidopKanban/`):** | 端点 | 数据源 | 说明 | |------|--------|------| | `GET s4-metric-catalog` | KpiMaster | 返回 id、metricCode、parentId、formula、calcRule 等富字段 | | `GET s4-operation-layout` | LayoutItem + KpiMaster | 布局行 + KpiMaster 补充默认名/单位/方向 | | `PUT s4-operation-layout` | LayoutItem + KpiMaster | 保存布局 + DisplayName 变更同步回写 KpiMaster | | `GET s4-home-grid` | LayoutItem + KpiMaster + kpi_value_l1_day | 九宫格卡片 | | `GET s4-detail-kpis` | LayoutItem + KpiMaster + kpi_value_l2_day | 详情页 KPI | ### Phase 3:前端适配 ✅ **改动文件:** | 文件 | 变更 | |------|------| | `kanbanData.ts` | `S4CatalogRow` 增加 `id`、`parentId`、`description`、`formula`、`calcRule` 等 7 个 KpiMaster 字段 | | `s0.vue` | ① 一次获取全量 S4 指标按层级分组;② L2/L3 子集过滤改为 KpiMaster `parentId` 关系;③ RowId 按实际 metricLevel 区分前缀;④ L3 父级选择器自动显示 L2 选项 | | `operationModelSchema.ts` | S4 模块从 3 个 L1 扩展为 4 个,名称与 KpiMaster 对齐 | | `s4GraphServerMap.ts` | 映射数组从 3 项扩展为 4 项 | | `kpiMaster.vue` | 左侧树增加 S 模块分组层(S1~S9),虚拟模块节点不可拖拽/删除,样式区分 | ### Phase 4:构建部署 ✅ - Docker 镜像构建已优化(分层缓存),改代码后增量构建 ≤2 分钟 - 九宫格、详情页、建模页面端到端验证通过 --- ## 3. 当前数据架构 ### 3.1 KpiMaster 表结构 表名:`ado_smart_ops_kpi_master` | 字段 | 类型 | 说明 | |------|------|------| | Id | bigint PK | 自增主键 | | MetricCode | varchar(50) UK | 自动生成,格式 `S{n}_L{m}_NNN` | | ModuleCode | varchar(20) | S1~S9 | | MetricLevel | int | 1=L1, 2=L2, 3=L3 | | ParentId | bigint? | 父指标 ID(L1 为 null) | | MetricName | varchar(200) | 指标名称 | | Description | text | KPI 描述 | | Formula | text | 计算公式 | | CalcRule | text | 计算规则(含举例) | | DataSource | varchar(200) | 数据来源模块 | | StatFrequency | varchar(50) | 统计频率 | | Department | varchar(200) | 责任部门 | | DopFields | varchar(500) | DOP 系统字段映射 | | Unit | varchar(50) | 单位 | | Direction | varchar(20) | 优劣方向 | | IsHomePage | bool | 是否可上九宫格首页 | | SortNo | int | 同级排序号 | | IsEnabled | bool | 启用/禁用 | | TenantId | bigint | 租户 ID | **数据统计:** 115 条(S1~S9 全模块),其中 S4 有 L1×4、L2×4、L3×4 = 12 条。 ### 3.2 S4 指标父子关系 ``` S4_L1_001 物料交货周期(天) id=13 └─ S4_L2_001 品类物料交货周期 id=54, parentId=13 └─ S4_L3_001 供应商物料交货周期 id=99, parentId=54 S4_L1_002 物料交货满足率(%) id=14 └─ S4_L2_002 品类物料交货满足率 id=55, parentId=14 └─ S4_L3_002 供应商物料交货满足率 id=100, parentId=55 S4_L1_003 物料采购人效(颗/人) id=15 └─ S4_L2_003 品类物料采购人效 id=56, parentId=15 └─ S4_L3_003 供应商物料采购人效 id=101, parentId=56 S4_L1_004 采购在途周转(天) id=16 └─ S4_L2_004 品类在途物料周转 id=57, parentId=16 └─ S4_L3_004 供应商在途物料周转 id=102, parentId=57 ``` ### 3.3 LayoutItem 与 KpiMaster 的关系 | 概念 | 表 | 职责 | |------|---|------| | 指标定义 | `KpiMaster` | 名称、公式、规则、单位、方向 — **单一事实源** | | 展示配置 | `LayoutItem` | 哪些指标上首页、显示名覆盖、排序、面板分区 — **租户级偏好** | | 运行数据 | `kpi_value_l*_day` | 日值、目标值 — 引用 KpiMaster 编码 | --- ## 4. 功能页面现状 ### 4.1 运营指标主数据(`/aidop/kanban/kpiMaster`) - **左侧树**:三层结构 = S 模块分组层(S1~S9)→ L1 → L2 → L3 - **右侧详情**:编辑 KpiMaster 全部字段(名称、公式、规则、数据来源等) - **操作**:新增/删除/拖拽排序/启用禁用 - **数据**:直连 `AdoSmartOpsKpiMasterController` ### 4.2 运营指标建模(`/aidop/kanban/s0`) - **图谱**:ECharts 力导向图,显示 S1~S7 全模块 L1/L2 关系 - **S4 面板**: - 点击 S4 模块 → 右侧 L1 勾选表(最多 8 个上首页) - 点击 S4 L1 节点 → 右侧显示该 L1 的 L2/L3 子指标(按 KpiMaster parentId 过滤) - **双向同步**:保存布局时 DisplayName 变更自动回写 KpiMaster - **数据**:S4 部分读 `AidopKanbanController` S4 端点(底层查 KpiMaster);其他模块为本地图谱草稿 ### 4.3 S4 采购看板(九宫格 + 详情) - **首页九宫格**:`GET s4-home-grid`,展示 L1 布局 × kpi_value_l1_day - **详情页**:`GET s4-detail-kpis`,展示 L2/L3 布局 × kpi_value_l2_day - **指标元信息**(名称、单位、方向)全部从 KpiMaster 实时读取 --- ## 5. Docker 构建优化 Dockerfile 已优化为分层缓存策略: | Dockerfile | 优化方式 | 改代码后耗时 | |-----------|---------|------------| | `Dockerfile.api` | 先拷 `*.csproj` + `sln` → `dotnet restore` → 再拷源码 → `dotnet publish` | ~38s | | `Dockerfile.web` | 先拷 `package*.json` → `npm install` → 再拷源码 → `npm run build` | ~1m46s | 只有修改 `package.json`(加依赖)或 `.csproj`(加 NuGet 包)时才会触发 install/restore 重跑。 --- ## 6. Demo 数据租户隔离方案 > 状态:待执行 ### 6.1 背景与需求 需要方便快速地导入用于演示的数据来展示系统功能,但不影响实际业务数据。要求: - 正式账号看正式数据,Demo 账号看 Demo 数据,可同时在线 - 前端完全无感知,无需切换模式 - Demo 数据可一键刷新 ### 6.2 当前问题:AiDOP 未接入 Admin.NET 多租户体系 Admin.NET 框架已有完整的行级多租户隔离(`ITenantIdFilter` + SqlSugar 全局过滤),但 AiDOP 插件完全绕开了这套体系: | 问题 | 现状 | |------|------| | 实体未实现 `ITenantIdFilter` | KpiMaster/LayoutItem/HomeModule 有 TenantId 字段但无自动过滤 | | Controller 硬编码 tenantId | 所有 API 用 `[FromQuery] long tenantId = 1` 默认参数 | | TenantId 值不一致 | AiDOP 用 `1`,Admin.NET 默认租户是 `1300000000001` | | 前端硬传 tenantId | `kanbanData.ts` 所有请求带 `tenantId=1` | ### 6.3 架构方案 ``` 登录入口 ├─ 正式账号 → Token.TenantId = 1300000000001 │ └─ 后端自动识别 → WHERE TenantId=1300000000001 → 正式数据 └─ Demo 账号 → Token.TenantId = 1300000000888 └─ 后端自动识别 → WHERE TenantId=1300000000888 → Demo 数据 同一数据库,按 TenantId 行级隔离 系统管理(菜单/角色/权限)共享,业务数据隔离 ``` ### 6.4 执行步骤 **Phase 1:后端 — AiDOP 接入租户体系** - **1a** 新建 `AidopTenantHelper.cs`:从 `HttpContext.User.Claims` 取 TenantId,未登录 fallback `1300000000001` - **1b** 三个实体加 `ITenantIdFilter` 接口:`AdoSmartOpsKpiMaster`、`AdoSmartOpsLayoutItem`、`AdoSmartOpsHomeModule` - **1c** 所有 Controller 移除 `[FromQuery] long tenantId = 1`,改用 Helper 自动获取 - **1d** `AidopKpiMasterSeed.cs` 默认值从 `1` 改为 `SqlSugarConst.DefaultTenantId` **Phase 2:数据迁移 — TenantId 从 1 改为 1300000000001** ```sql UPDATE ado_smart_ops_kpi_master SET TenantId=1300000000001 WHERE TenantId=1; UPDATE ado_smart_ops_layout_item SET TenantId=1300000000001 WHERE TenantId=1; UPDATE ado_smart_ops_home_module SET TenantId=1300000000001 WHERE TenantId=1; UPDATE ado_s9_kpi_value_l1_day SET tenant_id=1300000000001 WHERE tenant_id=1; UPDATE ado_s9_kpi_value_l2_day SET tenant_id=1300000000001 WHERE tenant_id=1; ``` **Phase 3:前端 — 去掉硬编码 tenantId** - `kanbanData.ts`:所有 API 函数移除 `tenantId` 参数 - `s0.vue`:`loadS4Panel`/`saveS4LayoutClick` 去掉 tenantId 实参 - `kpiMasterApi.ts`:同上 **Phase 4:创建 Demo 租户 + 账号** SQL 脚本 `scripts/setup-demo-tenant.sql`: 1. `SysTenant` 插入 Demo 租户(Id=`1300000000888`,TenantType=Id) 2. `SysTenantMenu` 授权全部菜单 3. `SysUser` 创建 DemoAdmin 账号 4. `SysRole` + `SysUserRole` + `SysRoleMenu` 配权限 **Phase 5:Demo 数据智能生成** Python 脚本 `scripts/gen_demo_data.py`,读取 KpiMaster 定义自动生成全套自洽数据: 输入: - 连接数据库读取 115 条 KpiMaster 定义(层级、父子关系、direction、unit) - 可选:CSV 覆盖部分 L1 的 target/actual 生成逻辑: 1. 根据 unit/direction 确定合理的 target 范围(%类 90~99.5、天类 2~10、人效类 500~1000) 2. 生成 30 天日值(带趋势 + 随机波动) 3. L1 → L2 → L3 自动拆分,加权平均自洽(误差 <1%) 4. 制造红/黄/绿分布(约 60%/25%/15%) 自洽规则: - `higher_is_better`:绿 actual >= target;黄 actual >= target×0.95;红 < target×0.95 - `lower_is_better`:绿 actual <= target;黄 actual <= target×1.1;红 > target×1.1 - 末位 L2 做差值补偿确保加权平均 ≈ L1 输出数据量: | 表 | 行数 | 说明 | |----|------|------| | `kpi_master` | 115 | 复制全套指标定义 | | `layout_item` | ~12 | S4 看板布局 | | `home_module` | 1 | S4 模块配置 | | `kpi_value_l1_day` | ~960 | 32 L1 × 30天 | | `kpi_value_l2_day` | ~1800 | 60 L2 × 30天 | | `kpi_value_l3_day` | ~690 | 23 L3 × 30天 | 使用方式: ```bash python3 scripts/gen_demo_data.py --db-host=... --demo-tenant-id=1300000000888 mysql -h... -u... -p... aidopdev < scripts/import-demo-data.sql ``` **Phase 6:构建部署 + 验证** - Docker 构建(利用缓存 ≤3 分钟) - 正式账号登录 → 正式数据不受影响 - DemoAdmin 登录 → 完整 Demo 数据 - 两个浏览器同时在线互不干扰 ### 6.5 风险与注意 | 项目 | 说明 | |------|------| | 旧表无 TenantId | `ado_order`/`ado_work_order`/`ado_plan` 无 TenantId 列,本期不改,后续按需加列 | | kpi_value 原生 SQL | S4 控制器中 raw SQL 已有 `tenant_id=@t`,需确认 @t 取自 Helper | | 系统管理不动 | 菜单/用户/角色的租户隔离由 Admin.NET 框架自动处理 | --- ## 7. 遗留与后续 | 项目 | 状态 | 说明 | |------|------|------| | `S4_ONTIME` 编码 | 待确认 | 存在于 `kpi_value_l1_day` 但无对应 KpiMaster 行,可能是历史残留 | | 其他模块(S1~S3, S5~S9)看板 | 未接入 | 目前不走 LayoutItem/KpiMaster 看板流程,仅 S4 完成了全链路 | | MetricCatalog 旧表 | 可清理 | 实体和种子已删除,数据库表 `ado_smart_ops_metric_catalog` 可手动 DROP | | 建模页面非 S4 模块 | 本地草稿 | S1~S3、S5~S7 的图谱编辑保存在浏览器 localStorage,未接入服务端 |