Files
gaokao-volunteer-system/docs/plans/T1-4-to-1-10-audit-completion.md
Hermes Agent 56a9327e78 feat: 技术架构设计 + 详细实施计划
根据产品规划设计完成技术设计和实施分解:

1. docs/TECH_ARCHITECTURE.md(新,587行)
   - 分层架构图(接入层/网关/服务/数据/基础设施)
   - 技术栈选型(Python + SQLite + FastAPI)
   - 核心模块设计:
     * AI审核服务(49元版)
     * 反扎堆检测
     * 数据溯源
     * 订单管理
   - 数据架构与目录结构
   - 安全设计(脱敏/权限/审计)
   - 性能指标
   - 11项技术决策记录

2. docs/IMPLEMENTATION_PLAN.md(新,432行)
   - 5大开发任务(30天)
   - T1: AI审核服务(10天,P0核心)
   - T2: 反扎堆检测(5天,P0)
   - T3: 数据溯源(5天,P1)
   - T4: 订单管理(5天,P1)
   - T5: 集成测试(5天,P0)
   - 详细周计划(4周)
   - 每个Task的DoD
   - 风险与应对

3. docs/plans/ 新增3份详细计划:
   - T1-1-crowd-db-setup.md (扎堆数据库,3个子任务)
   - T1-2-audit-skill-and-parser.md (Skill+解析器)
   - T1-4-to-1-10-audit-completion.md (T1.4-1.8完整代码)

4. docs/NAVIGATION.md 更新
   - 添加新文档索引

每个实施计划都包含:
- TDD流程(写测试→失败→实现→通过→提交)
- 完整可复制代码
- 精确文件路径
- 验证命令和预期输出
- 提交步骤

技术决策:
- 选用SQLite(本地优先,零配置)
- FastAPI作为Web框架
- 扎堆数据库手动维护(合规考虑)
- 数据溯源JSON文件存储(Git友好)
2026-06-12 09:18:52 +08:00

30 KiB
Raw Blame History

T1.4-T1.10 实施计划汇总

AI审核服务剩余任务详细计划

For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.


Task 1.4.1: 集成规范检查器

Objective: 实现 checker_integration.py复用现有规范检查器

Files:

  • Create: skills/gaokao-audit/scripts/checker_integration.py
  • Create: skills/gaokao-audit/tests/test_checker_integration.py

Step 1: 写测试

"""规范检查集成测试"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

import pytest
from skills.gaokao-audit.scripts.checker_integration import CheckerIntegration


def test_check_hunan_plan():
    """测试检查湖南方案"""
    checker = CheckerIntegration()
    plan_text = "湖南 578分 45个学校 院校专业组"
    result = checker.check(plan_text, province="湖南")

    assert "errors" in result
    assert "summary" in result
    # 应该检测出"45个学校"这个错误(致命)


def test_check_zhejiang_plan():
    """测试检查浙江方案"""
    checker = CheckerIntegration()
    plan_text = "浙江 620分 80个院校专业组"
    result = checker.check(plan_text, province="浙江")

    # 浙江是"专业+学校"模式,不是院校专业组
    # 应该检查模式是否正确
    assert "errors" in result


def test_check_unknown_province():
    """测试不存在的省份"""
    checker = CheckerIntegration()
    result = checker.check("test", province="不存在的省")

    # 应该优雅处理
    assert "errors" in result


def test_format_check_results():
    """测试结果格式化"""
    checker = CheckerIntegration()
    result = checker.check("test", province="湖南")
    formatted = checker.format_results(result)

    assert isinstance(formatted, dict)
    assert "policy_errors" in formatted
    assert "fatal_count" in formatted

Step 2: 实现 checker_integration.py

"""
规范检查集成

集成 gaokao-spec-checker 复用27省规则库。
"""
import sys
import os
from typing import Dict, Any

# 添加路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

from skills.gaokao-spec-checker.scripts.spec_checker_v2 import (
    GaokaoSpecCheckerV2,
    PROVINCE_RULES,
    detect_province,
)


class CheckerIntegration:
    """规范检查集成器"""

    def __init__(self):
        self.checker = GaokaoSpecCheckerV2()

    def check(self, plan_text: str, province: str = None) -> Dict[str, Any]:
        """执行规范检查

        Args:
            plan_text: 方案文本
            province: 省份(可选,自动检测)

        Returns:
            检查结果字典
        """
        # 自动检测省份
        if not province:
            province = detect_province(plan_text)

        # 执行检查
        if province:
            report = self.checker.auto_detect_and_check(plan_text)
        else:
            report = "未识别省份"

        # 解析报告
        errors = self._parse_report(report)

        return {
            "province": province,
            "errors": errors,
            "summary": self._summarize(errors),
            "raw_report": report,
        }

    def _parse_report(self, report: str) -> Dict[str, list]:
        """解析报告为结构化数据"""
        result = {"fatal": [], "warning": [], "info": []}

        current_section = None
        for line in report.split("\n"):
            line = line.strip()

            if "致命错误" in line:
                current_section = "fatal"
            elif "严重错误" in line or "严重警告" in line:
                current_section = "warning"
            elif "一般警告" in line or "一般提示" in line:
                current_section = "info"
            elif line and current_section and (line[0].isdigit() or line.startswith("•")):
                # 简化的错误提取
                result[current_section].append({
                    "description": line,
                })

        return result

    def _summarize(self, errors: Dict[str, list]) -> Dict[str, int]:
        """生成摘要"""
        return {
            "fatal_count": len(errors.get("fatal", [])),
            "warning_count": len(errors.get("warning", [])),
            "info_count": len(errors.get("info", [])),
        }

    def format_results(self, result: Dict[str, Any]) -> Dict[str, Any]:
        """格式化结果"""
        return {
            "policy_errors": result["errors"].get("fatal", []),
            "warnings": result["errors"].get("warning", []),
            "info": result["errors"].get("info", []),
            "fatal_count": result["summary"]["fatal_count"],
            "warning_count": result["summary"]["warning_count"],
            "info_count": result["summary"]["info_count"],
        }

Step 3: 提交

git add skills/gaokao-audit/scripts/checker_integration.py
git add skills/gaokao-audit/tests/test_checker_integration.py
git commit -m "feat(audit): 集成规范检查器 - T1.4.1"

Task 1.5.1: 实现扎堆检测器

Objective: 实现 crowd_detector.py检测方案中院校的扎堆风险

Files:

  • Create: skills/gaokao-audit/scripts/crowd_detector.py
  • Create: skills/gaokao-audit/tests/test_crowd_detector.py

Step 1: 写测试

"""扎堆检测器测试"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

import pytest
from skills.gaokao-audit.scripts.crowd_detector import CrowdDetector, CrowdRisk


@pytest.fixture
def detector():
    return CrowdDetector()


def test_detect_high_risk_school(detector):
    """测试检测高风险院校"""
    plan = [
        {"school": "长沙理工大学", "major": "会计学"},
    ]
    risks = detector.detect_risks(plan, province="湖南", score=575)

    assert len(risks) >= 1
    first = risks[0]
    assert "长沙理工" in first.school
    assert first.risk_level == "high"
    assert first.predicted_increase == 18


def test_detect_no_risk(detector):
    """测试没有扎堆风险"""
    plan = [
        {"school": "某某不知名学校", "major": "某专业"},
    ]
    risks = detector.detect_risks(plan, province="湖南", score=575)

    assert len(risks) == 0


def test_detect_multiple_risks(detector):
    """测试多个风险"""
    plan = [
        {"school": "长沙理工大学", "major": "会计学"},
        {"school": "江西财经大学", "major": "会计学"},
        {"school": "湖南工商大学", "major": "会计学"},
    ]
    risks = detector.detect_risks(plan, province="湖南", score=575)

    # 前两个有风险,第三个没有
    assert len(risks) == 2


def test_get_risk_label(detector):
    """测试风险等级标签"""
    assert detector.get_risk_label(4) == "🔴 高风险"
    assert detector.get_risk_label(3) == "🟡 中风险"
    assert detector.get_risk_label(1) == "🟢 低风险"


def test_alternatives_included(detector):
    """测试包含替代方案"""
    plan = [{"school": "长沙理工大学", "major": "会计学"}]
    risks = detector.detect_risks(plan, province="湖南", score=575)

    assert len(risks) >= 1
    assert len(risks[0].alternatives) > 0
    assert all("name" in a for a in risks[0].alternatives)


def test_format_risks_for_report(detector):
    """测试报告格式化"""
    plan = [{"school": "长沙理工大学", "major": "会计学"}]
    risks = detector.detect_risks(plan, province="湖南", score=575)

    formatted = detector.format_for_report(risks)
    assert isinstance(formatted, list)
    if formatted:
        assert "name" in formatted[0]
        assert "risk_level_label" in formatted[0]
        assert "predicted_increase" in formatted[0]

Step 2: 实现 crowd_detector.py

"""
扎堆检测器

检测方案中的院校是否被大厂AI高频推荐存在扎堆风险。
"""
import sys
import os
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

from data.crowd_db.loader import CrowdDBLoader


@dataclass
class CrowdRisk:
    """扎堆风险"""
    school: str
    major: str
    frequency: int
    platforms: List[str]
    predicted_increase: int
    risk_level: str  # high/medium/low
    alternatives: List[Dict[str, Any]] = field(default_factory=list)

    @property
    def risk_level_label(self) -> str:
        """中文标签"""
        labels = {
            "high": "🔴 高风险",
            "medium": "🟡 中风险",
            "low": "🟢 低风险",
        }
        return labels.get(self.risk_level, self.risk_level)


class CrowdDetector:
    """扎堆检测器"""

    def __init__(self, loader: Optional[CrowdDBLoader] = None):
        self.loader = loader or CrowdDBLoader()

    def detect_risks(
        self,
        volunteers: List[Dict[str, str]],
        province: str,
        score: int
    ) -> List[CrowdRisk]:
        """检测扎堆风险

        Args:
            volunteers: 志愿列表 [{"school": "xxx", "major": "yyy"}]
            province: 省份
            score: 用户分数

        Returns:
            CrowdRisk列表
        """
        risks = []

        for vol in volunteers:
            school = vol.get("school", "")
            major = vol.get("major", "")

            # 在大厂AI推荐库中查找
            rec = self.loader.find_recommendation_by_school(province, school)
            if not rec:
                continue

            # 检查分数段匹配
            score_match = self._is_in_score_range(province, score, school)
            if not score_match:
                continue

            # 创建风险对象
            risk = CrowdRisk(
                school=rec["name"],
                major=major or rec.get("major", ""),
                frequency=rec["frequency"],
                platforms=rec.get("platforms", []),
                predicted_increase=rec["predicted_increase"],
                risk_level=self._get_risk_level(rec["frequency"]),
                alternatives=rec.get("alternatives", []),
            )
            risks.append(risk)

        return risks

    def _is_in_score_range(self, province: str, score: int, school: str) -> bool:
        """检查学校是否在用户的分数段推荐中"""
        recs = self.loader.find_recommendations(province, score)
        return any(r["name"] in school or school in r["name"] for r in recs)

    def _get_risk_level(self, frequency: int) -> str:
        """根据频次计算风险等级"""
        if frequency >= 4:
            return "high"
        elif frequency >= 2:
            return "medium"
        else:
            return "low"

    def get_risk_label(self, frequency: int) -> str:
        """获取风险等级标签(供测试)"""
        level = self._get_risk_level(frequency)
        labels = {
            "high": "🔴 高风险",
            "medium": "🟡 中风险",
            "low": "🟢 低风险",
        }
        return labels.get(level, level)

    def format_for_report(self, risks: List[CrowdRisk]) -> List[Dict[str, Any]]:
        """格式化为报告格式"""
        return [
            {
                "name": r.school,
                "major": r.major,
                "frequency": r.frequency,
                "predicted_increase": r.predicted_increase,
                "risk_level": r.risk_level,
                "risk_level_label": r.risk_level_label,
                "platforms": r.platforms,
                "alternatives": r.alternatives,
            }
            for r in risks
        ]

Step 3: 运行测试

python3 -m pytest skills/gaokao-audit/tests/test_crowd_detector.py -v

Step 4: 提交

git add skills/gaokao-audit/scripts/crowd_detector.py
git add skills/gaokao-audit/tests/test_crowd_detector.py
git commit -m "feat(audit): 实现扎堆检测器 - T1.5.1"

Task 1.6.1: 实现审核服务主类

Objective: 实现 audit_service.py 主入口

Files:

  • Create: skills/gaokao-audit/scripts/audit_service.py
  • Create: skills/gaokao-audit/tests/test_audit_service.py

Step 1: 写测试

"""审核服务测试"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

import pytest
from skills.gaokao-audit.scripts.audit_service import AuditService, AuditResult


@pytest.fixture
def service():
    return AuditService()


@pytest.fixture
def sample_plan_text():
    return """
百度AI志愿助手为您推荐

考生信息
省份:湖南
高考分数578
位次约26800
选科:物理+化学+生物

推荐院校
1. 长沙理工大学 - 会计学
2. 湖南师范大学 - 会计学
3. 江西财经大学 - 会计学
4. 湘潭大学 - 工商管理
5. 湖南工商大学 - 财务管理
"""


def test_audit_plan_basic(service, sample_plan_text):
    """测试审核基本功能"""
    result = service.audit(sample_plan_text, format="text")

    assert isinstance(result, AuditResult)
    assert result.province == "湖南"
    assert result.candidate_score == 578
    assert len(result.volunteers) >= 3


def test_audit_detects_crowd_risk(service, sample_plan_text):
    """测试审核能检测扎堆风险"""
    result = service.audit(sample_plan_text, format="text")

    # 长沙理工大学会计学应该是高风险
    assert len(result.crowd_risks) >= 1
    high_risks = [r for r in result.crowd_risks if r.risk_level == "high"]
    assert len(high_risks) >= 1


def test_audit_calculates_score(service, sample_plan_text):
    """测试审核计算综合评分"""
    result = service.audit(sample_plan_text, format="text")

    assert 0 <= result.overall_score <= 100
    # 有扎堆风险的方案分数应该不高
    assert result.overall_score < 90


def test_audit_generates_suggestions(service, sample_plan_text):
    """测试审核生成建议"""
    result = service.audit(sample_plan_text, format="text")

    assert len(result.suggestions) > 0


def test_audit_to_dict(service, sample_plan_text):
    """测试审核结果序列化"""
    result = service.audit(sample_plan_text, format="text")
    d = result.to_dict()

    assert isinstance(d, dict)
    assert "province" in d
    assert "overall_score" in d
    assert "crowd_risks" in d
    assert "policy_errors" in d
    assert "suggestions" in d


def test_audit_data_trace(service, sample_plan_text):
    """测试数据溯源检查"""
    result = service.audit(sample_plan_text, format="text")

    # 数据溯源是警告级别
    assert hasattr(result, 'data_issues')

Step 2: 实现 audit_service.py

"""
审核服务主类

组合各模块,提供端到端的审核服务。
"""
import sys
import os
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

from skills.gaokao-audit.scripts.plan_parser import PlanParser, ParsedPlan
from skills.gaokao-audit.scripts.checker_integration import CheckerIntegration
from skills.gaokao-audit.scripts.crowd_detector import CrowdDetector, CrowdRisk


@dataclass
class AuditResult:
    """审核结果"""
    # 考生信息
    province: str = None
    candidate_score: int = None
    candidate_rank: int = None
    subjects: str = None
    source: str = None
    volunteers: List[Dict[str, Any]] = field(default_factory=list)

    # 检查结果
    policy_errors: List[Dict[str, Any]] = field(default_factory=list)
    crowd_risks: List[CrowdRisk] = field(default_factory=list)
    data_issues: List[str] = field(default_factory=list)
    suggestions: List[str] = field(default_factory=list)

    # 综合评分
    overall_score: int = 100

    def to_dict(self) -> Dict[str, Any]:
        d = asdict(self)
        # 转换CrowdRisk为dict
        d["crowd_risks"] = [
            {
                "school": r.school,
                "major": r.major,
                "frequency": r.frequency,
                "predicted_increase": r.predicted_increase,
                "risk_level": r.risk_level,
                "risk_level_label": r.risk_level_label,
                "platforms": r.platforms,
                "alternatives": r.alternatives,
            }
            for r in self.crowd_risks
        ]
        return d


class AuditService:
    """审核服务"""

    def __init__(self):
        self.parser = PlanParser()
        self.checker = CheckerIntegration()
        self.detector = CrowdDetector()

    def audit(self, plan_text: str, format: str = "text") -> AuditResult:
        """执行审核

        Args:
            plan_text: 方案文本
            format: 格式 'text' | 'pdf_text' | 'screenshot_ocr'

        Returns:
            AuditResult对象
        """
        # 1. 解析方案
        parsed = self.parser.parse_text(plan_text)

        # 2. 政策检查
        check_result = self.checker.check(plan_text, province=parsed.province)
        policy_errors = check_result["errors"]["fatal"]
        warnings = check_result["errors"]["warning"]

        # 3. 扎堆检测
        crowd_risks = []
        if parsed.province and parsed.score:
            crowd_risks = self.detector.detect_risks(
                parsed.volunteers,
                province=parsed.province,
                score=parsed.score,
            )

        # 4. 数据溯源检查
        data_issues = self._check_data_trace(parsed)

        # 5. 生成建议
        suggestions = self._generate_suggestions(
            policy_errors, crowd_risks, data_issues
        )

        # 6. 计算综合评分
        overall_score = self._calculate_score(
            policy_errors, crowd_risks, data_issues
        )

        return AuditResult(
            province=parsed.province,
            candidate_score=parsed.score,
            candidate_rank=parsed.rank,
            subjects=parsed.subjects,
            source=parsed.source,
            volunteers=parsed.volunteers,
            policy_errors=policy_errors,
            crowd_risks=crowd_risks,
            data_issues=data_issues,
            suggestions=suggestions,
            overall_score=overall_score,
        )

    def _check_data_trace(self, parsed: ParsedPlan) -> List[str]:
        """检查数据溯源"""
        issues = []

        # 检查是否标注数据来源
        if not parsed.source:
            issues.append("未明确标注AI来源千问/元宝/百度/豆包)")

        # 检查是否有分数标注年份
        if parsed.score and "2025" not in parsed.raw_text and "2024" not in parsed.raw_text:
            issues.append("未明确数据年份建议标注2025年参考位次")

        return issues

    def _generate_suggestions(
        self,
        policy_errors: List[Dict],
        crowd_risks: List[CrowdRisk],
        data_issues: List[str],
    ) -> List[str]:
        """生成建议"""
        suggestions = []

        if crowd_risks:
            high_risks = [r for r in crowd_risks if r.risk_level == "high"]
            if high_risks:
                suggestions.append(
                    f"检测到 {len(high_risks)} 所高风险扎堆院校,"
                    f"建议替换为低风险替代方案"
                )

        if policy_errors:
            suggestions.append(
                f"存在 {len(policy_errors)} 个政策错误,"
                f"必须修正后才能使用该方案"
            )

        if data_issues:
            suggestions.append(
                "建议核实数据来源,确保使用的位次/分数数据真实可靠"
            )

        if not suggestions:
            suggestions.append("方案整体合理,建议结合自身情况微调")

        return suggestions

    def _calculate_score(
        self,
        policy_errors: List[Dict],
        crowd_risks: List[CrowdRisk],
        data_issues: List[str],
    ) -> int:
        """计算综合评分"""
        score = 100

        # 政策错误扣分
        score -= len(policy_errors) * 15

        # 扎堆风险扣分
        for risk in crowd_risks:
            if risk.risk_level == "high":
                score -= 10
            elif risk.risk_level == "medium":
                score -= 5

        # 数据问题扣分
        score -= len(data_issues) * 3

        return max(0, min(100, score))

Step 3: 运行测试

python3 -m pytest skills/gaokao-audit/tests/test_audit_service.py -v

Step 4: 提交

git add skills/gaokao-audit/scripts/audit_service.py
git add skills/gaokao-audit/tests/test_audit_service.py
git commit -m "feat(audit): 实现审核服务主类 - T1.6.1"

Task 1.7.1: 实现报告生成器

Objective: 实现 report_generator.py生成PDF报告

Files:

  • Create: skills/gaokao-audit/scripts/report_generator.py
  • Create: skills/gaokao-audit/tests/test_report_generator.py

Step 1: 写测试

"""报告生成器测试"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

import pytest
from skills.gaokao-audit.scripts.audit_service import AuditResult
from skills.gaokao-audit.scripts.crowd_detector import CrowdRisk
from skills.gaokao-audit.scripts.report_generator import ReportGenerator


@pytest.fixture
def generator():
    return ReportGenerator()


@pytest.fixture
def sample_result():
    return AuditResult(
        province="湖南",
        candidate_score=578,
        candidate_rank=26800,
        subjects="物理+化学+生物",
        source="百度",
        volunteers=[
            {"index": 1, "school": "长沙理工大学", "major": "会计学"},
        ],
        policy_errors=[],
        crowd_risks=[
            CrowdRisk(
                school="长沙理工大学",
                major="会计学",
                frequency=4,
                platforms=["千问", "元宝", "百度", "豆包"],
                predicted_increase=18,
                risk_level="high",
                alternatives=[
                    {"name": "湖南工商大学", "major": "会计学"},
                ],
            ),
        ],
        data_issues=["未明确数据年份"],
        suggestions=["检测到1所高风险扎堆院校"],
        overall_score=75,
    )


def test_generate_html(generator, sample_result):
    """测试生成HTML"""
    html = generator.render_html(sample_result)

    assert isinstance(html, str)
    assert "湖南" in html
    assert "长沙理工大学" in html
    assert "578" in html


def test_generate_pdf(tmp_path, generator, sample_result):
    """测试生成PDF"""
    output_path = tmp_path / "audit_report.pdf"

    pdf_path = generator.generate_pdf(
        sample_result,
        str(output_path),
    )

    assert os.path.exists(pdf_path)
    assert os.path.getsize(pdf_path) > 1000  # 至少1KB

Step 2: 实现 report_generator.py

"""
审核报告生成器

将审核结果格式化为HTML或PDF。
"""
import os
import sys
from datetime import datetime
from typing import Optional

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

from skills.gaokao-audit.scripts.audit_service import AuditResult


class ReportGenerator:
    """报告生成器"""

    TEMPLATE_PATH = os.path.join(
        os.path.dirname(__file__), '..', 'templates', 'audit_report.html'
    )

    def render_html(self, result: AuditResult) -> str:
        """渲染HTML报告"""
        # 简单模板渲染生产可使用jinja2
        with open(self.TEMPLATE_PATH, "r", encoding="utf-8") as f:
            template = f.read()

        # 准备数据
        data = {
            "source": result.source or "未指定",
            "candidate_info": f"{result.province} {result.candidate_score}{result.subjects or ''}",
            "audit_time": datetime.now().strftime("%Y-%m-%d %H:%M"),
            "overall_score": result.overall_score,
            "fatal_count": len(result.policy_errors),
            "warning_count": len(result.crowd_risks),
            "info_count": len(result.data_issues),
            "policy_errors": result.policy_errors,
            "crowd_risks": [
                {
                    "name": r.school,
                    "major": r.major,
                    "frequency": r.frequency,
                    "predicted_increase": r.predicted_increase,
                    "risk_level": r.risk_level,
                    "risk_level_label": r.risk_level_label,
                    "alternatives": r.alternatives,
                }
                for r in result.crowd_risks
            ],
            "data_issues": result.data_issues,
            "suggestions": result.suggestions,
        }

        # 简单模板替换生产用jinja2
        html = template
        for key, value in data.items():
            if isinstance(value, str):
                html = html.replace("{{ " + key + " }}", value)
            elif isinstance(value, int):
                html = html.replace("{{ " + key + " }}", str(value))

        return html

    def generate_pdf(
        self,
        result: AuditResult,
        output_path: str,
    ) -> str:
        """生成PDF报告

        Args:
            result: 审核结果
            output_path: 输出文件路径

        Returns:
            实际写入的PDF路径
        """
        try:
            from weasyprint import HTML

            html_content = self.render_html(result)

            # 确保输出目录存在
            os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)

            # 生成PDF
            HTML(string=html_content).write_pdf(output_path)

            return output_path
        except ImportError:
            # weasyprint 未安装
            html_path = output_path.replace(".pdf", ".html")
            html_content = self.render_html(result)
            with open(html_path, "w", encoding="utf-8") as f:
                f.write(html_content)
            return html_path

Step 3: 运行测试

pip3 install --user --break-system-packages weasyprint
python3 -m pytest skills/gaokao-audit/tests/test_report_generator.py -v

Step 4: 提交

git add skills/gaokao-audit/scripts/report_generator.py
git add skills/gaokao-audit/tests/test_report_generator.py
git commit -m "feat(audit): 实现报告生成器 - T1.7.1"

Task 1.8.1: 创建命令行入口

Objective: 创建 gaokao-audit CLI 脚本

Files:

  • Create: skills/gaokao-audit/scripts/audit_cli.py
  • Create: scripts/gaokao-audit (可执行)

Step 1: 实现 CLI

Create file: skills/gaokao-audit/scripts/audit_cli.py

"""
AI方案审核命令行工具
"""
import sys
import os
import argparse
import json

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))

from skills.gaokao-audit.scripts.audit_service import AuditService
from skills.gaokao-audit.scripts.report_generator import ReportGenerator


def main():
    parser = argparse.ArgumentParser(description="AI志愿方案审核工具")
    parser.add_argument("input", help="方案文件路径txt格式")
    parser.add_argument("-o", "--output", help="PDF输出路径")
    parser.add_argument("-f", "--format", default="text", choices=["text", "pdf_text"],
                        help="输入格式")
    parser.add_argument("--json", action="store_true", help="输出JSON结果")

    args = parser.parse_args()

    # 读取输入
    if not os.path.exists(args.input):
        print(f"❌ 文件不存在: {args.input}")
        sys.exit(1)

    with open(args.input, "r", encoding="utf-8") as f:
        plan_text = f.read()

    print(f"📄 读取文件: {args.input} ({len(plan_text)} 字符)")

    # 执行审核
    print("🔍 开始审核...")
    service = AuditService()
    result = service.audit(plan_text, format=args.format)

    # 输出结果
    if args.json:
        print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2))
    else:
        print(f"\n📊 审核结果:")
        print(f"  省份: {result.province}")
        print(f"  分数: {result.candidate_score}")
        print(f"  来源: {result.source or '未指定'}")
        print(f"  综合评分: {result.overall_score}/100")
        print(f"  致命错误: {len(result.policy_errors)} 个")
        print(f"  扎堆风险: {len(result.crowd_risks)} 个")

        if result.crowd_risks:
            print(f"\n🔴 扎堆风险院校:")
            for r in result.crowd_risks:
                print(f"  - {r.school} {r.major} "
                      f"({r.frequency}/4个AI推荐, 预测+{r.predicted_increase}分) "
                      f"[{r.risk_level_label}]")

    # 生成PDF
    if args.output or not args.json:
        output_path = args.output or f"audit_report_{os.path.basename(args.input)}.pdf"
        generator = ReportGenerator()
        result_path = generator.generate_pdf(result, output_path)
        print(f"\n📄 报告已生成: {result_path}")


if __name__ == "__main__":
    main()

Step 2: 创建可执行脚本

cd /home/long/project/gaokao-volunteer-system

# 创建可执行包装脚本
cat > scripts/gaokao-audit << 'EOF'
#!/bin/bash
# AI方案审核CLI wrapper
exec python3 /home/long/project/gaokao-volunteer-system/skills/gaokao-audit/scripts/audit_cli.py "$@"
EOF

chmod +x scripts/gaokao-audit

Step 3: 测试CLI

# 用之前的测试样本
./scripts/gaokao-audit skills/gaokao-audit/tests/fixtures/sample_xianyu.txt

Expected: 输出审核结果和PDF报告

Step 4: 提交

git add skills/gaokao-audit/scripts/audit_cli.py
git add scripts/gaokao-audit
git commit -m "feat(audit): 创建命令行CLI入口 - T1.8.1"

总结

完成清单

  • T1.4.1: 集成规范检查器
  • T1.5.1: 扎堆检测器
  • T1.6.1: 审核服务主类
  • T1.7.1: 报告生成器
  • T1.8.1: CLI入口

完整审核流程

用户上传方案 (text/pdf/screenshot)
    ↓
audit_cli.py
    ↓
AuditService.audit()
    ├─ PlanParser.parse_text()
    ├─ CheckerIntegration.check()
    ├─ CrowdDetector.detect_risks()
    └─ 计算评分 + 生成建议
    ↓
ReportGenerator.generate_pdf()
    ↓
PDF报告

产出文件

文件 说明
scripts/audit_cli.py CLI入口
scripts/audit_service.py 主服务
scripts/checker_integration.py 规范检查集成
scripts/crowd_detector.py 扎堆检测
scripts/plan_parser.py 方案解析
scripts/report_generator.py 报告生成
scripts/gaokao-audit 可执行命令
templates/audit_report.html 报告模板
tests/*.py 单元测试

验证

  • 所有单元测试通过
  • CLI命令可用
  • PDF生成成功
  • 端到端流程跑通

下一步: 启动服务承接大厂AI用户