Files
cma-management/docs/global-config-keys-caliber-20260923.md
T
wecom-fullstack 477515c453
ci/woodpecker/push/main Pipeline was canceled
fix(cma): 隔离扫尾 — kpi_alert_higher_better 拆账套 + tenant/switch 审计收口
任务 dama-entity-isolation-sweep-20260923(DAMA 独立回查漏项 / 任总 2026-09-23「按照建议执行」)

① 补漏同源全局键 kpi_alert_higher_better(system_configs,零 DDL)
   - deviation_engine: 新增 higher_better_key(entity_id);get_higher_better_codes(db, entity_id)
     改为只读本账套键、不回退旧全局键;未配置 → DEFAULT_HIGHER_BETTER(兜底,与原行为逐字一致)
   - budget.py: GET/PUT /alert-direction 挂 get_entity_id,只读写会话账套键;body entity_id 不信
   - scripts/migrate_higher_better_key_entity_20260923.py: --check/--apply/--down + 库名保护 + 幂等
   - 生产实况:库内该键 0 行 ⇒ 无原值可搬运(--apply 走 skip 零写入),e1 行为不变

② kpi_visibility 口径裁定:角色级天然全局 = 非账套维度,不拆(关闭本项)
   - auth_middleware 注释 + docstring 登记「有意不做账套后缀」;口径字典
     docs/global-config-keys-caliber-20260923.md(含 budget_mode/budget_driver_mode 评估结论)

③ tenant/switch 审计收口
   - detail 显式 from_entity_id → to_entity_id + source + operator{id,username,name,role}
   - 审计失败不静默:同事务回滚 + AUDIT_WRITE_FAILED ERROR + HTTP 500(不留「改了但没审计」)
   - GET /current 无写路径不落审计;历史缺口不补写

证据:新增/改写用例 17 条;全量 1397 passed(去新增文件 1380 + 17);RED 态 8 failed
2026-09-23 18:29:52 +08:00

107 lines
8.1 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.
# 口径登记:全局配置键的「维度字典」(账套维度 / 非账套维度 / 集团共享)
- **立档**: wecom-fullstack,2026-09-23
- **来源**: `dama-entity-isolation-sweep-20260923` §一(DAMA 独立回查新增漏项,任总 2026-09-23
「按照建议执行」);父单 `dama-workbench-entity-isolation-fullstack-20260922`
- **性质**: 口径登记 + 代码收口说明。**不含**任何数据订正(数据订正另立单 + 审批)
- **配套**: 审计字面/通知日志口径见 `audit-and-notification-caliber-20260922.md`
---
## 一、判据(把「全局唯一键」分三档,别再一刀切)
`system_configs.config_key` 与 `role_permissions.key` 都是 **unique 单行键** ⇒ 一次写入全平台生效。
并非所有全局键都是缺陷,必须按键的**语义维度**分档:
| 档 | 名称 | 判据(问:这个值描述的是谁的什么?) | 处置 |
|---|---|---|---|
| **A** | 账套维度 | 值描述**某个账套**的状态,或**按账套业务**取值(余额、方向、期间窗口…) | **必须拆**:键名带 `.<entity_id>`(零 DDL);读**不回退**旧全局键;写侧 `entity_id` 必传;配 `--check/--apply/--down` 搬运脚本 |
| **B** | 非账套维度 | 值按**角色**(或其它平台级维度)组织,与账套无关 | **保持全局**,但必须在代码里写明「**有意不做账套后缀,这是集团共享口径,不是漏隔离**」并在本档登记 |
| **C** | 集团共享(配置/模板) | 对齐模式模板、通知渠道、OKR 模板、路由/动作权限 | 保持全局 + docstring 声明口径 |
**不写声明的代价**:下一轮隔离审计会把 B/C 档重复报成缺口,来回返工(本仓已发生两轮)。
---
## 二、逐键裁定表(2026-09-23 现况)
| 键 | 载体 | 消费点 | 档 | 处置 | 状态 |
|---|---|---|---|---|---|
| `kpi_alert_higher_better` | `system_configs` | `utils/deviation_engine.py::get_higher_better_codes`(L35-66)、`build_deviation_alert`(L305);`api/budget.py` GET/PUT `/alert-direction` | **A** | 拆为 `kpi_alert_higher_better.<entity_id>`;本账套未配置 → `DEFAULT_HIGHER_BETTER`(内置兜底) | ✅ 2026-09-23 本单 |
| `cash.current_balance` | `system_configs` | `utils/cash_forecast_engine.py`、`api/cash.py` | A | 同上(B1) | ✅ 2026-09-22 |
| `alignment_config` | `role_permissions` | `api/alignment.py`(GET/POST `/config`、GET `/tree`) | A | 同上(B3a) | ✅ 2026-09-22 |
| `current_tenant` | `system_configs` | `api/tenant.py` | A(特殊) | **语义收敛**:非数据维度,仅项目Bot A2A 状态记录;GET 限 it/ceo;POST 落审计(含 from→to) | ✅ 2026-09-22 / 审计收口 2026-09-23 |
| `kpi_visibility` | `role_permissions` | `auth_middleware.py::_load_kpi_visibility`(L248-286) | **B** | **不拆**;代码注释登记 | ✅ 2026-09-23 关闭 |
| `route_permissions` / `action_permissions` | `role_permissions` | `auth_middleware.py`(路由/动作权限) | C | 集团共享 | ✅ |
| `notification_channels` / `okr_templates` | 各自表 | 通知渠道 / OKR 模板 | C | 集团共享(父单 docstring 已声明) | ✅ |
| `budget_mode` | `system_configs` | `api/budget.py` L509(GET `/config`)、L535(POST `/config`)、L568(`/roll-forward`)、L677(预测期间窗口) | **A(待拆)** | 见 §三 | ⏳ 排期 |
| `budget_driver_mode` | `system_configs` | `api/driver_budget.py` L114(GET `/mode`)、L135(POST `/mode`) | **A(待拆)** | 见 §三 | ⏳ 排期 |
---
## 三、`kpi_visibility` 裁定(本单 §一②,**关闭本项**)
**结论:属「非账套维度」的全局配置,不拆账套;按 B 档登记后关闭。**
理由(三条,均以库内/代码实况为据):
1. **值本身是角色维度字典**:`{role: [维度规则]}`(默认 `DEFAULT_KPI_VISIBILITY`:
`ceo/it → ["*"]`、`finance → ["finance_*"]`、`business → ["customer_*","process_*","learning_*"]`),
条目的键是**角色**,没有任何账套语义。
2. **载体天然无账套列**:`role_permissions` 只有 `id/key/value/updated_at`,`key` unique
⇒ 表结构上不存在「按账套存一份」的位置(要按账套分必须 DDL,与「零 DDL 优先」相悖)。
3. **同表同族一致性**:`route_permissions` / `action_permissions` 同为角色级权限面,都是全局;
只拆 `kpi_visibility` 会造成同表三种维度口径分裂,反而更难维护。
**口径**:同表同族的键按同一维度处理。本键**不消费任何账套状态**,也不被账套写侧改写
(现库该键 0 行 ⇒ 走内置默认表,与任何账套无关)。
**落地**:代码注释 + `_load_kpi_visibility` docstring 已写明「**有意不做账套后缀**」并指向本档;
用例锁「结构面(无 `entity_id` 列)+ 行为面(裸键读值)+ 登记面(口径写在代码里)」。
> 现网实况(2026-09-23 只读核对):`role_permissions` 4 行 = `route_permissions` /
> `action_permissions` / `alignment_config` / `alignment_config.1`;**`kpi_visibility` 无行**。
---
## 四、`budget_mode` / `budget_driver_mode` 评估结论(本单「顺带」项,**不并入本单**)
**现况(只读核对)**:库内各 1 行 ——
`budget_mode = {"mode":"rolling","rolling_months":12}`(2026-09-08 更新)、
`budget_driver_mode = {"mode":"driver","label":"驱动因子模式"}`(2026-08-26 更新)。
**为什么属 A 档(不是集团共享)**:
- `budget_mode` 决定**本账套**预算预测的**期间窗口**(`budget.py:677-702`:`rolling` = 当月起 12 个月、
`fixed` = 当年 1-12 月),而其数据查询本身**按会话 entity 过滤**(同函数 L710-713)
⇒ 全局键在决定「本账套的取数窗口」,属跨账套污染同源。
- `budget_driver_mode` 决定**本账套**预算编制模式(`subject` 科目模式 / `driver` 驱动因子模式)。
- 任一账套调 `POST /budget/config`(或 `POST /driver-budget/mode`)即**改全平台模式**。
**结论:应拆(B1 同款,零 DDL),但不并入本单**,理由三条:
1. **库内确有旧值,且两个 active 账套都在用预算**(`budget_plans` 逐账套:e1 = 276 行、
e2 = 25 行)⇒ 必须裁定「原值落哪家」,或「是否双落」。若按单账套搬运(只落 e1),
e2 会由 `rolling` 退化为默认 `fixed`(期间窗口从「当月+12 月」变「全年 1-12 月」)——
这是**跨账套行为变更**,须任总拍板,不能由执行方顺手决定。
2. **牵动既有用例**:`tests/test_budget.py`(8 处)、`tests/test_cross_entity_isolation_batch2_20260918.py`(2 处)、
`tests/test_budget_tech_improve.py` 均直接读写该全局键字面 ⇒ 应作为独立单元出 RED→GREEN + 全量回归。
3. **又是一次生产写**(`system_configs` 新键)⇒ 按「**同一生产写目标只开一张单**」的约定不并入本单。
**建议排期**:P2 单「`budget_mode` / `budget_driver_mode` 拆账套(含原值落账裁定)」。
**建议口径(供拍板)**:**原集团值落全部 active 账套(e1 + e2)、demo 账套留空走默认** ——
即「原值 = 拆键前的事实生效面」(该键拆键前对两个 active 账套同时生效),
这样拆键**零行为变更**,只是把「谁改谁生效」的写入面收窄到本账套;
比「只落 e1、e2 退化」更保守,也比「读侧回退旧键」更干净(回退仍是借用通路)。
---
## 五、消费者自查法(防下一次漏扫)
1. **枚举**:`grep -rn 'SystemConfig' backend/app`+
`grep -rhoP 'config_key == "\K[a-zA-Z0-9_.]+' backend/app`(`role_permissions` 同法 grep `RolePermission`);
2. **分档**:对每个键问「这个值描述的是**谁的**什么」→ A / B / C;
3. **A 档改完必须四件套**:读**不回退**旧键 + 写侧 `entity_id` 必传(不给默认值,漏传即 `TypeError`)
+ 本档登记 + 搬运脚本 `--check/--apply/--down`(库名保护 + 幂等 + 目标账套存在性校验);
4. **B/C 档必须留声明**(代码注释/docstring 指回本档),否则下一轮审计重复报缺口。