任务 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
8.1 KiB
口径登记:全局配置键的「维度字典」(账套维度 / 非账套维度 / 集团共享)
- 立档: 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 档登记后关闭。
理由(三条,均以库内/代码实况为据):
- 值本身是角色维度字典:
{role: [维度规则]}(默认DEFAULT_KPI_VISIBILITY:ceo/it → ["*"]、finance → ["finance_*"]、business → ["customer_*","process_*","learning_*"]), 条目的键是角色,没有任何账套语义。 - 载体天然无账套列:
role_permissions只有id/key/value/updated_at,keyunique ⇒ 表结构上不存在「按账套存一份」的位置(要按账套分必须 DDL,与「零 DDL 优先」相悖)。 - 同表同族一致性:
route_permissions/action_permissions同为角色级权限面,都是全局; 只拆kpi_visibility会造成同表三种维度口径分裂,反而更难维护。
口径:同表同族的键按同一维度处理。本键不消费任何账套状态,也不被账套写侧改写 (现库该键 0 行 ⇒ 走内置默认表,与任何账套无关)。
落地:代码注释 + _load_kpi_visibility docstring 已写明「有意不做账套后缀」并指向本档;
用例锁「结构面(无 entity_id 列)+ 行为面(裸键读值)+ 登记面(口径写在代码里)」。
现网实况(2026-09-23 只读核对):
role_permissions4 行 =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),但不并入本单,理由三条:
- 库内确有旧值,且两个 active 账套都在用预算(
budget_plans逐账套:e1 = 276 行、 e2 = 25 行)⇒ 必须裁定「原值落哪家」,或「是否双落」。若按单账套搬运(只落 e1), e2 会由rolling退化为默认fixed(期间窗口从「当月+12 月」变「全年 1-12 月」)—— 这是跨账套行为变更,须任总拍板,不能由执行方顺手决定。 - 牵动既有用例:
tests/test_budget.py(8 处)、tests/test_cross_entity_isolation_batch2_20260918.py(2 处)、tests/test_budget_tech_improve.py均直接读写该全局键字面 ⇒ 应作为独立单元出 RED→GREEN + 全量回归。 - 又是一次生产写(
system_configs新键)⇒ 按「同一生产写目标只开一张单」的约定不并入本单。
建议排期:P2 单「budget_mode / budget_driver_mode 拆账套(含原值落账裁定)」。
建议口径(供拍板):原集团值落全部 active 账套(e1 + e2)、demo 账套留空走默认 ——
即「原值 = 拆键前的事实生效面」(该键拆键前对两个 active 账套同时生效),
这样拆键零行为变更,只是把「谁改谁生效」的写入面收窄到本账套;
比「只落 e1、e2 退化」更保守,也比「读侧回退旧键」更干净(回退仍是借用通路)。
五、消费者自查法(防下一次漏扫)
- 枚举:
grep -rn 'SystemConfig' backend/app+grep -rhoP 'config_key == "\K[a-zA-Z0-9_.]+' backend/app(role_permissions同法 grepRolePermission); - 分档:对每个键问「这个值描述的是谁的什么」→ A / B / C;
- A 档改完必须四件套:读不回退旧键 + 写侧
entity_id必传(不给默认值,漏传即TypeError)- 本档登记 + 搬运脚本
--check/--apply/--down(库名保护 + 幂等 + 目标账套存在性校验);
- 本档登记 + 搬运脚本
- B/C 档必须留声明(代码注释/docstring 指回本档),否则下一轮审计重复报缺口。