from html import escape from pathlib import Path from openpyxl import Workbook from openpyxl.styles import Font, PatternFill from sqlalchemy import select from weasyprint import HTML from .config import get_settings from .models import (CriterionScore, ExportTask, Project, ScoreEvidence, ScoringBatch, Student, SubmissionFile) from .rules import totals def project_result(db, batch: ScoringBatch, project: Project): scores = db.scalars(select(CriterionScore).where(CriterionScore.batch_id == batch.id, CriterionScore.project_id == project.id)).all() by_id = {score.criterion_id: score for score in scores} criteria = batch.rubric_snapshot["criteria"] totals_ = totals({key: score.final_score for key, score in by_id.items()}, criteria, batch.rubric_snapshot["weights"]) rows = [] for criterion in criteria: score = by_id.get(criterion["criterion_id"]) evidence = db.scalars(select(ScoreEvidence).where(ScoreEvidence.score_id == score.id)).all() if score else [] rows.append({"criterion": criterion, "score_id": score.id if score else None, "model_score": score.model_score if score else None, "teacher_score": score.teacher_score if score else None, "final_score": score.final_score if score else None, "confidence": score.confidence if score else None, "model_comment": score.model_comment if score else "", "teacher_comment": score.teacher_comment if score else None, "missing_evidence": score.missing_evidence if score else [], "suggestion": score.suggestion if score else "", "status": score.status if score else "pending", "error": score.error if score else "", "evidence": [{"page": item.page, "quote": item.quote, "file_id": item.submission_file_id, "document": db.get(SubmissionFile, item.submission_file_id).relative_path if item.submission_file_id else "课程资料", "image_url": f"/api/assets/{item.submission_file_id}/{item.page}" if item.image_path else None} for item in evidence]}) student = db.get(Student, project.student_id) return {"project_id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic, "totals": totals_, "rows": rows, "facts": project.facts} def batch_projects(db, batch: ScoringBatch): if batch.project_ids is None: return db.scalars(select(Project).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)).all() if not batch.project_ids: return [] by_id = {project.id: project for project in db.scalars(select(Project).where(Project.id.in_(batch.project_ids))).all()} return [by_id[project_id] for project_id in batch.project_ids if project_id in by_id] def export_excel(db, batch_id: int): batch = db.get(ScoringBatch, batch_id) if not batch: raise ValueError("评分批次不存在") root = get_settings().exports_path root.mkdir(parents=True, exist_ok=True) task = ExportTask(batch_id=batch_id, kind="excel", status="running") db.add(task) db.flush() path = root / f"batch-{batch_id}-scores-{task.id}.xlsx" try: book = Workbook() sheet = book.active sheet.title = "成绩" final_criteria = [c for c in batch.rubric_snapshot["criteria"] if c["category"] == "final"] sheet.append(["学号", "姓名", "选题", "阶段报告1", "阶段报告2", "阶段报告3", *[c["name"] for c in final_criteria], "期末成绩", "总评", "状态", "批次", "规则版本"]) for project in batch_projects(db, batch): result = project_result(db, batch, project) scores = {row["criterion"]["criterion_id"]: row["final_score"] for row in result["rows"]} totals_ = result["totals"] sheet.append([result["student_no"], result["name"], result["topic"], *[totals_[key] for key in ("stage1", "stage2", "stage3")], *[scores.get(c["criterion_id"]) for c in final_criteria], totals_["final"], totals_["overall"], "完成" if totals_["overall"] is not None else "未完成/待人工处理", batch.id, batch.rubric_version_id]) sheet.freeze_panes = "D2" sheet.auto_filter.ref = sheet.dimensions for cell in sheet[1]: cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill("solid", fgColor="153C65") for column in sheet.columns: letter = column[0].column_letter sheet.column_dimensions[letter].width = min(50, max(12, max(len(str(cell.value or "")) for cell in column) + 2)) book.save(path) task.path, task.status = str(path), "completed" except Exception as exc: task.status, task.error = "failed", str(exc)[:1000] db.commit() raise db.commit() return task def _html_report(result, batch): esc = lambda value: escape(str(value or "")) rows = [] for row in result["rows"]: criterion = row["criterion"] evidences = "".join( f"
  • {esc(e['document'])} · 第{e['page']}{'张幻灯片' if str(e['document']).lower().endswith('.pptx') else '页'}:{esc(e['quote'])}
  • " for e in row["evidence"] ) missing = "、".join(row["missing_evidence"] or []) rows.append(f"

    {esc(criterion['name'])}({esc(row['final_score'])} / {criterion['max_score']})

    " f"

    模型原始分:{esc(row['model_score'])};教师修改分:{esc(row['teacher_score'])};状态:{esc(row['status'])}

    " f"

    模型评语:{esc(row['model_comment'])}

    教师评语:{esc(row['teacher_comment'])}

    " f"

    缺失证据:{esc(missing)}

    建议:{esc(row['suggestion'])}

    异常:{esc(row['error'])}

    " f"
    ") totals_ = result["totals"] return f"""

    WiseGrade 智评 · 学生反馈

    学号:{esc(result['student_no'])} 姓名:{esc(result['name'])} 选题:{esc(result['topic'])}

    评分批次:{batch.id} 课程版本:{batch.course_version_id} 规则版本:{batch.rubric_version_id}

    模型:{esc(batch.model_config_snapshot.get('model'))} 嵌入模型:{esc(batch.embedding_model_name)} ({esc(batch.embedding_model_version)})

    成绩

    阶段1:{esc(totals_['stage1'])} 阶段2:{esc(totals_['stage2'])} 阶段3:{esc(totals_['stage3'])} 期末:{esc(totals_['final'])} 总评:{esc(totals_['overall'])}

    {''.join(rows)}""" def export_project_pdf(db, batch_id: int, project_id: int): batch = db.get(ScoringBatch, batch_id) project = db.get(Project, project_id) if not batch or not project or project.course_version_id != batch.course_version_id or (batch.project_ids is not None and project_id not in batch.project_ids): raise ValueError("批次或项目不存在") root = get_settings().exports_path root.mkdir(parents=True, exist_ok=True) task = ExportTask(batch_id=batch_id, project_id=project_id, kind="pdf", status="running") db.add(task) db.flush() path = root / f"batch-{batch_id}-project-{project_id}-{task.id}.pdf" try: HTML(string=_html_report(project_result(db, batch, project), batch)).write_pdf(path) task.path, task.status = str(path), "completed" except Exception as exc: task.status, task.error = "failed", str(exc)[:1000] db.commit() raise db.commit() return task