feat(t12): expand structured intake wizard and status summary
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

This commit is contained in:
Hermes Agent
2026-06-15 17:08:25 +08:00
parent b965dc2863
commit d24901b88e
2 changed files with 70 additions and 1 deletions

View File

@@ -510,6 +510,21 @@ def _build_portal_context(order: Order, settings: Settings) -> dict[str, Any]:
title, subtitle = _STAGE_META[stage] title, subtitle = _STAGE_META[stage]
latest_station_notice: dict[str, Any] | None = None latest_station_notice: dict[str, Any] | None = None
intake_summary = None
attachment_count = 0
if intake is not None:
attachment_count = len(list((intake.payload or {}).get("attachments") or []))
intake_summary = {
"candidate_score": intake.payload.get("candidate_score"),
"candidate_rank": intake.payload.get("candidate_rank"),
"candidate_subjects": intake.payload.get("candidate_subjects") or [],
"candidate_interests": intake.payload.get("candidate_interests"),
"target_cities": intake.payload.get("target_cities") or [],
"target_majors": intake.payload.get("target_majors") or [],
"university_preferences": intake.payload.get("university_preferences"),
"existing_plan_summary": intake.payload.get("existing_plan_summary"),
"guardian_notes": intake.payload.get("guardian_notes"),
}
if sent_station_events: if sent_station_events:
try: try:
latest_payload = json.loads(sent_station_events[-1].payload_json) latest_payload = json.loads(sent_station_events[-1].payload_json)
@@ -524,6 +539,8 @@ def _build_portal_context(order: Order, settings: Settings) -> dict[str, Any]:
"stage_subtitle": subtitle, "stage_subtitle": subtitle,
"payment": payment, "payment": payment,
"intake": intake, "intake": intake,
"intake_summary": intake_summary,
"attachment_count": attachment_count,
"report_html_ready": report_html_ready, "report_html_ready": report_html_ready,
"report_pdf_ready": report_pdf_ready, "report_pdf_ready": report_pdf_ready,
"order": order, "order": order,
@@ -848,6 +865,10 @@ def _render_info_page(
<label>位次<input name=\"candidate_rank\" value=\"{escape(str(payload.get("candidate_rank") or ""))}\" /></label><br/> <label>位次<input name=\"candidate_rank\" value=\"{escape(str(payload.get("candidate_rank") or ""))}\" /></label><br/>
<label>选科(逗号分隔)<input name=\"candidate_subjects\" value=\"{escape(",".join(payload.get("candidate_subjects") or []))}\" /></label><br/> <label>选科(逗号分隔)<input name=\"candidate_subjects\" value=\"{escape(",".join(payload.get("candidate_subjects") or []))}\" /></label><br/>
<label>兴趣方向<input name=\"candidate_interests\" value=\"{escape(str(payload.get("candidate_interests") or ""))}\" /></label><br/> <label>兴趣方向<input name=\"candidate_interests\" value=\"{escape(str(payload.get("candidate_interests") or ""))}\" /></label><br/>
<label>目标城市(逗号分隔)<input name=\"target_cities\" value=\"{escape(",".join(payload.get("target_cities") or []))}\" /></label><br/>
<label>目标专业(逗号分隔)<input name=\"target_majors\" value=\"{escape(",".join(payload.get("target_majors") or []))}\" /></label><br/>
<label>院校偏好说明<textarea name=\"university_preferences\">{escape(str(payload.get("university_preferences") or ""))}</textarea></label><br/>
<label>已有方案说明<textarea name=\"existing_plan_summary\">{escape(str(payload.get("existing_plan_summary") or ""))}</textarea></label><br/>
<label>家长备注<textarea name=\"guardian_notes\">{escape(str(payload.get("guardian_notes") or ""))}</textarea></label><br/> <label>家长备注<textarea name=\"guardian_notes\">{escape(str(payload.get("guardian_notes") or ""))}</textarea></label><br/>
<input type=\"hidden\" name=\"consent_version\" value=\"{escape(consent_version)}\" /> <input type=\"hidden\" name=\"consent_version\" value=\"{escape(consent_version)}\" />
<input type=\"hidden\" name=\"consent_scope\" value=\"{escape(consent_scope)}\" /> <input type=\"hidden\" name=\"consent_scope\" value=\"{escape(consent_scope)}\" />
@@ -872,12 +893,18 @@ def _render_info_page(
async function submitIntake(mode) {{ async function submitIntake(mode) {{
const form = new FormData(document.getElementById('intake-form')); const form = new FormData(document.getElementById('intake-form'));
const subjects = String(form.get('candidate_subjects') || '').split(',').map(s => s.trim()).filter(Boolean); const subjects = String(form.get('candidate_subjects') || '').split(',').map(s => s.trim()).filter(Boolean);
const targetCities = String(form.get('target_cities') || '').split(',').map(s => s.trim()).filter(Boolean);
const targetMajors = String(form.get('target_majors') || '').split(',').map(s => s.trim()).filter(Boolean);
const payload = {{ const payload = {{
mode, mode,
candidate_score: form.get('candidate_score') ? Number(form.get('candidate_score')) : null, candidate_score: form.get('candidate_score') ? Number(form.get('candidate_score')) : null,
candidate_rank: form.get('candidate_rank') ? Number(form.get('candidate_rank')) : null, candidate_rank: form.get('candidate_rank') ? Number(form.get('candidate_rank')) : null,
candidate_subjects: subjects, candidate_subjects: subjects,
candidate_interests: form.get('candidate_interests') || null, candidate_interests: form.get('candidate_interests') || null,
target_cities: targetCities,
target_majors: targetMajors,
university_preferences: form.get('university_preferences') || null,
existing_plan_summary: form.get('existing_plan_summary') || null,
guardian_notes: form.get('guardian_notes') || null, guardian_notes: form.get('guardian_notes') || null,
consent_version: form.get('consent_version') || null, consent_version: form.get('consent_version') || null,
consent_scope: form.get('consent_scope') || null, consent_scope: form.get('consent_scope') || null,
@@ -936,7 +963,13 @@ def _render_status_page(token: str, context: dict[str, Any]) -> str:
if isinstance(station_notice, dict): if isinstance(station_notice, dict):
title = escape(str(station_notice.get("title") or "站内通知")) title = escape(str(station_notice.get("title") or "站内通知"))
body = escape(str(station_notice.get("body") or "")) body = escape(str(station_notice.get("body") or ""))
sent_at = escape(str(station_notice.get("sent_at") or "")) sent_at = escape(
str(
station_notice.get("sent_at")
or station_notice.get("delivered_at")
or ""
)
)
station_notice_html = f""" station_notice_html = f"""
<section style=\"background:#eef6ff;border:1px solid #b9d7ff;border-radius:18px;padding:24px;\"> <section style=\"background:#eef6ff;border:1px solid #b9d7ff;border-radius:18px;padding:24px;\">
<h2>通知已发送</h2> <h2>通知已发送</h2>
@@ -944,6 +977,26 @@ def _render_status_page(token: str, context: dict[str, Any]) -> str:
<p>{body}</p> <p>{body}</p>
<p style=\"color:#4f6480;font-size:14px;\">发送时间:{sent_at}</p> <p style=\"color:#4f6480;font-size:14px;\">发送时间:{sent_at}</p>
</section>""" </section>"""
summary_html = ""
intake_summary = context.get("intake_summary") or {}
attachment_count = int(context.get("attachment_count") or 0)
if intake_summary:
summary_items = [
f"<li>分数:{escape(str(intake_summary.get('candidate_score') or '-'))}</li>",
f"<li>位次:{escape(str(intake_summary.get('candidate_rank') or '-'))}</li>",
f"<li>选科:{escape(','.join(intake_summary.get('candidate_subjects') or [])) or '-'}</li>",
f"<li>兴趣方向:{escape(str(intake_summary.get('candidate_interests') or '-'))}</li>",
f"<li>目标城市:{escape(','.join(intake_summary.get('target_cities') or [])) or '-'}</li>",
f"<li>目标专业:{escape(','.join(intake_summary.get('target_majors') or [])) or '-'}</li>",
f"<li>院校偏好:{escape(str(intake_summary.get('university_preferences') or '-'))}</li>",
f"<li>已有方案说明:{escape(str(intake_summary.get('existing_plan_summary') or '-'))}</li>",
f"<li>附件数量:{attachment_count}</li>",
]
summary_html = f"""
<section style=\"background:#fff;border:1px solid #dbe3f0;border-radius:18px;padding:24px;\">
<h2>当前资料摘要</h2>
<ul>{''.join(summary_items)}</ul>
</section>"""
return f"""<!doctype html> return f"""<!doctype html>
<html lang=\"zh-CN\"><head><meta charset=\"utf-8\" /><title>订单状态</title></head> <html lang=\"zh-CN\"><head><meta charset=\"utf-8\" /><title>订单状态</title></head>
<body style=\"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f7fb;margin:0;padding:32px 20px;color:#172033;\"> <body style=\"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f7fb;margin:0;padding:32px 20px;color:#172033;\">
@@ -955,6 +1008,7 @@ def _render_status_page(token: str, context: dict[str, Any]) -> str:
<p>服务版本:{escape(order.service_version)}</p> <p>服务版本:{escape(order.service_version)}</p>
<p>当前订单状态:{escape(order.status)}</p> <p>当前订单状态:{escape(order.status)}</p>
</section> </section>
{summary_html}
{station_notice_html} {station_notice_html}
<section style=\"background:#fff;border:1px solid #dbe3f0;border-radius:18px;padding:24px;\"> <section style=\"background:#fff;border:1px solid #dbe3f0;border-radius:18px;padding:24px;\">
<h2>下一步操作</h2> <h2>下一步操作</h2>

View File

@@ -47,6 +47,9 @@ def test_order_info_form_accepts_draft_and_submit(client, settings):
assert page.status_code == 200, page.text assert page.status_code == 200, page.text
assert "考生资料填写" in page.text assert "考生资料填写" in page.text
assert "监护人已知情并同意" in page.text assert "监护人已知情并同意" in page.text
assert "目标城市" in page.text
assert "目标专业" in page.text
assert "已有方案说明" in page.text
draft = client.post( draft = client.post(
f"/portal/{token}/info", f"/portal/{token}/info",
@@ -56,6 +59,10 @@ def test_order_info_form_accepts_draft_and_submit(client, settings):
"candidate_rank": 12034, "candidate_rank": 12034,
"candidate_subjects": ["物理", "化学", "生物"], "candidate_subjects": ["物理", "化学", "生物"],
"candidate_interests": "计算机", "candidate_interests": "计算机",
"target_cities": ["长沙", "上海"],
"target_majors": ["计算机科学与技术", "自动化"],
"university_preferences": "更偏向 985 / 211接受省内优先",
"existing_plan_summary": "已有一份千问方案,需要人工校验是否扎堆",
"guardian_notes": "更看重省内城市", "guardian_notes": "更看重省内城市",
"consent_version": "t12-web-mvp-v1", "consent_version": "t12-web-mvp-v1",
"consent_scope": "web-self-service-order-intake", "consent_scope": "web-self-service-order-intake",
@@ -76,6 +83,10 @@ def test_order_info_form_accepts_draft_and_submit(client, settings):
"candidate_rank": 12034, "candidate_rank": 12034,
"candidate_subjects": ["物理", "化学", "生物"], "candidate_subjects": ["物理", "化学", "生物"],
"candidate_interests": "计算机", "candidate_interests": "计算机",
"target_cities": ["长沙", "上海"],
"target_majors": ["计算机科学与技术", "自动化"],
"university_preferences": "更偏向 985 / 211接受省内优先",
"existing_plan_summary": "已有一份千问方案,需要人工校验是否扎堆",
"guardian_notes": "更看重省内城市", "guardian_notes": "更看重省内城市",
"consent_version": "t12-web-mvp-v1", "consent_version": "t12-web-mvp-v1",
"consent_scope": "web-self-service-order-intake", "consent_scope": "web-self-service-order-intake",
@@ -91,6 +102,10 @@ def test_order_info_form_accepts_draft_and_submit(client, settings):
status_page = client.get(f"/portal/{token}/status") status_page = client.get(f"/portal/{token}/status")
assert status_page.status_code == 200, status_page.text assert status_page.status_code == 200, status_page.text
assert "处理中" in status_page.text assert "处理中" in status_page.text
assert "当前资料摘要" in status_page.text
assert "长沙,上海" in status_page.text
assert "计算机科学与技术,自动化" in status_page.text
assert "已有一份千问方案" in status_page.text
def test_order_info_form_becomes_read_only_after_report_ready( def test_order_info_form_becomes_read_only_after_report_ready(