fix: RANK_INVALID 落地 + 空门槛库不判安全(改判边缘+V1)+ 批量坏文件跳过 + 教授补一作/通讯独立门槛
This commit is contained in:
@@ -19,6 +19,7 @@ import uuid
|
||||
from typing import Any, Optional
|
||||
|
||||
from db.connection import get_conn, init_db
|
||||
from db.seed_data import KNOWN_RANKS
|
||||
from util.logging_config import get_skill_logger
|
||||
from util.timeutil import now_unix
|
||||
|
||||
@@ -39,6 +40,7 @@ _METRIC_ALIASES = {
|
||||
"applyRank": ("apply_rank", "申报级别", "申报职称"),
|
||||
"presidingProvincial": ("presiding_provincial", "主持省部级项目", "省部级项目"),
|
||||
"corePapers": ("core_papers", "核心期刊论文", "核心论文"),
|
||||
"firstAuthorCorePapers": ("first_author_core_papers", "核心期刊一作/通讯论文", "一作/通讯论文"),
|
||||
"teachingHours": ("teaching_hours", "年均教学工作量", "教学工作量"),
|
||||
"currentTitleYears": ("current_title_years", "任现职年限", "现职称年限"),
|
||||
"monographs": ("monographs", "专著/教材", "专著"),
|
||||
@@ -165,25 +167,43 @@ def verify_metrics(metrics_raw: dict[str, Any], thresholds: dict[str, dict[str,
|
||||
"advice": f"{advice}(依据:{basis})" if advice else "",
|
||||
})
|
||||
|
||||
# V1:申报指标中存在但门槛库未配置的键(排除标识字段与其别名形式)
|
||||
# V1:申报指标中存在、但该级别门槛库未配置的指标。
|
||||
# 覆盖判定以"该级别门槛键 + 其全部别名形式"为准——已知别名但本级别未配门槛
|
||||
# 的指标同样要出 V1 灰行,绝不能当作已核验而漏掉。
|
||||
identity_keys = {"teacher_name", "teacherName", "department", "apply_rank", "applyRank"}
|
||||
known_keys = set(thresholds.keys()) | set(_METRIC_ALIASES.keys())
|
||||
unconfigured = []
|
||||
|
||||
def _alias_canonical(key: str) -> Optional[str]:
|
||||
for canonical, names in _METRIC_ALIASES.items():
|
||||
if key == canonical or key == _snake(canonical) or key in names:
|
||||
return canonical
|
||||
return None
|
||||
|
||||
covered_names: set[str] = set()
|
||||
for tk in thresholds:
|
||||
covered_names.add(tk)
|
||||
covered_names.add(_camel(tk))
|
||||
for canonical, names in _METRIC_ALIASES.items():
|
||||
if canonical in thresholds or _snake(canonical) in thresholds:
|
||||
covered_names.add(canonical)
|
||||
covered_names.add(_snake(canonical))
|
||||
covered_names.update(names)
|
||||
|
||||
seen_v1: set[str] = set()
|
||||
for key in metrics:
|
||||
if key in identity_keys or key in known_keys:
|
||||
continue
|
||||
if _snake(key) in known_keys or _camel(key) in known_keys:
|
||||
if key in identity_keys or key in covered_names:
|
||||
continue
|
||||
if metrics.get(key) in (None, ""):
|
||||
continue
|
||||
if any(key in names for names in _METRIC_ALIASES.values()):
|
||||
canonical = _alias_canonical(key)
|
||||
v1_key = _snake(canonical) if canonical else str(key)
|
||||
if v1_key in thresholds or v1_key in seen_v1:
|
||||
continue
|
||||
unconfigured.append(str(key))
|
||||
for key in unconfigured:
|
||||
seen_v1.add(v1_key)
|
||||
has_yellow_or_gray = True
|
||||
display = str(_METRIC_ALIASES[canonical][0]) if canonical else str(key)
|
||||
matrix.append({
|
||||
"metric": str(_METRIC_ALIASES.get(key, (key,))[0]) if key in _METRIC_ALIASES else key,
|
||||
"metric_key": key,
|
||||
"metric": display,
|
||||
"metric_key": v1_key,
|
||||
"required": None,
|
||||
"actual": metrics.get(key),
|
||||
"margin": None,
|
||||
@@ -195,9 +215,12 @@ def verify_metrics(metrics_raw: dict[str, Any], thresholds: dict[str, dict[str,
|
||||
if has_red:
|
||||
verdict = VERDICT_RED
|
||||
summary = "存在不达门槛的硬伤项,暂缓提交;请按矩阵补足材料后重新核验。"
|
||||
elif has_yellow_or_gray:
|
||||
elif has_yellow_or_gray or not thresholds:
|
||||
verdict = VERDICT_BORDER
|
||||
summary = "无硬伤,但存在边缘项或待核数据,建议补充说明后提交。"
|
||||
if not thresholds:
|
||||
summary = f"门槛库未配置「{apply_rank}」级别的门槛,本次无法完成硬指标核验,请人事处补充配置后重新核验(V1)。"
|
||||
else:
|
||||
summary = "无硬伤,但存在边缘项或待核数据,建议补充说明后提交。"
|
||||
else:
|
||||
verdict = VERDICT_SAFE
|
||||
summary = "全部硬指标超标准 10% 以上,形式审查通过。"
|
||||
@@ -212,6 +235,35 @@ def verify_metrics(metrics_raw: dict[str, Any], thresholds: dict[str, dict[str,
|
||||
}
|
||||
|
||||
|
||||
def collect_input_cases(input_dir: str) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
|
||||
"""读取 input/ 目录下的申报案例 JSON;坏文件跳过并记录原因,不中断整批。
|
||||
|
||||
返回 (案例列表, 跳过文件列表);跳过文件形如 {"file": 名称, "reason": 原因}。
|
||||
"""
|
||||
import os
|
||||
|
||||
files = sorted(
|
||||
os.path.join(input_dir, name)
|
||||
for name in os.listdir(input_dir)
|
||||
if name.lower().endswith(".json")
|
||||
)
|
||||
cases: list[dict[str, Any]] = []
|
||||
skipped_files: list[dict[str, str]] = []
|
||||
for path in files:
|
||||
name = os.path.basename(path)
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
payload = json.load(f)
|
||||
if isinstance(payload, dict):
|
||||
payload = payload.get("cases") or [payload]
|
||||
if not isinstance(payload, list):
|
||||
raise ValueError("内容不是申报案例对象数组")
|
||||
cases.extend(item for item in payload if isinstance(item, dict))
|
||||
except (OSError, ValueError) as exc:
|
||||
skipped_files.append({"file": name, "reason": str(exc)})
|
||||
return cases, skipped_files
|
||||
|
||||
|
||||
def run_verify(cases: list[dict[str, Any]], *, batch_no: str = "") -> dict[str, Any]:
|
||||
"""核验内核:逐案例矩阵计算 → 写库 → 生成《核验报告》。"""
|
||||
from service.report_service import write_report_for_kernel, build_batch_brief
|
||||
@@ -234,7 +286,12 @@ def run_verify(cases: list[dict[str, Any]], *, batch_no: str = "") -> dict[str,
|
||||
for case in cases:
|
||||
if not isinstance(case, dict):
|
||||
raise ValueError("申报案例必须是 JSON 对象")
|
||||
result = verify_metrics(case, load_thresholds(str(case.get("applyRank") or case.get("apply_rank") or "").strip()))
|
||||
rank = str(case.get("applyRank") or case.get("apply_rank") or "").strip()
|
||||
if rank not in KNOWN_RANKS:
|
||||
raise ValueError(
|
||||
f"申报级别只接受 {'、'.join(KNOWN_RANKS)}:{rank or '(未提供)'}(RANK_INVALID)"
|
||||
)
|
||||
result = verify_metrics(case, load_thresholds(rank))
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO verify_cases
|
||||
|
||||
Reference in New Issue
Block a user