feat: add versioned backend scoring workflow

This commit is contained in:
2026-09-25 11:11:18 +08:00
parent 146d5566cf
commit eece55d08d
20 changed files with 2063 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
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 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 db.scalars(select(Project).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)):
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"<li>{esc(e['document'])} · 第{e['page']}页/张:{esc(e['quote'])}</li>" for e in row["evidence"])
missing = "、".join(row["missing_evidence"] or [])
rows.append(f"<section><h3>{esc(criterion['name'])}({esc(row['final_score'])} / {criterion['max_score']})</h3>"
f"<p>模型原始分:{esc(row['model_score'])};教师修改分:{esc(row['teacher_score'])};状态:{esc(row['status'])}</p>"
f"<p>模型评语:{esc(row['model_comment'])}</p><p>教师评语:{esc(row['teacher_comment'])}</p>"
f"<p>缺失证据:{esc(missing)}</p><p>建议:{esc(row['suggestion'])}</p><p>异常:{esc(row['error'])}</p>"
f"<ul>{evidences}</ul></section>")
totals_ = result["totals"]
return f"""<!doctype html><html lang="zh"><meta charset="utf-8"><style>
body {{ font-family: 'Noto Sans CJK SC', sans-serif; font-size: 11px; color: #172b4d; }}
h1 {{ color: #153c65; }} section {{ border-bottom: 1px solid #ccd7e0; break-inside: avoid; }}
h3 {{ margin-bottom: 3px; }} p {{ margin: 3px 0; }} li {{ margin: 3px 0; }}
</style><body><h1>WiseGrade 智评 · 学生反馈</h1>
<p>学号:{esc(result['student_no'])} 姓名:{esc(result['name'])} 选题:{esc(result['topic'])}</p>
<p>评分批次:{batch.id} 课程版本:{batch.course_version_id} 规则版本:{batch.rubric_version_id}</p>
<p>模型:{esc(batch.model_config_snapshot.get('model'))} 嵌入模型:{esc(batch.embedding_model_name)} ({esc(batch.embedding_model_version)})</p>
<h2>成绩</h2><p>阶段1:{esc(totals_['stage1'])} 阶段2:{esc(totals_['stage2'])} 阶段3:{esc(totals_['stage3'])} 期末:{esc(totals_['final'])} 总评:{esc(totals_['overall'])}</p>
{''.join(rows)}</body></html>"""
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:
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