Files
cma-management/docs/global-config-keys-caliber-20260923.md
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

8.1 KiB
Raw Permalink Blame History

口径登记:全局配置键的「维度字典」(账套维度 / 非账套维度 / 集团共享)

  • 立档: 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 指回本档),否则下一轮审计重复报缺口。