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)
12 KiB
12 KiB
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 字段反查 |
判定三问(新表/新键入库前必答):
- 同一业务对象在两个账套会不会同时存在且值不同?会 → A;不会 → 看第 2 问。
- 它是否天然由角色/平台/行业决定(写一份即全平台通用)?是 → B。
- 它是否总依附于另一个 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 约定(本单 §二之二④ 落地)
- 每条审计尽量带
detail.entity_id—— 语义 = 本动作所属账套: 会话账套(Depends(get_entity_id))/ Bot 入参账套 / 被处理对象自身的账套(优先后者,最准)。 - 确无账套上下文 ⇒ 留空 + 可 grep 哨兵(日志告警,绝不猜账套):
OPLOG_NO_ENTITY:写入口无账套上下文(覆盖缺口,待收口);OPLOG_BATCH_NO_ENTITY:一次动作跨多账套批量(如scripts/erp_sync.py全库遍历)。
- 显式非账套维度 ⇒
detail.entity_scope = "global"(不报警;如平台字典表zxt_account_dict、模板库写入)。 跨账套批量 ⇒ 同时写entity_scope = "batch"+ 哨兵。 - 集中封装:新代码一律调
app/utils/oplog.py::log_op(),不要再裸写OperationLog(...)(裸构造点已收敛到封装件内 1 处;源级用例TestAuditWriteSideEntityId会拦住第 28 处)。 - 不做:不加列、不改读侧口径、不回填历史(历史行不猜账套)。
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 复算 / 取证方式
# 结构 + 行数 + 键集合 + 审计覆盖(只读,零写)
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'"
读库三禁忌(本档口径):
- 禁把
entity_id IS NULL当「公共/默认账套」直接返回给某个账套的界面; - 禁按非 FK 字段(如
subject_code)反查 C 态表(kpi_subject_map); - 禁把
current_tenant当数据隔离维度(它是状态记录,不是维度)。
变更记录
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-09-23 | v1.0 | 首版:8 表三态登记 + 键级登记(system_configs 5 键 / role_permissions 4 键)+ operation_logs 写侧约定 + 5 项待施工/排期 |