chore: auto release commit (2026-09-14 16:38:51)
This commit is contained in:
154
scripts/util/score_util.py
Normal file
154
scripts/util/score_util.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""课程教学大纲体检 - 纯函数工具层。
|
||||
|
||||
设计原则:
|
||||
- 仅依赖标准库;不依赖 jiangchang_skill_core / jinja2 / python-docx
|
||||
- 这些函数是 service 层 / CLI 入口都要复用的纯计算,便于 unit test
|
||||
- 输入允许 str / list[str] / dict;返回稳定标签 / 数值
|
||||
|
||||
模块内函数:
|
||||
- clamp_score(value): 把 0~100 之外的分数裁剪到合法区间
|
||||
- weighted_total(format_score, matrix_score, ideology_score, weights): 加权总分
|
||||
- classify_audit_status(score, pass_thr, modify_thr): 根据总分给最终建议
|
||||
- parse_score_threshold(env_value, default): 从 env 字符串读取阈值
|
||||
- parse_ideology_keywords(env_value, default_keywords): 解析思政关键词 JSON 数组
|
||||
- count_keyword_hits(text, keywords): 统计文本中思政关键词命中数
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterable, Mapping
|
||||
|
||||
from util.constants import (
|
||||
AUDIT_STATUS_MODIFY,
|
||||
AUDIT_STATUS_PASS,
|
||||
AUDIT_STATUS_REWRITE,
|
||||
DEFAULT_WEIGHT_FORMAT,
|
||||
DEFAULT_WEIGHT_IDEOLOGY,
|
||||
DEFAULT_WEIGHT_MATRIX,
|
||||
TOTAL_SCORE_MAX,
|
||||
TOTAL_SCORE_MIN,
|
||||
)
|
||||
|
||||
|
||||
def clamp_score(value, min_val: float = TOTAL_SCORE_MIN, max_val: float = TOTAL_SCORE_MAX) -> float:
|
||||
"""把分数裁剪到 [min_val, max_val] 区间。None / 非数字输入 → min_val。"""
|
||||
try:
|
||||
v = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return float(min_val)
|
||||
if v < min_val:
|
||||
return float(min_val)
|
||||
if v > max_val:
|
||||
return float(max_val)
|
||||
return v
|
||||
|
||||
|
||||
def weighted_total(
|
||||
format_score,
|
||||
matrix_score,
|
||||
ideology_score,
|
||||
weights: Mapping[str, float] | None = None,
|
||||
) -> float:
|
||||
"""三个子项加权求和,权重默认 (0.3 / 0.4 / 0.3)。
|
||||
|
||||
weights 可选 dict,覆盖默认值;缺 key 时回退默认权重。
|
||||
返回值经 clamp_score 裁剪到 [0, 100]。
|
||||
"""
|
||||
w = weights or {}
|
||||
wf = float(w.get("format", DEFAULT_WEIGHT_FORMAT))
|
||||
wm = float(w.get("matrix", DEFAULT_WEIGHT_MATRIX))
|
||||
wi = float(w.get("ideology", DEFAULT_WEIGHT_IDEOLOGY))
|
||||
total = (
|
||||
clamp_score(format_score) * wf
|
||||
+ clamp_score(matrix_score) * wm
|
||||
+ clamp_score(ideology_score) * wi
|
||||
)
|
||||
return round(clamp_score(total), 2)
|
||||
|
||||
|
||||
def classify_audit_status(
|
||||
score,
|
||||
pass_thr,
|
||||
modify_thr,
|
||||
) -> str:
|
||||
"""根据总分给最终建议。
|
||||
|
||||
- score >= pass_thr → pass(直接送审)
|
||||
- modify_thr <= score < pass_thr → modify(修改后送审)
|
||||
- score < modify_thr → rewrite(重写)
|
||||
"""
|
||||
s = clamp_score(score)
|
||||
try:
|
||||
p = float(pass_thr)
|
||||
except (TypeError, ValueError):
|
||||
p = 0.0
|
||||
try:
|
||||
m = float(modify_thr)
|
||||
except (TypeError, ValueError):
|
||||
m = 0.0
|
||||
if p < m:
|
||||
p, m = m, p # 防御性:保证 pass >= modify
|
||||
if s >= p:
|
||||
return AUDIT_STATUS_PASS
|
||||
if s >= m:
|
||||
return AUDIT_STATUS_MODIFY
|
||||
return AUDIT_STATUS_REWRITE
|
||||
|
||||
|
||||
def parse_score_threshold(env_value, default: float) -> float:
|
||||
"""从 env 字符串读取分数阈值;非法值回退 default。"""
|
||||
if env_value is None:
|
||||
return float(default)
|
||||
s = str(env_value).strip()
|
||||
if not s:
|
||||
return float(default)
|
||||
try:
|
||||
v = float(s)
|
||||
except ValueError:
|
||||
return float(default)
|
||||
if v < TOTAL_SCORE_MIN or v > TOTAL_SCORE_MAX:
|
||||
return float(default)
|
||||
return v
|
||||
|
||||
|
||||
def parse_ideology_keywords(env_value, default_keywords: Iterable[str]) -> list[str]:
|
||||
"""从 env(JSON 数组字符串)解析思政关键词列表。
|
||||
|
||||
- 空 / 非法 JSON → 默认关键词列表
|
||||
- 元素需为 str;非 str 一律跳过
|
||||
"""
|
||||
if env_value is None:
|
||||
return list(default_keywords)
|
||||
s = str(env_value).strip()
|
||||
if not s:
|
||||
return list(default_keywords)
|
||||
try:
|
||||
data = json.loads(s)
|
||||
except (ValueError, TypeError):
|
||||
return list(default_keywords)
|
||||
if not isinstance(data, list):
|
||||
return list(default_keywords)
|
||||
out: list[str] = []
|
||||
for item in data:
|
||||
if isinstance(item, str) and item.strip():
|
||||
out.append(item.strip())
|
||||
return out or list(default_keywords)
|
||||
|
||||
|
||||
def count_keyword_hits(text: str, keywords: Iterable[str]) -> dict[str, int]:
|
||||
"""统计文本中每个关键词出现的次数(不区分大小写)。
|
||||
|
||||
返回 {keyword: count};不命中的关键词不出现在结果中(count=0 省略)。
|
||||
"""
|
||||
if not isinstance(text, str):
|
||||
return {}
|
||||
lower = text.lower()
|
||||
out: dict[str, int] = {}
|
||||
for kw in keywords:
|
||||
if not isinstance(kw, str) or not kw:
|
||||
continue
|
||||
c = lower.count(kw.lower())
|
||||
if c > 0:
|
||||
out[kw] = c
|
||||
return out
|
||||
Reference in New Issue
Block a user