185 lines
7.3 KiB
Python
185 lines
7.3 KiB
Python
"""《核验报告》HTML 组装。
|
||
|
||
- 每案例一份 `report-{case_id}.html`(三色结论 + 达标矩阵 + 补充材料清单)
|
||
- 批量核验另生成汇总页 `brief-{batch_no}.html`
|
||
写入 `{skill_data}/html/`,宿主数据管理侧栏可预览;重建走固定 Action id `render-html`。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from service.html_page_service import load_html_template, render_html_placeholders, write_html_page
|
||
from util.logging_config import get_skill_logger
|
||
|
||
|
||
def _log():
|
||
try:
|
||
return get_skill_logger()
|
||
except RuntimeError:
|
||
return None
|
||
|
||
_TEMPLATE_REL = "assets/html/verify-report.html"
|
||
|
||
_VERDICT_BADGE = {"安全": "tier-strong", "边缘": "tier-try", "硬伤": "tier-strong"}
|
||
_COLOR_BADGE = {"green": "tier-strong", "yellow": "tier-try", "red": "tag-red", "gray": "tier-not"}
|
||
_COLOR_MARK = {"green": "🟢 达标", "yellow": "🟡 边缘", "red": "🔴 硬伤", "gray": "⚪ 待核"}
|
||
|
||
|
||
def _esc(value: Any) -> str:
|
||
import html
|
||
|
||
return html.escape(str(value if value is not None else "—"), quote=True)
|
||
|
||
|
||
def _verdict_badge(verdict: str) -> str:
|
||
return {"安全": "tier-strong", "边缘": "tier-try", "硬伤": "tag-red"}.get(verdict, "tier-not")
|
||
|
||
|
||
def _matrix_rows(matrix: list[dict[str, Any]]) -> str:
|
||
rows = []
|
||
for item in matrix:
|
||
color = str(item.get("color") or "gray")
|
||
badge = _COLOR_BADGE.get(color, "tier-not")
|
||
required = item.get("required")
|
||
required_text = "—" if required is None else f"{required:g}"
|
||
margin = item.get("margin")
|
||
margin_text = "—" if margin is None else f"{margin * 100:.1f}%"
|
||
rows.append(
|
||
"<tr>"
|
||
f"<td>{_esc(item.get('metric'))}</td>"
|
||
f"<td class='nowrap'>{_esc(required_text)}</td>"
|
||
f"<td class='nowrap'>{_esc(item.get('actual'))}</td>"
|
||
f"<td class='nowrap'>{_esc(margin_text)}</td>"
|
||
f"<td class='nowrap'><span class='tag {badge}'>{_COLOR_MARK.get(color, color)}</span></td>"
|
||
f"<td class='muted'>{_esc(item.get('basis'))}</td>"
|
||
"</tr>"
|
||
)
|
||
if not rows:
|
||
rows.append('<tr><td colspan="6" class="muted">无</td></tr>')
|
||
return "".join(rows)
|
||
|
||
|
||
def _advice_list(matrix: list[dict[str, Any]]) -> str:
|
||
advices = [item.get("advice") for item in matrix if item.get("advice")]
|
||
if not advices:
|
||
return '<p class="muted">无补充事项。</p>'
|
||
items = "".join(f"<li>{_esc(a)}</li>" for a in advices)
|
||
return f"<ol class='reasons'>{items}</ol>"
|
||
|
||
|
||
def build_verify_html(result: dict[str, Any], batch_no: str = "") -> str:
|
||
verdict = str(result.get("verdict") or "")
|
||
mark = {"安全": "🟢 安全", "边缘": "🟡 边缘", "硬伤": "🔴 硬伤"}.get(verdict, verdict)
|
||
body = f"""
|
||
<div class="verdict-line">
|
||
<span class="tag {_verdict_badge(verdict)}">{mark}</span>
|
||
<span class="muted">{_esc(result.get("department") or "")} · 申报 {_esc(result.get("apply_rank") or "—")} · 批次 {_esc(batch_no or result.get("batch_no"))}</span>
|
||
</div>
|
||
<p class="para">{_esc(result.get("summary"))}</p>
|
||
<div class="section"><h2>硬指标达标矩阵</h2>
|
||
<table>
|
||
<tr><th>指标</th><th>最低要求</th><th>实有</th><th>余量</th><th>判定</th><th>依据</th></tr>
|
||
{_matrix_rows(result.get("matrix") or [])}
|
||
</table>
|
||
</div>
|
||
<div class="section"><h2>补充材料清单</h2>{_advice_list(result.get("matrix") or [])}</div>
|
||
<footer>核验仅针对硬指标形式审查,不构成评审结论;门槛以人事处最新文件为准,是否通过评审由评委会决定。</footer>
|
||
"""
|
||
template = load_html_template(_TEMPLATE_REL)
|
||
return render_html_placeholders(
|
||
template,
|
||
{
|
||
"TITLE": f"职称申报核验报告 · {result.get('teacher_name') or ''}",
|
||
"GENERATED_AT": "",
|
||
"BODY": body,
|
||
},
|
||
)
|
||
|
||
|
||
def write_report_for_kernel(*, result: dict[str, Any], batch_no: str) -> str:
|
||
html_text = build_verify_html(result, batch_no)
|
||
log = _log()
|
||
path = write_html_page(f"report-{result['case_id']}.html", html_text)
|
||
if log:
|
||
log.info("verify_report_written case_id=%s path=%s stage=report", result["case_id"], path)
|
||
return path
|
||
|
||
|
||
def build_report_for_case_id(case_id: int) -> str:
|
||
"""按案例 id 从库中取数并重写 HTML(重建入口用)。"""
|
||
from db.connection import get_conn
|
||
|
||
log = _log()
|
||
if log:
|
||
log.info("report_render_start case_id=%s stage=render", case_id)
|
||
conn = get_conn()
|
||
try:
|
||
cur = conn.cursor()
|
||
cur.execute("SELECT * FROM verify_cases WHERE id = ?", (case_id,))
|
||
row = cur.fetchone()
|
||
if not row:
|
||
raise ValueError(f"核验案例不存在:{case_id}")
|
||
columns = [d[0] for d in cur.description]
|
||
record = dict(zip(columns, row))
|
||
finally:
|
||
conn.close()
|
||
result = dict(record)
|
||
try:
|
||
result["matrix"] = json.loads(record.get("matrix_json") or "[]")
|
||
except (TypeError, ValueError):
|
||
result["matrix"] = []
|
||
html_text = build_verify_html(result)
|
||
path = write_html_page(f"report-{case_id}.html", html_text)
|
||
log.info("report_render_done case_id=%s path=%s stage=render", case_id, path)
|
||
return path
|
||
|
||
|
||
def build_batch_brief(*, batch_no: str, results: list[dict[str, Any]]) -> str:
|
||
rows = []
|
||
for result in results:
|
||
verdict = str(result.get("verdict") or "")
|
||
badge = _verdict_badge(verdict)
|
||
red_items = [m for m in result.get("matrix") or [] if m.get("color") == "red"]
|
||
red_text = "、".join(str(m.get("metric")) for m in red_items) if red_items else "—"
|
||
rows.append(
|
||
"<tr>"
|
||
f"<td>{_esc(result.get('teacher_name'))}</td>"
|
||
f"<td class='muted'>{_esc(result.get('department'))}</td>"
|
||
f"<td class='nowrap'>{_esc(result.get('apply_rank'))}</td>"
|
||
f"<td class='nowrap'><span class='tag {badge}'>{_esc(verdict)}</span></td>"
|
||
f"<td class='muted'>{_esc(red_text)}</td>"
|
||
"</tr>"
|
||
)
|
||
if not rows:
|
||
rows.append('<tr><td colspan="5" class="muted">本期无核验结果。</td></tr>')
|
||
|
||
counts: dict[str, int] = {}
|
||
for result in results:
|
||
counts[str(result.get("verdict"))] = counts.get(str(result.get("verdict")), 0) + 1
|
||
chips = " ".join(
|
||
f"<span class='tag {_verdict_badge(v)}'>{_esc(v)} × {n}</span>" for v, n in sorted(counts.items())
|
||
)
|
||
|
||
body = f"""
|
||
<div class="verdict-line">
|
||
{chips or '<span class="muted">无</span>'}
|
||
<span class="muted">批次 {_esc(batch_no)} · 案例 {_esc(len(results))} 个</span>
|
||
</div>
|
||
<div class="section"><h2>核验结论一览</h2>
|
||
<table><tr><th>教师</th><th>学院</th><th>申报级别</th><th>结论</th><th>硬伤项</th></tr>{''.join(rows)}</table>
|
||
</div>
|
||
<footer>汇总由批量核验自动生成,供师资科排序复核;核验不构成评审结论。</footer>
|
||
"""
|
||
template = load_html_template(_TEMPLATE_REL)
|
||
page = render_html_placeholders(
|
||
template,
|
||
{"TITLE": "职称核验汇总", "GENERATED_AT": "", "BODY": body},
|
||
)
|
||
path = write_html_page(f"brief-{batch_no}.html", page)
|
||
log = _log()
|
||
if log:
|
||
log.info("verify_brief_written batch_no=%s path=%s stage=report", batch_no, path)
|
||
return path
|