154 lines
4.6 KiB
Python
154 lines
4.6 KiB
Python
"""课程教学大纲体检 - 纯函数工具层。
|
||
|
||
设计原则:
|
||
- 仅依赖标准库;不依赖 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 |