|
@@ -1,6 +1,7 @@
|
|
|
"""异常 Case 闭环与审计日志。"""
|
|
"""异常 Case 闭环与审计日志。"""
|
|
|
|
|
|
|
|
import sqlite3
|
|
import sqlite3
|
|
|
|
|
+from contextlib import closing
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
import pandas as pd
|
|
import pandas as pd
|
|
@@ -9,10 +10,18 @@ from defect_analysis.database import init_database
|
|
|
|
|
|
|
|
|
|
|
|
|
VALID_CASE_STATUSES = {"OPEN", "IN_PROGRESS", "IMPROVED", "CLOSED", "REJECTED"}
|
|
VALID_CASE_STATUSES = {"OPEN", "IN_PROGRESS", "IMPROVED", "CLOSED", "REJECTED"}
|
|
|
|
|
+VALID_CASE_TRANSITIONS = {
|
|
|
|
|
+ "OPEN": {"IN_PROGRESS", "CLOSED", "REJECTED"},
|
|
|
|
|
+ "IN_PROGRESS": {"IMPROVED", "CLOSED", "REJECTED"},
|
|
|
|
|
+ "IMPROVED": {"CLOSED", "IN_PROGRESS"},
|
|
|
|
|
+ "CLOSED": set(),
|
|
|
|
|
+ "REJECTED": set(),
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
|
|
|
def _connect(db_path):
|
|
def _connect(db_path):
|
|
|
conn = sqlite3.connect(Path(db_path))
|
|
conn = sqlite3.connect(Path(db_path))
|
|
|
|
|
+ conn.execute("PRAGMA foreign_keys = ON")
|
|
|
conn.row_factory = sqlite3.Row
|
|
conn.row_factory = sqlite3.Row
|
|
|
return conn
|
|
return conn
|
|
|
|
|
|
|
@@ -41,7 +50,7 @@ def create_root_cause_case(
|
|
|
):
|
|
):
|
|
|
"""从根因候选创建异常 Case。"""
|
|
"""从根因候选创建异常 Case。"""
|
|
|
init_database(db_path)
|
|
init_database(db_path)
|
|
|
- with _connect(db_path) as conn:
|
|
|
|
|
|
|
+ with closing(_connect(db_path)) as conn:
|
|
|
cursor = conn.execute(
|
|
cursor = conn.execute(
|
|
|
"""
|
|
"""
|
|
|
INSERT INTO root_cause_cases (
|
|
INSERT INTO root_cause_cases (
|
|
@@ -70,6 +79,7 @@ def create_root_cause_case(
|
|
|
actor=created_by,
|
|
actor=created_by,
|
|
|
details=f"创建 Case: {title}; 建议: {recommendation}",
|
|
details=f"创建 Case: {title}; 建议: {recommendation}",
|
|
|
)
|
|
)
|
|
|
|
|
+ conn.commit()
|
|
|
return case_id
|
|
return case_id
|
|
|
|
|
|
|
|
|
|
|
|
@@ -78,21 +88,26 @@ def update_case_status(db_path, *, case_id, status, actor, note=""):
|
|
|
if status not in VALID_CASE_STATUSES:
|
|
if status not in VALID_CASE_STATUSES:
|
|
|
raise ValueError(f"无效 Case 状态: {status}")
|
|
raise ValueError(f"无效 Case 状态: {status}")
|
|
|
init_database(db_path)
|
|
init_database(db_path)
|
|
|
- with _connect(db_path) as conn:
|
|
|
|
|
|
|
+ with closing(_connect(db_path)) as conn:
|
|
|
current = conn.execute(
|
|
current = conn.execute(
|
|
|
"SELECT status FROM root_cause_cases WHERE case_id = ?",
|
|
"SELECT status FROM root_cause_cases WHERE case_id = ?",
|
|
|
(int(case_id),),
|
|
(int(case_id),),
|
|
|
).fetchone()
|
|
).fetchone()
|
|
|
if current is None:
|
|
if current is None:
|
|
|
raise ValueError(f"未找到 Case: {case_id}")
|
|
raise ValueError(f"未找到 Case: {case_id}")
|
|
|
- closed_at_expr = "CURRENT_TIMESTAMP" if status == "CLOSED" else "closed_at"
|
|
|
|
|
|
|
+ current_status = current["status"]
|
|
|
|
|
+ if status not in VALID_CASE_TRANSITIONS.get(current_status, set()):
|
|
|
|
|
+ raise ValueError(f"不允许的 Case 状态流转: {current_status} -> {status}")
|
|
|
|
|
+ closed_at = pd.Timestamp.utcnow().strftime("%Y-%m-%d %H:%M:%S") if status == "CLOSED" else None
|
|
|
conn.execute(
|
|
conn.execute(
|
|
|
- f"""
|
|
|
|
|
|
|
+ """
|
|
|
UPDATE root_cause_cases
|
|
UPDATE root_cause_cases
|
|
|
- SET status = ?, updated_at = CURRENT_TIMESTAMP, closed_at = {closed_at_expr}
|
|
|
|
|
|
|
+ SET status = ?,
|
|
|
|
|
+ updated_at = CURRENT_TIMESTAMP,
|
|
|
|
|
+ closed_at = COALESCE(?, closed_at)
|
|
|
WHERE case_id = ?
|
|
WHERE case_id = ?
|
|
|
""",
|
|
""",
|
|
|
- (status, int(case_id)),
|
|
|
|
|
|
|
+ (status, closed_at, int(case_id)),
|
|
|
)
|
|
)
|
|
|
_write_audit_log(
|
|
_write_audit_log(
|
|
|
conn,
|
|
conn,
|
|
@@ -100,8 +115,9 @@ def update_case_status(db_path, *, case_id, status, actor, note=""):
|
|
|
entity_id=case_id,
|
|
entity_id=case_id,
|
|
|
action="UPDATE_STATUS",
|
|
action="UPDATE_STATUS",
|
|
|
actor=actor,
|
|
actor=actor,
|
|
|
- details=f"{current['status']} -> {status}; {note}",
|
|
|
|
|
|
|
+ details=f"{current_status} -> {status}; {note}",
|
|
|
)
|
|
)
|
|
|
|
|
+ conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_cases(db_path, *, status=None):
|
|
def list_cases(db_path, *, status=None):
|
|
@@ -112,7 +128,7 @@ def list_cases(db_path, *, status=None):
|
|
|
if status is not None:
|
|
if status is not None:
|
|
|
where = "WHERE status = ?"
|
|
where = "WHERE status = ?"
|
|
|
params.append(status)
|
|
params.append(status)
|
|
|
- with _connect(db_path) as conn:
|
|
|
|
|
|
|
+ with closing(_connect(db_path)) as conn:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
f"""
|
|
f"""
|
|
|
SELECT case_id, title, status, candidate_type, candidate_value,
|
|
SELECT case_id, title, status, candidate_type, candidate_value,
|
|
@@ -139,7 +155,7 @@ def get_audit_logs(db_path, *, entity_type=None, entity_id=None):
|
|
|
clauses.append("entity_id = ?")
|
|
clauses.append("entity_id = ?")
|
|
|
params.append(int(entity_id))
|
|
params.append(int(entity_id))
|
|
|
where = "WHERE " + " AND ".join(clauses) if clauses else ""
|
|
where = "WHERE " + " AND ".join(clauses) if clauses else ""
|
|
|
- with _connect(db_path) as conn:
|
|
|
|
|
|
|
+ with closing(_connect(db_path)) as conn:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
f"""
|
|
f"""
|
|
|
SELECT audit_id, entity_type, entity_id, action, actor, details, created_at
|
|
SELECT audit_id, entity_type, entity_id, action, actor, details, created_at
|