Files
cma-management/docs/entity-dimension-data-dictionary.md
T
wecom-fullstack 467472311d feat(cma): audit write-side entity closure + entity-dimension data dictionary
task dama-entity-isolation-sweep-20260923 (sections 2-2 4/5, merged by projectBot 2026-09-23 18:2x)

4) operation_logs write side: every audit write carries the account (no DDL, no column added)

   - new app/utils/oplog.py::log_op() single wrapper: sets detail.entity_id; scope=global for

     by-design non-entity writes (platform dict / role-level); scope=batch + OPLOG_BATCH_NO_ENTITY

     sentinel for cross-account batches; no context -> OPLOG_NO_ENTITY sentinel (never guess)

   - 16 remaining construction points converted (data/alert_rules/bi_reports/kpi_causality/

     bot_bridge x2/budget_generate/driver_budget/kpis _log x3/templates _log x2/erp_sync);

     pre-4 state had 20 sites without entity_id, now 0 outside the wrapper

   - read side untouched (knowledge_service still user_id fallback); history not backfilled

5) docs/entity-dimension-data-dictionary.md: 8-table three-state registry

   (kpi_templates/driver_factor_templates/scenario_suggestions/bot_bridge_config = non-entity;

    kpi_subject_map = inherited via kpi_id; analysis_results/subjects = account dimension,

    legacy rows still NULL -> other task; operation_logs = non-entity column + detail.entity_id)

Evidence: RED 2 failed/7 passed (reverted call sites) -> GREEN full 1428 passed; 9 new tests

including two read-back rows; app md5 restore verified identical; no cross-account writes

LOCAL commit only - push is gated (ahead contains the P1 entity-dim batch commit fb4609b)
2026-09-23 23:08:26 +08:00

163 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CMA 账套维度数据字典(entity-dimension data dictionary)
- **版本**: v1.0(2026-09-23)
- **来源任务**: `dama-entity-isolation-sweep-20260923` §二之二⑤(项目Bot 2026-09-23 18:2x 并入)
- **裁定依据**: `dama-entity-dimension-ruling-20260923`(项目Bot)+
`0-P1-project-20260923-entity-dimension-convergence-fullstack`(施工面)+
本单 §一/§二(DAMA 独立回查漏项与口径裁定)
- **取证脚本**: `_verification/20260923-entity-isolation-sweep/round2/inspect_entity_dimension_dictionary_20260923.py`
(只读:`information_schema` + `COUNT(*)`;产物 `entity-dimension-schema-20260923.json`)
- **本档性质**: **只落文档、不落代码**。规则口径以本档为准;表结构/行数为 2026-09-23 生产实况快照。
> 一句话口径:**「表里有 `entity_id` 列」≠「这张表是账套维度」**;反之亦然
> ——三态登记的是**归属语义**,不是列面。查库前先查本档。
---
## §0 三态定义与判定法
| 态 | 含义 | 判定法 | 归属写法 |
|---|---|---|---|
| **A|账套维度** | 每一行都属于某一个账套,跨账套读=污染 | 表有 `entity_id`(或等价外键)**且**查询按会话账套过滤 | 必须显式写会话账套,禁 default |
| **B|非账套维度(天然全局)** | 行本身没有账套语义(平台字典 / 角色级配置 / 模板库) | 表无 `entity_id`,且**加了也不会有第二个值**(如角色可见性按角色而不按账套) | 不写账套;审计侧记 `entity_scope=global` |
| **C|经 X 继承** | 表无 `entity_id`,归属由其外键指向的对象决定 | 表无 `entity_id`,但有指向 A 态对象的 FK | **必须经该 FK 查(join)**;禁按非 FK 字段反查 |
**判定三问**(新表/新键入库前必答):
1. 同一业务对象在两个账套会不会**同时存在且值不同**?会 → A;不会 → 看第 2 问。
2. 它是否**天然由角色/平台/行业**决定(写一份即全平台通用)?是 → B。
3. 它是否总**依附于另一个 A 态对象**存在?是 → C(并写明经哪个键继承)。
---
## §1 八张表逐条登记(本轮裁定结论)
| # | 表 | 态 | 行数(09-23) | 关键依据 | 处置 / 施工状态 |
|---|---|---|---|---|---|
| 1 | `kpi_templates` | **B 非账套** | 59 | 无 `entity_id`;系统预置模板库(`is_system`),全体账套共用 | 不拆。写入审计记 `entity_scope=global`(`templates.py::_log(create_template)`) |
| 2 | `driver_factor_templates` | **B 非账套** | **0** | 无 `entity_id`;行业模板库(行业包定义) | 不拆。空表,将来若按客户定制再回本档加「例外行」 |
| 3 | `kpi_subject_map` | **C 经 `kpi_id` 继承** | 89 | 无 `entity_id`;列面仅 `kpi_id/subject_code/calc_type/weight/remark` | **禁按 `subject_code` 反查**(科目码在 `subjects` 里分层,反查会跨账套);一律 `join kpi_definitions ON kpi_id` 后按 `kpi_definitions.entity_id` 过滤 |
| 4 | `analysis_results` | **A 账套维度** | 2 | `entity_id` 列**已落地**(`NULL` 允许,`MUL`) | ⚠️**存量 2 行仍为 `NULL`** ⇒ 归属回填 + 读侧按账套收敛,属**另单施工**(本单不动数据) |
| 5 | `scenario_suggestions` | **B 非账套** | 4 | 无 `entity_id`;按 `alert_type` 的**情景建议模板库** | 不拆(模板与账套无关,实例化时才落账套) |
| 6 | `subjects` | **A + 平台层(分层)** | 207 | `entity_id` 列**已落地**(`NULL` 允许,`MUL`) | ⚠️**存量 207 行全为 `NULL`**。语义 = `NULL` 全局标准准则科目(共享)/非 `NULL` 账套私有科目。回填与「新写入必带账套」属**另单施工**(本单 §三-5 只记「列落地与部署须同窗」) |
| 7 | `operation_logs` | **B(列面非账套)+ 写 `detail.entity_id`** | 338 | 列面仅 `id/user_id/action/target_type/target_id/detail(json)/created_at`,**不加列** | 账套信息写进 `detail.entity_id`;按设计无法归属的写入口显式 `entity_scope`。**读侧本轮不动**(见 §3) |
| 8 | `bot_bridge_config` | **B 非账套** | 1 | 无 `entity_id`;Bot 鉴权注册表(`bot_name` 唯一 + `token`) | 不拆(Bot 身份与账套正交;账套由请求的会话/Bot 白名单决定) |
### 逐条补充
- **1/2/5(模板库三兄弟)**:都不是「某账套的模板」,而是「可被任一账套实例化的模板」。
实例化产物(`kpi_definitions` / `budget_plans` / `action_plans`)才是 A 态 ⇒
**污染点在实例化写入**(本单 §二之二④ 已覆盖这些写入点的审计侧;写入侧的账套强制在各自单内完成)。
- **3(`kpi_subject_map`)**:唯一允许的归属入口是 `kpi_id`。**反查禁令**:
`SELECT * FROM kpi_subject_map WHERE subject_code = ?` 拿到的可能是他账套 KPI 的映射
(科目码在 `subjects` 里是同码多行分层)。
- **4(`analysis_results`)**:列已落地、存量归属未回填 ⇒ 现网读侧**不能**假设「非空 = 已隔离」;
回填前一律按「`entity_id IS NULL` = 未知归属」处理。
- **6(`subjects` 分层)**:`entity_id IS NULL` 的行 = **平台标准准则科目**(只读共享);
账套私有行必须显式带 `entity_id`。**新写入不得依赖模型 `default`**(`api/bot_bridge.py::bot_subjects_upsert` 已显式落本会话账套)。
- **7(`operation_logs`)**:见 §3。
- **8(`bot_bridge_config`)**:1 行 = 当前在册 Bot。**注意**:这是一张**凭据表**,
任何导出/日志都不得落 `token` 明文。
---
## §2 键级登记(`system_configs` / `role_permissions`)
### 2.1 `system_configs`(当前键集合 = 5 个,09-23 实况)
| 键 | 态 | 归属写法 | 说明 |
|---|---|---|---|
| `cash.current_balance.1` | **A 已拆** | 账套后缀 | 治本单 `0c2e57a`(B1 同款)。读不回退旧键 |
| `cash.current_balance` | 旧全局键(保留) | — | **回退杠杆**,不再被读;不删 |
| `kpi_alert_higher_better.<eid>` | **A 已拆**(本单 §一) | 账套后缀 | ⚠️ 生产库该键**当前 0 行**(无值可搬)。未配置 ⇒ 走内置 `DEFAULT_HIGHER_BETTER`,**不回退旧全局键** |
| `current_tenant` | **非数据维度(状态记录)** | — | 全平台「当前租户状态」记录,**不是数据隔离维度**(语义已收敛,禁再当隔离用)。写路径审计见 §3 |
| `budget_mode` | **A 待拆(已排期)** | 账套后缀 | 决定**本账套**预算期间窗口(rolling=当月起 12 月 / fixed=全年)。任一账套 PUT 即改全平台 ⇒ 真缺口。**待拍板**:拆键时旧值落账口径(建议落全部 active 账套 e1+e2,demo 留空) |
| `budget_driver_mode` | **A 待拆(已排期)** | 账套后缀 | 决定**本账套**预算编制模式(subject/driver)。同上;当前审计写入口已带会话账套并标注 `entity_scope=global_key`(`driver_budget.py::set_driver_mode`) |
> 排期单不在本单范围内(本单 §二之三「不强行纳入」;理由与被拒样态见本单回报 §五)。
### 2.2 `role_permissions`(当前键集合 = 4 个)
| 键 | 态 | 裁定与理由 |
|---|---|---|
| `route_permissions` | **B 非账套(角色维度)** | 值 = `{role: [页面]}`,条目的键是**角色**,无账套语义 |
| `action_permissions` | **B 非账套(角色维度)** | 值 = `{role: [动作]}`,同上 |
| `kpi_visibility`(库内**当前无此键**,走代码 `DEFAULT_KPI_VISIBILITY`) | **B 非账套(角色维度)** | 本单 §二裁定**不拆**:①值本身按角色索引(`ceo/it→[*]`、`finance→[finance_*]`…);②载体只有 `key/value` 且 `key` 唯一,结构上无处放「按账套存一份」(要分必须 DDL,与零 DDL 优先相悖);③同表同族另两键同为角色级 —— 只拆本键会让同表三种维度口径分裂。**代码内已登记口径**(`auth_middleware.py` 注释 + docstring 明写「非账套维度…有意不做账套后缀」) |
| `alignment_config` / `alignment_config.1` | **A 已拆** | `alignment_config` = 旧全局键(回退杠杆);`.1` = 账套 1(治本单 `0c2e57a`) |
> **新增键纪律**:往 `system_configs` 加键前必须回答 §0 三问并**在本档补一行**。
> 若判定为 A 态而未带账套后缀 ⇒ 下轮审计会重复报缺口(DAMA 回查机制如此)。
---
## §3 `operation_logs` 写侧约定(零 DDL)
### 3.1 约定(本单 §二之二④ 落地)
1. **每条审计尽量带 `detail.entity_id`** —— 语义 = **本动作所属账套**:
会话账套(`Depends(get_entity_id)`)/ Bot 入参账套 / 被处理对象自身的账套(优先后者,最准)。
2. **确无账套上下文** ⇒ 留空 + 可 grep 哨兵(日志告警,**绝不猜账套**):
- `OPLOG_NO_ENTITY`:写入口无账套上下文(覆盖缺口,待收口);
- `OPLOG_BATCH_NO_ENTITY`:一次动作跨多账套批量(如 `scripts/erp_sync.py` 全库遍历)。
3. **显式非账套维度** ⇒ `detail.entity_scope = "global"`(**不**报警;如平台字典表 `zxt_account_dict`、模板库写入)。
跨账套批量 ⇒ 同时写 `entity_scope = "batch"` + 哨兵。
4. **集中封装**:新代码一律调 `app/utils/oplog.py::log_op()`,**不要**再裸写 `OperationLog(...)`
(裸构造点已收敛到封装件内 1 处;源级用例 `TestAuditWriteSideEntityId` 会拦住第 28 处)。
5. **不做**:不加列、不改读侧口径、不回填历史(历史行不猜账套)。
### 3.2 读侧现状(本轮**有意不动**)
`app/services/knowledge_service.py::extract_events` 仍按 `user_id → user_entities` 兜底过滤 ⇒
`user_id IS NULL` 的 Bot/脚本日志**进不了记忆抽取**(**覆盖缺口,非泄漏**)。
`detail.entity_id` 全量到位后,下一轮才切读侧(届时改按 `detail.entity_id` 归集,覆盖缺口随之闭合)。
### 3.3 生产实况(2026-09-23 快照,可复算)
| 指标 | 值 |
|---|---|
| 总行数 | 338 |
| `user_id IS NULL` | 231(Bot/脚本写入) |
| `detail.entity_id` 非空 | 50(e1 25 / e2 25 —— **先例写法**,本单只是把先例普及到全部写入口) |
| `detail.entity_scope` 非空 | 0(新码未部署,字段由本轮起才写入) |
---
## §4 待施工 / 排期项(本档只登记,不在本单施工)
| # | 项 | 态 | 归属单 | 前置 |
|---|---|---|---|---|
| D1 | `subjects` 存量 207 行 `entity_id` 回填 + 新写入必带账套 | A | 另单(施工面 = `0-P1-project-20260923-entity-dimension-convergence-fullstack`) | 列已落地;部署与新写入同窗 |
| D2 | `analysis_results` 存量 2 行 `entity_id` 回填 + 读侧收敛 | A | 同上 | 同上 |
| D3 | `budget_mode` / `budget_driver_mode` 拆账套 | A | **P2 排期单(待任总拍板旧值落账口径)** | 拍板 → 单开 |
| D4 | `operation_logs` 读侧改按 `detail.entity_id` 归集 | — | 下一轮收口单 | 写侧全量到位(本单)+ 部署 |
| D5 | `bi_reports` 创建端点未绑定会话账套(`entity_id` 走模型 default=1) | A | **非阻塞新发现**,另立单 | 本单只把审计改成「取被处理行自身账套」,未改写入行为 |
---
## §5 复算 / 取证方式
```bash
# 结构 + 行数 + 键集合 + 审计覆盖(只读,零写)
cd /srv/hermes/shared/cma-management/backend
./venv/bin/python3 \
/root/.hermes/profiles/wecom-fullstack/pending-tasks/_verification/20260923-entity-isolation-sweep/round2/inspect_entity_dimension_dictionary_20260923.py
# 单表确认某列是否存在(判定 A 态的前置)
mysql ... -e "SELECT COLUMN_NAME FROM information_schema.COLUMNS \
WHERE TABLE_SCHEMA='cma' AND TABLE_NAME='subjects' AND COLUMN_NAME='entity_id'"
```
**读库三禁忌**(本档口径):
1. 禁把 `entity_id IS NULL` 当「公共/默认账套」直接返回给某个账套的界面;
2. 禁按非 FK 字段(如 `subject_code`)反查 C 态表(`kpi_subject_map`);
3. 禁把 `current_tenant` 当数据隔离维度(它是状态记录,不是维度)。
---
## 变更记录
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-09-23 | v1.0 | 首版:8 表三态登记 + 键级登记(`system_configs` 5 键 / `role_permissions` 4 键)+ `operation_logs` 写侧约定 + 5 项待施工/排期 |