141 lines
5.8 KiB
Python
141 lines
5.8 KiB
Python
"""Deterministic Markdown reports with escaped untrusted text and explicit data boundaries."""
|
||
|
||
import html
|
||
from datetime import date
|
||
|
||
from app.schemas.integration_risk import IntegrationRiskRequest
|
||
from app.schemas.risk_prediction import RiskAssessmentResult
|
||
from app.services.database_risk_scoring import normalize_category, normalize_material
|
||
from app.services.purchase_orders import calendar_year_window
|
||
from app.services.reports import LEVEL_LABELS
|
||
|
||
|
||
def md_text(value: object) -> str:
|
||
if value is None or value == "":
|
||
return "未提供"
|
||
value = html.escape(str(value), quote=False).replace("\r", " ").replace("\n", " ")
|
||
for char in ("\\", "`", "*", "_", "[", "]", "|", "#", "!"):
|
||
value = value.replace(char, "\\" + char)
|
||
return value
|
||
|
||
|
||
def render_markdown_report(
|
||
payload: IntegrationRiskRequest,
|
||
result: RiskAssessmentResult,
|
||
as_of: date,
|
||
operator_name: str,
|
||
) -> str:
|
||
product = payload.assessment
|
||
start, _ = calendar_year_window(as_of, 3)
|
||
info = (
|
||
("评估对象", product.product_name or product.subject_id),
|
||
("对象编号", product.subject_id),
|
||
("请求编号", payload.request_id),
|
||
("提交品类", product.product_category),
|
||
("评分品类", normalize_category(product.product_category) or "无法归类,品类维度使用默认 50 分"),
|
||
("规格", product.specification),
|
||
("包装原文", product.material_type),
|
||
("评分物料标签", normalize_material(product.material_type)),
|
||
("供应商", product.supplier),
|
||
("加工厂", product.factory),
|
||
("开发阶段", product.stage_new),
|
||
("已知缺陷等级", product.defect_level),
|
||
("评估人", operator_name),
|
||
("数据截止日期", as_of),
|
||
("评估背景", product.description),
|
||
)
|
||
lines = ["# 新品风险评估报告", "", "## 一、评估信息", "", "| 项目 | 内容 |", "|---|---|"]
|
||
lines.extend(f"| {label} | {md_text(value)} |" for label, value in info)
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## 二、综合风险结论",
|
||
"",
|
||
f"- 综合安全得分:{result.score:.2f}(越高风险越低)",
|
||
f"- 风险等级:{LEVEL_LABELS[result.level.value]}",
|
||
f"- 模型版本:{md_text(result.model_version)}",
|
||
]
|
||
)
|
||
if result.alert_text:
|
||
lines.append(f"- 数据与建议提示:{md_text(result.alert_text)}")
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## 三、七维风险评分",
|
||
"",
|
||
"| 风险维度 | 得分 | 权重 | 数据状态 | 说明 |",
|
||
"|---|---:|---:|---|---|",
|
||
]
|
||
)
|
||
lines.extend(
|
||
f"| {md_text(item.dimension)} | {item.score:.2f} | {item.weight:g}% | "
|
||
f"{md_text(item.status)} | {md_text(item.reason)} |"
|
||
for item in result.dimension_scores
|
||
)
|
||
lines.extend(["", "## 四、历史统计与相似异常案例", ""])
|
||
if result.category_warning:
|
||
lines.append(f"- 评分品类历史异常:{result.category_warning.anomaly_count} 条。")
|
||
if result.supplier_freq:
|
||
freq = result.supplier_freq
|
||
lines.extend(
|
||
[
|
||
f"- 供应商:{md_text(freq.supplier_name)};合作次数:{md_text(freq.cooperation_count)};"
|
||
f"历史异常:{md_text(freq.anomaly_count)}。",
|
||
f"- 供应商统计说明:{md_text(freq.note)}",
|
||
]
|
||
)
|
||
lines.extend(["", "| 日期 | 产品 | 异常现象 | 原因 | 历史处理方案 | 相似度 |", "|---|---|---|---|---|---:|"])
|
||
for case in result.similar_cases:
|
||
similarity = f"{case.similarity:.1%}" if case.similarity is not None else "未提供"
|
||
lines.append(
|
||
"| "
|
||
+ " | ".join(
|
||
md_text(value)
|
||
for value in (
|
||
case.date,
|
||
case.product,
|
||
case.problem or case.description,
|
||
case.root_cause,
|
||
case.solution,
|
||
similarity,
|
||
)
|
||
)
|
||
+ " |"
|
||
)
|
||
if not result.similar_cases:
|
||
lines.extend(["", "暂无匹配的历史案例,不能据此认定产品无风险。"])
|
||
lines.extend(["", "## 五、标准规则", "", "| 风险点 | 判定规则 | 来源 | 状态 |", "|---|---|---|---|"])
|
||
lines.extend(
|
||
"| "
|
||
+ " | ".join(
|
||
md_text(value)
|
||
for value in (
|
||
rule.risk_point,
|
||
rule.rule,
|
||
rule.source,
|
||
rule.status,
|
||
)
|
||
)
|
||
+ " |"
|
||
for rule in result.standard_judgment
|
||
)
|
||
if not result.standard_judgment:
|
||
lines.extend(["", "暂无匹配标准,需由品控补充核验;不代表已通过合规或检测判定。"])
|
||
lines.extend(["", "## 六、风险预防建议", ""])
|
||
lines.extend(f"{index}. {md_text(value)}" for index, value in enumerate(result.ai_suggestions, 1))
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## 七、数据口径说明",
|
||
"",
|
||
f"- 数据来自风险平台本地异常案例、采购订单及启用的标准规则;统计区间为 {start} 至 {as_of}。",
|
||
"- 沿用现有评分口径,以截止年份往前 3 年的 1 月 1 日为区间起点。",
|
||
"- 包装组合沿用现有关键词归一化规则,只使用一个匹配物料标签进行评分。",
|
||
"- 缺少巡检、检测结果或可用历史记录的维度按现有默认分处理,并在明细中标注。",
|
||
"- 缺陷维度基于历史 A/B/C 记录计算,不预测新品实际缺陷等级。",
|
||
"- 标准匹配和预防建议不替代实际检测与品控审核;最终放行由业务和品控人员确认。",
|
||
"",
|
||
]
|
||
)
|
||
return "\n".join(lines)
|