ci/woodpecker/push/main Pipeline was canceled
任务 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
93 lines
4.1 KiB
Python
93 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
"""只读侦查(生产读,本单 dama-entity-isolation-sweep-20260923 §一/§二 前置)
|
||
|
||
只 SELECT,无任何写。用于:
|
||
1. system_configs / role_permissions 全量键实况(含 kpi_alert_higher_better / budget_mode /
|
||
budget_driver_mode / current_tenant / cash.current_balance.* / alignment_config.*)
|
||
2. operation_logs 实况(action 分布、switch_entity / switch_tenant_state 行数、最近行)
|
||
3. entities 实况(账套清单)
|
||
4. kpi_definitions 列面(是否有自身方向字段)+ 逐账套 KPI 分布
|
||
用法: python3 <this> [--json out.json]
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
BACKEND = Path("/srv/hermes/shared/cma-management/backend")
|
||
os.chdir(BACKEND)
|
||
sys.path.insert(0, str(BACKEND))
|
||
|
||
from sqlalchemy import text # noqa: E402
|
||
from app.database import get_engine # noqa: E402
|
||
|
||
out = {}
|
||
eng = get_engine()
|
||
with eng.connect() as c:
|
||
out["db"] = c.execute(text("SELECT DATABASE()")).scalar()
|
||
out["system_configs"] = [
|
||
{"id": r[0], "config_key": r[1], "config_value": r[2], "description": r[3],
|
||
"updated_at": str(r[4])}
|
||
for r in c.execute(text(
|
||
"SELECT id, config_key, config_value, description, updated_at "
|
||
"FROM system_configs ORDER BY config_key"))
|
||
]
|
||
out["role_permissions"] = [
|
||
{"id": r[0], "key": r[1], "value": r[2], "updated_at": str(r[3])}
|
||
for r in c.execute(text("SELECT id, `key`, `value`, updated_at FROM role_permissions ORDER BY `key`"))
|
||
]
|
||
out["entities"] = [
|
||
{"id": r[0], "name": r[1], "short_name": r[2], "status": r[3]}
|
||
for r in c.execute(text("SELECT id, name, short_name, status FROM entities ORDER BY id"))
|
||
]
|
||
out["op_logs_actions"] = [
|
||
{"action": r[0], "n": r[1], "last": str(r[2])}
|
||
for r in c.execute(text(
|
||
"SELECT action, COUNT(*) n, MAX(created_at) last FROM operation_logs "
|
||
"GROUP BY action ORDER BY n DESC"))
|
||
]
|
||
out["op_logs_switch_rows"] = [
|
||
{"id": r[0], "user_id": r[1], "action": r[2], "target_type": r[3], "target_id": r[4],
|
||
"detail": (r[5] or "")[:300], "created_at": str(r[6])}
|
||
for r in c.execute(text(
|
||
"SELECT id, user_id, action, target_type, target_id, detail, created_at "
|
||
"FROM operation_logs WHERE action LIKE '%switch%' OR action LIKE '%tenant%' "
|
||
"ORDER BY id DESC LIMIT 10"))
|
||
]
|
||
out["op_logs_recent"] = [
|
||
{"id": r[0], "user_id": r[1], "action": r[2], "target_type": r[3], "target_id": r[4],
|
||
"created_at": str(r[5])}
|
||
for r in c.execute(text(
|
||
"SELECT id, user_id, action, target_type, target_id, created_at "
|
||
"FROM operation_logs ORDER BY id DESC LIMIT 12"))
|
||
]
|
||
out["system_configs_cols"] = [
|
||
{"col": r[0], "type": r[1], "null": r[2], "key": r[3], "default": str(r[4])}
|
||
for r in c.execute(text(
|
||
"SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT "
|
||
"FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() "
|
||
"AND TABLE_NAME IN ('system_configs','role_permissions','operation_logs') "
|
||
"ORDER BY TABLE_NAME, ORDINAL_POSITION"))
|
||
]
|
||
out["kpi_def_cols"] = [
|
||
r[0] for r in c.execute(text(
|
||
"SELECT COLUMN_NAME FROM information_schema.COLUMNS "
|
||
"WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kpi_definitions' "
|
||
"ORDER BY ORDINAL_POSITION"))
|
||
]
|
||
out["kpi_by_entity"] = [
|
||
{"entity_id": r[0], "n": r[1]} for r in c.execute(text(
|
||
"SELECT entity_id, COUNT(*) n FROM kpi_definitions GROUP BY entity_id ORDER BY entity_id"))
|
||
]
|
||
out["budgetplan_by_entity"] = [
|
||
{"entity_id": r[0], "n": r[1]} for r in c.execute(text(
|
||
"SELECT kd.entity_id, COUNT(*) n FROM budget_plans bp "
|
||
"JOIN kpi_definitions kd ON kd.id=bp.kpi_id GROUP BY kd.entity_id ORDER BY kd.entity_id"))
|
||
]
|
||
|
||
print(json.dumps(out, ensure_ascii=False, indent=1, default=str))
|
||
if "--json" in sys.argv:
|
||
p = sys.argv[sys.argv.index("--json") + 1]
|
||
Path(p).write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str))
|
||
print("WROTE", p)
|