feat: 职称评审核验助手首版 v1.0.1(verify-promotion-metric-default)
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
184
scripts/service/report_service.py
Normal file
184
scripts/service/report_service.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""《核验报告》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
|
||||
Reference in New Issue
Block a user