From 814da7ffc08a7a0e66dfcde70e84f9c68df8af88 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Wed, 9 Sep 2026 17:30:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=8A=A5=E5=91=8A=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E5=BD=92=E5=B9=B6=E4=B8=8E=E7=B1=BB=E5=9E=8B=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/reports.py | 49 +++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/app/services/reports.py b/app/services/reports.py index 8f46c6b..b6fdd0f 100644 --- a/app/services/reports.py +++ b/app/services/reports.py @@ -168,6 +168,7 @@ def aggregate_report_data(result: RiskAssessmentResult) -> dict[str, Any]: supplier_cards = [card for card in cards if card.type == "supplier"] material_cards = [card for card in cards if card.type == "material"] solution_cards = [card for card in cards if card.type == "solution"] + standard_cards = [card for card in cards if card.type in {"standard", "compliance"}] standards = _unique_by( [*result.standard_judgment, *(item for card in cards for item in card.standards)], lambda item: (item.category, item.risk_point, item.rule, item.status), @@ -182,10 +183,10 @@ def aggregate_report_data(result: RiskAssessmentResult) -> dict[str, Any]: item.solution, ), ) - suggestions = _unique_by( - [*result.ai_suggestions, *(item for card in cards for item in card.suggestions)], - lambda item: item, - ) + # Only base assessment suggestions belong to the prevention section. + # Card-specific suggestions stay with their owning section (supplier, + # material, standard/compliance or AI reasoning) to avoid duplication. + suggestions = _unique_by(list(result.ai_suggestions), lambda item: item) detected_values = [item for card in cards for item in card.detected_values] risks = [(card.title, risk) for card in solution_cards for risk in card.risks] analyses = [card for card in solution_cards if card.analysis_base or card.confidence or card.warning] @@ -194,6 +195,7 @@ def aggregate_report_data(result: RiskAssessmentResult) -> dict[str, Any]: "supplier_cards": supplier_cards, "material_cards": material_cards, "solution_cards": solution_cards, + "standard_cards": standard_cards, "standards": standards, "similar_cases": similar_cases, "suggestions": suggestions, @@ -252,6 +254,9 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: standards = report_data["standards"] similar_cases = report_data["similar_cases"] suggestions = report_data["suggestions"] + material_cards = report_data["material_cards"] + standard_cards = report_data["standard_cards"] + section_shift = 1 if material_cards else 0 story = [ Paragraph("新品风险预估报告", title), Paragraph(f"蝶雅产品部 · 风险预估系统 · 自动生成 · {generated_at}", subtitle), @@ -308,7 +313,7 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: order_text = f"合作 {supplier.cooperation_count} 次、" if supplier.cooperation_count is not None else "" conclusion = f"{round(score)} 分 · {order_text}异常 {supplier.anomaly_count or 0} 次" elif name == "标准符合性" and result.standard_judgment: - conclusion = f"{round(score)} 分 · 命中 {len(result.standard_judgment)} 条判定标准,待检测项需人工确认" + conclusion = f"{round(score)} 分 · 命中 {len(standards)} 条判定标准,待检测项需人工确认" else: conclusion = f"{round(score)} 分" + (f" · {reason}" if reason else "") rows.append([f"{name}({weight:g}%)", conclusion]) @@ -353,7 +358,6 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: supplier_rows.append([label, value, "追加供应商画像数据"]) story += [Paragraph("三、供应商合作频次分析", section), table(supplier_rows, [38 * mm, 35 * mm, 87 * mm])] - material_cards = report_data["material_cards"] if material_cards: story += [Paragraph("四、物料风险分析", section)] material_rows = [["物料/字段", "分析结果"]] @@ -370,7 +374,7 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: ) story.append(table(material_rows, [45 * mm, 115 * mm])) - story += [Paragraph(f"五、相似案例匹配({len(similar_cases)} 条)", section)] + story += [Paragraph(f"{'五' if section_shift else '四'}、相似案例匹配({len(similar_cases)} 条)", section)] if similar_cases: story.append( table( @@ -385,11 +389,11 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: else: story.append(Paragraph("暂无相关案例,RAG 相似案例检索未返回匹配结果。", small)) - story += [Paragraph("六、预防建议", section)] + story += [Paragraph(f"{'六' if section_shift else '五'}、预防建议", section)] for index, suggestion in enumerate(suggestions or ["优先针对低分维度安排专项验证,并明确责任人与完成节点。"], 1): story.append(Paragraph(f"{index}. {suggestion}", body)) story += [ - Paragraph("七、AI 推理方案(无历史案例场景)", section), + Paragraph(f"{'七' if section_shift else '六'}、AI 推理方案(无历史案例场景)", section), Paragraph( "本次评估已采用历史案例与标准库双引擎结论;若后续未匹配到足够历史案例,将基于相似品类、相似物料生成类比方案,并由标准库进行规则兜底。", body, @@ -406,15 +410,28 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: if card.warning: story.append(Paragraph(f"风险提示:{card.warning}", body)) - story += [Paragraph("八、标准判定结果", section)] - if standards: + story += [Paragraph(f"{'八' if section_shift else '七'}、标准判定结果", section)] + standard_rows = [ + [f"{item.category} · {item.risk_point}", item.rule, item.source or "标准文件.xlsx", item.status] + for item in standards + ] + for card in standard_cards: + if card.detected_values: + standard_rows.append( + [ + "检测值", + "、".join(f"{item.get('name', '')}:{item.get('value', '')}" for item in card.detected_values), + card.title, + "待确认", + ] + ) + if card.note: + standard_rows.append(["补充说明", card.note, card.title, "待确认"]) + if standard_rows: story.append( table( [["判定维度", "判定标准", "标准来源", "当前状态"]] - + [ - [f"{item.category} · {item.risk_point}", item.rule, item.source or "标准文件.xlsx", item.status] - for item in standards - ], + + standard_rows, [38 * mm, 55 * mm, 40 * mm, 27 * mm], ) ) @@ -525,7 +542,7 @@ def build_risk_report_docx(result: RiskAssessmentResult) -> bytes: order_text = f"合作 {supplier.cooperation_count} 次、" if supplier.cooperation_count is not None else "" conclusion = f"{round(score)} 分 · {order_text}异常 {supplier.anomaly_count or 0} 次" elif name == "标准符合性" and result.standard_judgment: - conclusion = f"{round(score)} 分 · 命中 {len(result.standard_judgment)} 条判定标准,待检测项需人工确认" + conclusion = f"{round(score)} 分 · 命中 {len(standards)} 条判定标准,待检测项需人工确认" else: conclusion = f"{round(score)} 分" + (f" · {reason}" if reason else "") rows.append([f"{name}({weight:g}%)", conclusion])