Files
verify-promotion-metric-def…/scripts/service/threshold_service.py
chenfuqiang 04026d0372
All checks were successful
技能自动化发布 / release (push) Successful in 7s
feat: 职称评审核验助手首版 v1.0.1(verify-promotion-metric-default)
2026-09-13 16:05:20 +08:00

189 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""评审门槛库 / 核验案例 的维护与查询。"""
from __future__ import annotations
import json
from typing import Any, Optional
from db.connection import get_conn, init_db
from service.verify_service import _METRIC_ALIASES, _snake, norm_metrics
from util.logging_config import get_skill_logger
from util.timeutil import now_unix
_KNOWN_RANKS = ("副教授", "教授")
def _log():
try:
return get_skill_logger()
except RuntimeError:
return None
def upsert_threshold(fields: dict[str, Any]) -> dict[str, Any]:
"""按(级别+指标键)幂等写入门槛。"""
normalized = norm_metrics({str(k): v for k, v in fields.items()})
rank = str(normalized.get("rank") or fields.get("rank") or "").strip()
metric_key = str(normalized.get("metricKey") or fields.get("metric_key") or fields.get("metricKey") or "").strip()
metric_name = str(normalized.get("metricName") or fields.get("metric_name") or "").strip()
rank = rank if rank in _KNOWN_RANKS else (str(fields.get("rank") or "").strip() or rank)
if rank not in _KNOWN_RANKS:
raise ValueError(f"申报级别只接受 {'、'.join(_KNOWN_RANKS)}:{rank}")
if not metric_key:
raise ValueError("指标键(metric_key)不能为空")
min_value = fields.get("min_value", fields.get("minValue"))
try:
min_value = float(min_value)
except (TypeError, ValueError) as exc:
raise ValueError(f"最低要求必须为数字:{min_value}") from exc
if min_value < 0:
raise ValueError(f"最低要求必须非负:{min_value}")
metric_name = metric_name or str(_METRIC_ALIASES.get(metric_key, (metric_key,))[0])
values = {
"rank": rank,
"metric_key": metric_key,
"metric_name": metric_name,
"min_value": min_value,
"unit": str(fields.get("unit") or "").strip(),
"basis_doc": str(fields.get("basis_doc") or fields.get("basisDoc") or "").strip(),
"clause": str(fields.get("clause") or "").strip(),
}
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT id FROM threshold_rules WHERE rank = ? AND metric_key = ?", (rank, metric_key))
row = cur.fetchone()
if row:
threshold_id = int(row[0])
cur.execute(
"""
UPDATE threshold_rules
SET metric_name = ?, min_value = ?, unit = ?, basis_doc = ?, clause = ?, updated_at = ?
WHERE id = ?
""",
(values["metric_name"], values["min_value"], values["unit"], values["basis_doc"], values["clause"], now_unix(), threshold_id),
)
action = "updated"
else:
cur.execute(
"""
INSERT INTO threshold_rules
(rank, metric_key, metric_name, min_value, unit, basis_doc, clause, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(values["rank"], values["metric_key"], values["metric_name"], values["min_value"], values["unit"], values["basis_doc"], values["clause"], now_unix(), now_unix()),
)
threshold_id = int(cur.lastrowid)
action = "created"
conn.commit()
finally:
conn.close()
log = _log()
if log:
log.info("threshold_upserted rank=%s metric=%s action=%s stage=threshold", rank, metric_key, action)
return {"ok": True, "rank": rank, "metric_key": metric_key, "id": threshold_id, "action": action}
def import_thresholds(rows: list[dict[str, Any]]) -> dict[str, Any]:
if not isinstance(rows, list) or not rows:
raise ValueError("导入内容应为非空 JSON 数组(门槛)")
if len(rows) > 200:
raise ValueError(f"单次导入条数过多({len(rows)}),上限 200 条")
results = [upsert_threshold(row) for row in rows]
return {
"ok": True,
"count": len(results),
"created": sum(1 for r in results if r["action"] == "created"),
"updated": sum(1 for r in results if r["action"] == "updated"),
"items": results,
}
def list_thresholds(rank: str = "") -> list[dict[str, Any]]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
if rank:
cur.execute("SELECT * FROM threshold_rules WHERE rank = ? ORDER BY id", (rank,))
else:
cur.execute("SELECT * FROM threshold_rules ORDER BY rank, id")
columns = [d[0] for d in cur.description]
rows = [dict(zip(columns, row)) for row in cur.fetchall()]
finally:
conn.close()
return rows
def list_cases(limit: int = 20, verdict: str = "") -> list[dict[str, Any]]:
init_db()
limit = max(1, min(int(limit), 200))
conn = get_conn()
try:
cur = conn.cursor()
if verdict:
cur.execute(
"SELECT id, batch_no, teacher_name, department, apply_rank, verdict, report_file, created_at FROM verify_cases WHERE verdict = ? ORDER BY id DESC LIMIT ?",
(verdict, limit),
)
else:
cur.execute(
"SELECT id, batch_no, teacher_name, department, apply_rank, verdict, report_file, created_at FROM verify_cases ORDER BY id DESC LIMIT ?",
(limit,),
)
columns = [d[0] for d in cur.description]
rows = [dict(zip(columns, row)) for row in cur.fetchall()]
finally:
conn.close()
return rows
def get_case(case_id: int) -> dict[str, Any]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT * FROM verify_cases WHERE id = ?", (case_id,))
row = cur.fetchone()
if not row:
raise ValueError(f"核验案例不存在:{case_id}")
columns = [d[0] for d in cur.description]
case = dict(zip(columns, row))
finally:
conn.close()
for key in ("matrix_json", "metrics_json"):
try:
case[key[:-5]] = json.loads(case.get(key) or "[]")
except (TypeError, ValueError):
case[key[:-5]] = [] if key == "matrix_json" else {}
return case
def stats() -> dict[str, Any]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT verdict, COUNT(*) FROM verify_cases GROUP BY verdict")
by_verdict = {str(r[0]): int(r[1]) for r in cur.fetchall()}
cur.execute("SELECT COUNT(*) FROM threshold_rules")
threshold_total = int(cur.fetchone()[0])
cur.execute("SELECT COUNT(*) FROM verify_cases")
case_total = int(cur.fetchone()[0])
cur.execute(
"SELECT teacher_name, department, apply_rank FROM verify_cases WHERE verdict = '硬伤' ORDER BY id DESC LIMIT 5"
)
red_cases = [{"teacher_name": r[0], "department": r[1], "apply_rank": r[2]} for r in cur.fetchall()]
finally:
conn.close()
return {
"ok": True,
"case_total": case_total,
"by_verdict": by_verdict,
"threshold_total": threshold_total,
"red_cases": red_cases,
}