feat(ai): 数据脱敏服务 + Prompt 模板渲染引擎

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-25 13:55:40 +08:00
parent 06f3d08c61
commit e0e4a7f9a1
3 changed files with 94 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
pub mod dto;
pub mod entity;
pub mod error;
pub mod prompt;
pub mod provider;
pub mod sanitization;
pub use error::{AiError, AiResult};

View File

@@ -0,0 +1,25 @@
use handlebars::Handlebars;
use serde_json::Value;
use crate::error::{AiError, AiResult};
/// Prompt 模板渲染引擎
pub struct PromptRenderer {
registry: Handlebars<'static>,
}
impl PromptRenderer {
pub fn new() -> Self {
let mut registry = Handlebars::new();
registry.set_strict_mode(true);
Self { registry }
}
/// 渲染 Prompt 模板 — 使用 Handlebars {{variable}} 语法
/// JSON 序列化注入,不做字符串拼接,防止 Prompt 注入
pub fn render(&self, template: &str, data: &Value) -> AiResult<String> {
self.registry
.render_template(template, data)
.map_err(|e| AiError::TemplateError(format!("模板渲染失败: {e}")))
}
}

View File

@@ -0,0 +1,67 @@
use erp_core::health_provider::{
HealthReportDto, LabReportDto, PatientSummaryDto, VitalSignDto,
};
use serde_json::Value;
use crate::error::{AiError, AiResult};
/// 数据脱敏服务 — 确保发送给 AI 的数据不含 PII
/// HealthDataProvider 返回的 DTO 已经是脱敏的(只有年龄/性别/医疗数据)
/// 此服务做二次检查和安全约束注入
pub struct SanitizationService;
impl SanitizationService {
pub fn new() -> Self {
Self
}
pub fn sanitize_lab_report(&self, report: &LabReportDto) -> AiResult<Value> {
let sanitized = serde_json::to_value(report)
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
self.verify_no_pii(&sanitized)?;
Ok(sanitized)
}
pub fn sanitize_vital_signs(&self, signs: &[VitalSignDto]) -> AiResult<Value> {
let sanitized = serde_json::to_value(signs)
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
self.verify_no_pii(&sanitized)?;
Ok(sanitized)
}
pub fn sanitize_patient_summary(&self, summary: &PatientSummaryDto) -> AiResult<Value> {
let sanitized = serde_json::to_value(summary)
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
self.verify_no_pii(&sanitized)?;
Ok(sanitized)
}
pub fn sanitize_health_report(&self, report: &HealthReportDto) -> AiResult<Value> {
let sanitized = serde_json::to_value(report)
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
self.verify_no_pii(&sanitized)?;
Ok(sanitized)
}
/// 二次验证: 确保没有意外泄漏的 PII
fn verify_no_pii(&self, value: &Value) -> AiResult<()> {
let pii_keys = [
"name",
"phone",
"id_number",
"address",
"birth_date",
"email",
];
if let Value::Object(map) = value {
for key in pii_keys {
if map.contains_key(key) {
return Err(AiError::SanitizationError(format!(
"检测到疑似 PII 字段: {key}"
)));
}
}
}
Ok(())
}
}