feat(cma): audit write-side entity closure + entity-dimension data dictionary

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)
This commit is contained in:
wecom-fullstack
2026-09-23 23:08:26 +08:00
parent fb4609be14
commit 467472311d
18 changed files with 525 additions and 71 deletions
+17 -14
View File
@@ -8,6 +8,7 @@ from app.database import get_db
from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import KPIDefinition, BudgetPlan, KPIValue, OperationLog
from app.utils.oplog import log_op, SCOPE_BATCH
import json
import logging
from datetime import datetime
@@ -225,6 +226,7 @@ def generate_budget_from_kpis(
period = f"{year}-{month:02d}"
results = []
total_amount = 0
_kpi_entities = set() # 2026-09-23 ④:审计带账套(取被处理 KPI 自身的账套,不猜会话账套)
for item in items:
kpi_id = item.get("kpi_id")
@@ -238,6 +240,7 @@ def generate_budget_from_kpis(
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi:
continue
_kpi_entities.add(getattr(kpi, "entity_id", None))
# 检查是否已有记录
existing = db.query(BudgetPlan).filter(
@@ -283,20 +286,20 @@ def generate_budget_from_kpis(
"plan_id": plan_id,
})
# 操作日志
log = OperationLog(
user_id=getattr(current_user, "id", None),
action="kpi_generate_budget",
target_type="budget",
detail=json.dumps({
"year": year,
"month": month,
"version": version,
"item_count": len(results),
"total_amount": total_amount,
}, ensure_ascii=False),
)
db.add(log)
# 操作日志(2026-09-23 ④:detail 带账套;跨账套批量 ⇒ 留空 + 哨兵,不猜账套)
_entities = sorted(x for x in _kpi_entities if x is not None)
log_op(db, "kpi_generate_budget", target_type="budget",
user_id=getattr(current_user, "id", None),
detail={
"year": year,
"month": month,
"version": version,
"item_count": len(results),
"total_amount": total_amount,
"kpi_entity_ids": _entities,
},
entity_id=(_entities[0] if len(_entities) == 1 else None),
scope=(SCOPE_BATCH if len(_entities) > 1 else None))
db.commit()
return {