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:
@@ -10,6 +10,7 @@ from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth, require_role, filter_kpis_by_role, kpi_visible_dims
|
||||
from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog, Entity, KPICausality, KPIHierarchy, User
|
||||
from app.utils.oplog import log_op
|
||||
from app.api.kpi_governance import validate_kpi_payload, kpi_issues_message
|
||||
from app.utils.threshold_parser import (
|
||||
calc_level as _tp_calc_level,
|
||||
@@ -794,7 +795,7 @@ def create_kpi(data: dict, db: Session = Depends(get_db), user=WRITE_ROLES, enti
|
||||
db.add(kpi)
|
||||
db.commit()
|
||||
db.refresh(kpi)
|
||||
_log(db, 1, "create", "kpi", kpi.id, data)
|
||||
_log(db, 1, "create", "kpi", kpi.id, data, entity_id=entity_id)
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
@@ -834,7 +835,7 @@ def update_kpi(kpi_id: int, data: dict, db: Session = Depends(get_db), user=WRIT
|
||||
if hasattr(kpi, k) and v is not None:
|
||||
setattr(kpi, k, v)
|
||||
db.commit()
|
||||
_log(db, 1, "update", "kpi", kpi_id, data)
|
||||
_log(db, 1, "update", "kpi", kpi_id, data, entity_id=entity_id)
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
@@ -907,14 +908,15 @@ def associate_kpi_map(kpi_id: int, data: dict, db: Session = Depends(get_db), us
|
||||
raise HTTPException(404, "战略地图不存在")
|
||||
kpi.map_id = map_id
|
||||
db.commit()
|
||||
_log(db, 1, "update", "kpi", kpi_id, {"action": "associate-map", "map_id": map_id})
|
||||
_log(db, 1, "update", "kpi", kpi_id, {"action": "associate-map", "map_id": map_id},
|
||||
entity_id=entity_id)
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
def _log(db, user_id, action, target_type, target_id, detail):
|
||||
log = OperationLog(user_id=user_id, action=action, target_type=target_type, target_id=target_id, detail=json.dumps(detail, ensure_ascii=False) if detail else None)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
def _log(db, user_id, action, target_type, target_id, detail, entity_id=None):
|
||||
"""统一走 log_op(2026-09-23 ④:审计写入统一带 `detail.entity_id`)"""
|
||||
return log_op(db, action, target_type=target_type, target_id=target_id,
|
||||
detail=detail, user_id=user_id, entity_id=entity_id, commit=True)
|
||||
|
||||
|
||||
@router.get("/{kpi_id}/objectives")
|
||||
|
||||
Reference in New Issue
Block a user