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:
@@ -22,6 +22,7 @@ from app.models import KPICausality
|
||||
from app.risk_levels import risk_level
|
||||
# 裁定 R1/R5(2026-09-19 财务口径):demo 账套任何写通道不产生业务预警;期间非规整月不入库(单一事实源)
|
||||
from app.utils.alert_entity_scope import entity_alert_guard, guard_alert_period
|
||||
from app.utils.oplog import log_op, SCOPE_GLOBAL
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("cma.bot_bridge")
|
||||
@@ -971,11 +972,11 @@ def bot_dict_zxt_account(
|
||||
before = db.execute(text("SELECT COUNT(*) FROM zxt_account_dict")).scalar() or 0
|
||||
db.execute(stmt, clean)
|
||||
after = db.execute(text("SELECT COUNT(*) FROM zxt_account_dict")).scalar() or 0
|
||||
db.add(OperationLog(
|
||||
action="import", target_type="zxt_account_dict", target_id=None,
|
||||
detail={"bot": bot.get("name"), "endpoint": "/api/cma/bot/dict/zxt-account",
|
||||
"upserted": len(clean), "before": before, "after": after, "source": source},
|
||||
))
|
||||
log_op(db, "import", target_type="zxt_account_dict", target_id=None,
|
||||
detail={"bot": bot.get("name"), "endpoint": "/api/cma/bot/dict/zxt-account",
|
||||
"upserted": len(clean), "before": before, "after": after, "source": source},
|
||||
# zxt_account_dict = 智享通平台级科目字典(无 entity 列)⇒ 显式声明非账套维度
|
||||
entity_id=None, scope=SCOPE_GLOBAL)
|
||||
db.commit()
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -1263,13 +1264,12 @@ def bot_subjects_upsert(
|
||||
}, synchronize_session=False)
|
||||
db.flush()
|
||||
after = db.query(func.count(Subject.id)).scalar() or 0
|
||||
db.add(OperationLog(
|
||||
action="import", target_type="subjects", target_id=None,
|
||||
detail={"bot": bot.get("name"), "endpoint": "/api/cma/bot/subjects",
|
||||
"risk_level": "L3", "inserted": len(to_insert), "updated": len(to_update),
|
||||
"skipped": len(to_skip), "before": before, "after": after,
|
||||
"update_existing": update_existing, "source": "json"},
|
||||
))
|
||||
log_op(db, "import", target_type="subjects", target_id=None,
|
||||
detail={"bot": bot.get("name"), "endpoint": "/api/cma/bot/subjects",
|
||||
"risk_level": "L3", "inserted": len(to_insert), "updated": len(to_update),
|
||||
"skipped": len(to_skip), "before": before, "after": after,
|
||||
"update_existing": update_existing, "source": "json"},
|
||||
entity_id=entity_id)
|
||||
db.commit()
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user