Files
gaokao-volunteer-system/data/crowd_db/special_programs_loader.py
Hermes Agent aeb1fd56a6
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(rules): Phase1 - 提前批军校/公安 + 专项计划规则补充
数据层:
1. special_programs.json: 新增2个项目(军校本科提前批+公安院校本科提前批)
   总项目数从12提升到14
2. special_programs_rules.json: 新增11条规则
   - 军校: 年龄限制+军检+政审+分数优势 (4条)
   - 公安: 体能测试+联考入警率+公安专业vs非公安专业 (3条)
   - 专项计划: 国家专项资格+地方专项资格+高校专项报名+降分对比 (4条)
   总规则数从23提升到34
3. crowd_db 31省JSON: 新增军校(29省)+公安院校(30省) program_type标记

代码层:
4. special_programs_loader.py: 新增4个查询接口
   - list_programs_by_batch(按批次筛选)
   - list_programs_by_category(按类别筛选)
   - list_categories(列出所有类别)
   - get_rules_by_category(按类别获取规则)

前端层:
5. 政策中心页增加'提前批与专项计划'板块
   含7类特殊招生类型说明

测试: 60+15=75 passed
2026-06-29 07:50:19 +08:00

186 lines
6.9 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.
"""特殊批次定向培养计划查询模块。
加载 data/crowd_db/special_programs.json提供按省份/分数匹配特殊批次推荐的能力。
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
_SPECIAL_PROGRAMS_PATH = Path(__file__).resolve().parent / "special_programs.json"
_SPECIAL_RULES_PATH = (
Path(__file__).resolve().parents[1] / "rules" / "special_programs_rules.json"
)
class SpecialProgramsLoader:
"""加载特殊批次定向培养计划数据。"""
def __init__(
self, data_path: Path | None = None, rules_path: Path | None = None
) -> None:
self._data_path = data_path or _SPECIAL_PROGRAMS_PATH
self._rules_path = rules_path or _SPECIAL_RULES_PATH
self._data: dict[str, Any] | None = None
self._rules: dict[str, Any] | None = None
@property
def data(self) -> dict[str, Any]:
if self._data is None:
self._data = json.loads(self._data_path.read_text(encoding="utf-8"))
return self._data # type: ignore[return-value]
@property
def rules(self) -> dict[str, Any]:
if self._rules is None:
self._rules = json.loads(self._rules_path.read_text(encoding="utf-8"))
return self._rules # type: ignore[return-value]
def list_programs(self) -> list[dict[str, Any]]:
"""列出全部 5 类特殊批次项目。"""
return self.data.get("programs", [])
def get_program(self, program_type: str) -> dict[str, Any] | None:
"""按 program_type 获取单个项目详情。"""
for p in self.list_programs():
if p.get("program_type") == program_type:
return p
return None
def list_programs_for_province(self, province: str) -> list[dict[str, Any]]:
"""列出某省份适用的特殊批次项目。"""
province = province.strip()
result = []
for p in self.list_programs():
applicable = p.get("applicable_provinces") or []
if "全国" in applicable or province in applicable:
result.append(p)
return result
def find_matching_schools(
self,
province: str,
score: int,
*,
program_type: str | None = None,
) -> list[dict[str, Any]]:
"""根据省份和分数,匹配可捡漏的特殊批次院校。
Args:
province: 考试省份。
score: 考生分数。
program_type: 可选,限定项目类型。
Returns:
匹配的院校列表,每个含 school/major/score_min/program_type 等。
"""
province = province.strip()
program_schools = self.data.get("program_schools", {})
results: list[dict[str, Any]] = []
for ptype, schools in program_schools.items():
if program_type and ptype != program_type:
continue
for s in schools:
s_province = s.get("province", "")
# "全国" 适用所有省份
if s_province != "全国" and s_province != province:
continue
score_min = s.get("score_min", 999)
# 考生分数 >= 院校最低分 * 0.9(允许一定弹性)
if score >= int(score_min) * 0.85:
results.append({
**s,
"program_type": ptype,
"gap": score - int(score_min),
})
# 按分数差升序(越接近的越优先推荐)
results.sort(key=lambda x: abs(x.get("gap", 0)))
return results
def get_applicable_rules(self, program_type: str) -> list[dict[str, Any]]:
"""获取某项目类型适用的规则列表。"""
all_rules = self.rules.get("rules", [])
return [r for r in all_rules if program_type in (r.get("applies_to") or [])]
def build_recommendation_for_review(
self,
province: str,
score: int,
) -> list[dict[str, Any]]:
"""为审核/复核场景生成特殊批次推荐摘要。
返回格式适合直接注入 LLM prompt 或 ReviewResultContract。
"""
programs = self.list_programs_for_province(province)
if not programs:
return []
recommendations = []
for p in programs:
ptype = p.get("program_type", "")
matching_schools = self.find_matching_schools(
province, score, program_type=ptype
)
if not matching_schools:
continue
# 取最近的院校
best_school = matching_schools[0]
features = p.get("key_features", {})
recommendations.append({
"program_type": ptype,
"program_name": p.get("program_name", ""),
"description": p.get("description", ""),
"batch": p.get("batch", ""),
"best_match_school": best_school.get("school", ""),
"best_match_major": best_school.get("major", ""),
"best_match_score_min": best_school.get("score_min", 0),
"tuition": features.get("tuition", ""),
"employment": features.get("employment", ""),
"service_years": features.get("service_years"),
"exam_required": features.get("exam_required", False),
"physical_check": features.get("physical_check", ""),
"match_score": max(0, 100 - abs(best_school.get("gap", 0))),
"warnings": p.get("warnings", []),
})
# 按 match_score 降序
recommendations.sort(key=lambda x: x.get("match_score", 0), reverse=True)
return recommendations
def list_programs_by_batch(self, batch: str) -> list[dict[str, Any]]:
"""按批次筛选项目(如"本科提前批"/"专科提前批"/"本科批")。"""
return [p for p in self.list_programs() if batch in p.get("batch", "")]
def list_programs_by_category(self, category: str) -> list[dict[str, Any]]:
"""按类别筛选规则关联的项目(如"提前批-军校"/"专项计划")。"""
rule_types = set()
for r in self.rules.get("rules", []):
if r.get("category") == category:
pt = r.get("program_type")
if pt:
rule_types.add(pt)
return [p for p in self.list_programs() if p.get("program_type") in rule_types]
def list_categories(self) -> list[str]:
"""列出所有规则类别。"""
return sorted(set(
r.get("category", "")
for r in self.rules.get("rules", [])
if r.get("category")
))
def get_rules_by_category(self, category: str) -> list[dict[str, Any]]:
"""按类别获取规则。"""
return [
r for r in self.rules.get("rules", [])
if r.get("category") == category
]