fix(cma): 工作台跨实体治本 · DAMA 回查收口 — 审计可归属 + 通知日志口径
DAMA 独立回查 dama-recheck-workbench-entity-isolation-20260922 的 §4b/§5 两处待办:
1. POST /api/cma/tenant/switch 审计行原来不可归属:
- user_id=None → 改为注入会话用户(user=Depends(require_auth_401))
- target_type/target_id 从 system_config/None 对齐为 entity/<entity_id>,
与 /auth/switch-entity 的 switch_entity 共用目标维度 ⇒ 按账套可过滤,零 DDL
- action 字面 switch_tenant_state 保留不合并(与会话级 switch_entity 语义不同),
口径登记 docs/audit-and-notification-caliber-20260922.md
2. /api/cma/notifications/logs 增加 policy/note 口径字段,前端发送记录表
empty-text 提示同一口径 —— 避免「B2 上线后 e1/e2 均 0 条」被当成页面故障
(现库 8 行历史记录全为 alert_id IS NULL 的 OKR 复盘提醒,无账套归属)
回归:tests/test_workbench_entity_isolation_20260922.py 22/22;全量 1370 passed;前端 vite build 通过
This commit is contained in:
@@ -130,6 +130,13 @@ def list_logs(page: int = 1,
|
||||
return {
|
||||
"total": total,
|
||||
"entity_id": entity_id,
|
||||
# 口径字段(2026-09-22 DAMA 回查 §5 收口):上线后本页在无预警推送的账套会显示 0 条,
|
||||
# 属口径而非故障——现有 8 行历史记录全是 `alert_id IS NULL` 的 OKR 复盘提醒(无账套归属),
|
||||
# 按「宁少勿错」不返回。前端据此文案提示,避免被当成页面坏了。
|
||||
"policy": "alert-linked-only",
|
||||
"note": ("仅显示与预警推送关联的记录(按父键 kpi_alerts 归属会话账套)。"
|
||||
"无父键的历史通知(如 OKR 复盘提醒,写入方 backend/scripts/okr_review_reminder.py)"
|
||||
"不属于任何账套,不在本列表返回。口径登记:docs/audit-and-notification-caliber-20260922.md"),
|
||||
"data": [{
|
||||
"id": l.id,
|
||||
"alert_id": l.alert_id,
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
收紧会切断其原始用途。
|
||||
5. 每次写都落 `operation_logs`(action=`switch_tenant_state`)留痕 —— 修复前本端点
|
||||
无审计(ops 止血单假定「走端点即有审计」不成立,已在本单补齐)。
|
||||
6. **审计行可归属(2026-09-22 DAMA 独立回查 §4b 后补)**:`user_id` = 当前会话用户
|
||||
(原写 None ⇒ 事后无法追操作人);`target_type="entity"` + `target_id=<entity_id>`
|
||||
与 `POST /auth/switch-entity`(action=`switch_entity`,`auth.py`)**共用同一目标维度**
|
||||
⇒「按账套查换账套记录」= `WHERE target_type='entity' AND target_id=?`(`operation_logs`
|
||||
无 `entity_id` 列,无需 DDL)。两字面**刻意不合并**:一个改「用户自己的会话账套」、
|
||||
一个改「平台全局状态记录」,合并后审计上不可分辨。口径登记见
|
||||
`docs/audit-and-notification-caliber-20260922.md`。
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -64,7 +71,8 @@ def get_current_tenant(
|
||||
|
||||
|
||||
@router.post("/switch")
|
||||
def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db)):
|
||||
def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db),
|
||||
user=Depends(require_auth_401)):
|
||||
"""切换当前租户状态记录(项目Bot 分发 A2A 任务用;CMA前端企业切换器已下线)
|
||||
|
||||
2026-09-22:写操作**不动**(保留 Bot 通道可用性),补 `operation_logs` 审计留痕。
|
||||
@@ -101,11 +109,14 @@ def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db)):
|
||||
))
|
||||
|
||||
# 审计留痕(2026-09-22 补齐:本端点原无 operation_logs 记录)
|
||||
# 2026-09-22 DAMA 回查 §4b 后补:user_id 记操作人(原 None ⇒ 无法追人);
|
||||
# target_type/target_id 与 /auth/switch-entity 的 switch_entity 对齐 ⇒ 按账套可过滤
|
||||
# (operation_logs 无 entity_id 列,无须 DDL)。
|
||||
db.add(OperationLog(
|
||||
user_id=None,
|
||||
user_id=user.id,
|
||||
action="switch_tenant_state",
|
||||
target_type="system_config",
|
||||
target_id=None,
|
||||
target_type="entity",
|
||||
target_id=data.entity_id,
|
||||
detail={
|
||||
"entity_id": data.entity_id,
|
||||
"entity_name": ent.name,
|
||||
|
||||
@@ -298,6 +298,14 @@ class TestP0CurrentTenantConvergence:
|
||||
db.expire_all()
|
||||
n = db.query(OperationLog).filter(OperationLog.action == "switch_tenant_state").count()
|
||||
assert n == 1, f"switch 应落 1 条审计,实际 {n}"
|
||||
# 审计行必须可归属(2026-09-22 DAMA 独立回查 §4b):操作人 + 账套目标维度
|
||||
log = (db.query(OperationLog)
|
||||
.filter(OperationLog.action == "switch_tenant_state")
|
||||
.order_by(OperationLog.id.desc()).first())
|
||||
assert log.user_id is not None, "审计行 user_id 不得为 None(事后须能追操作人)"
|
||||
assert log.target_type == "entity" and log.target_id == 2, (
|
||||
f"审计须按账套可过滤(target_type/target_id),实际 {log.target_type}/{log.target_id}")
|
||||
assert (log.detail or {}).get("entity_id") == 2, f"detail 须含 entity_id:{log.detail}"
|
||||
# 全局键虽被改成 e2,e1 会话工作台仍只见 e1 数据
|
||||
codes1 = {k["kpi_code"] for k in
|
||||
client.get(f"{DASH}/my-kpis", headers=auth_header(t1)).json()["data"]}
|
||||
@@ -522,6 +530,10 @@ class TestB2NotificationLogScoping:
|
||||
assert titles2 == {"e2 通知"}, f"e2 会话通知日志混入他账套/无归属行:{titles2}"
|
||||
assert l1["total"] == 1 and l2["total"] == 1
|
||||
assert l1["entity_id"] == 1 and l2["entity_id"] == 2
|
||||
# 口径字段(2026-09-22 DAMA 回查 §5):空页要能解释成口径而非故障
|
||||
for l in (l1, l2):
|
||||
assert l["policy"] == "alert-linked-only", l
|
||||
assert "无父键" in l["note"] and "okr_review_reminder" in l["note"], l
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# 口径登记:审计留痕(`operation_logs`)与通知发送记录(`notification_logs`)
|
||||
|
||||
- **立档**: wecom-fullstack,2026-09-22
|
||||
- **来源**: DAMA 独立回查 `dama-recheck-workbench-entity-isolation-20260922`(2026-09-22 11:07–11:20)
|
||||
§4b(审计字面与库内既有口径不一致 / 无 `entity_id` 列 / `user_id=None`)与
|
||||
§5(B2 上线后 `/notifications/logs` 在 e1/e2 均为 0 条 = 口径副作用)两处待办
|
||||
- **性质**: 口径登记 + 代码收口说明。**不含**任何数据订正(数据订正另立单 + 审批)
|
||||
|
||||
---
|
||||
|
||||
## 一、`operation_logs` 审计字面字典(换账套类)
|
||||
|
||||
| action 字面 | 写入端点 | 语义 | target_type | target_id |
|
||||
|---|---|---|---|---|
|
||||
| `switch_entity` | `post /api/cma/auth/switch-entity`(`app/api/auth.py`) | 改**用户自己的会话账套**(重签 token、旧 token 下线) | `entity` | 目标 `entity_id` |
|
||||
| `switch_tenant_state` | `post /api/cma/tenant/switch`(`app/api/tenant.py`) | 改**平台全局状态记录** `system_configs.current_tenant`(项目Bot 分发 A2A 任务用;**非数据维度**) | `entity` | 目标 `entity_id` |
|
||||
|
||||
**为什么两个字面不合并**:两者作用域不同(个人会话 vs 全局单值键),合并后审计上不可分辨
|
||||
「谁把自己切走了」与「谁改了全平台状态」。DAMA 复核提出「统一为 `switch_entity` 或登记字典」——
|
||||
本档取**后者(登记不合并)**,原因是前者会丢语义可分辨性;若后续裁定要求合并,改动面为
|
||||
`app/api/tenant.py` 一行字面 + 本表一行(可逆)。
|
||||
|
||||
**可归属与可过滤(2026-09-22 收口,随本档同一提交落地)**:
|
||||
|
||||
- `operation_logs` **无** `entity_id` 列(仅 `user_id / action / target_type / target_id / detail / created_at`)。
|
||||
故「按账套查换账套记录」不靠新列,而靠**目标维度**:
|
||||
```sql
|
||||
SELECT * FROM operation_logs WHERE target_type='entity' AND target_id=? ORDER BY id DESC;
|
||||
```
|
||||
- `/tenant/switch` 原写 `user_id=None`(端点无当前用户注入)⇒ 事后无法追操作人。
|
||||
已改为注入会话用户(`user=Depends(require_auth_401)` → `user_id=user.id`),
|
||||
并把 `target_type='entity'` / `target_id=entity_id` 与 `switch_entity` 对齐。
|
||||
- `detail` JSON 仍保留 `entity_id / entity_name / source / previous`(人读明细)。
|
||||
|
||||
**现网实况(只读核对,2026-09-22 11:15,`cma_reader` 16/16 PASS)**:
|
||||
`switch_entity` = 49 行、`switch_tenant_state` = 0 行(本单尚未部署 ⇒ 字面是新引入的,
|
||||
上线前库里无该字面属正常)。ops 止血那次 `POST /tenant/switch`(10:36:01,部署码)**没有**审计行——
|
||||
部署码该端点无审计,正是本单补齐项。
|
||||
|
||||
---
|
||||
|
||||
## 二、`/api/cma/notifications/logs` 口径(发送记录页)
|
||||
|
||||
- **归属口径**:`notification_logs` 无 `entity_id` 列 ⇒ 经**父键** join 归属:
|
||||
`notification_logs.alert_id → kpi_alerts.id → kpi_alerts.entity_id`;按会话账套过滤,
|
||||
与 batch5 对 `kpi_data_quality_log` 的处置同口径(**宁少勿错**)。
|
||||
- **`alert_id IS NULL` 的行**:无可归属父键 ⇒ **不返回给任何账套**(不跨账套暴露)。
|
||||
现库 8 行历史记录(2026-09-11/12「OKR 季度复盘提醒」)**100% 属此类**,
|
||||
写入方 = `backend/scripts/okr_review_reminder.py`(复用本表留存审计,`alert_id` 留空)。
|
||||
- **上线后现象(须预期,不是故障)**:`/notifications/logs` 在 e1/e2 均返回 `total=0`,
|
||||
前端发送记录页为空。现役预警推送写入器 `app/utils/notifier.py` **必带** `alert_id`,
|
||||
故此后由预警产生的新记录会正常显示;只有「无父键历史行」永不出现在该页。
|
||||
- **页面与接口的提示(随本档同一提交落地)**:
|
||||
- 接口返回 `policy="alert-linked-only"` + `note`(说明「仅显示与预警关联的推送」);
|
||||
- 前端 `NotificationManage.vue` 发送记录表 `empty-text` 写明同一口径。
|
||||
- **未做(留待 P2 裁定)**:为无父键通知单列只读视图(需先给 `notification_logs` 补账套维度,
|
||||
属 DDL/迁移,超出「零迁移 B2」范围)。
|
||||
|
||||
---
|
||||
|
||||
## 三、本档未覆盖(另立单,见对应任务文件)
|
||||
|
||||
1. `kpi_alerts` id 589/590 跨账套错挂(`entity_id=1` 挂 e2 的 `BH_ADM_EXPENSE`):数据订正须
|
||||
另立单 + 任总放行(生产写)。
|
||||
2. `kpi_id=1` 兜底残留清点(9 行、其中 1 行 `pending`)与归属裁定。
|
||||
3. 同源全局键继续拆账套:`kpi_alert_higher_better`、`kpi_visibility`(DAMA §7 反向挑错新增漏项)、
|
||||
`budget_mode` / `budget_driver_mode`(batch5 §七 已记录未施工)。
|
||||
@@ -42,7 +42,8 @@
|
||||
|
||||
<!-- 发送记录 -->
|
||||
<h3 class="section-title">发送记录</h3>
|
||||
<el-table :data="logs" style="width:100%;margin-top:8px;" size="small">
|
||||
<el-table :data="logs" style="width:100%;margin-top:8px;" size="small"
|
||||
empty-text="仅显示与预警推送关联的记录;无账套归属的历史通知不在此列">
|
||||
<el-table-column prop="created_at" label="时间" width="160" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" />
|
||||
<el-table-column prop="channel" label="渠道" width="80" />
|
||||
|
||||
Reference in New Issue
Block a user