feat(health+core+ai): 业务流程全面修复 Phase 4-6 + 集成测试修复
Phase 4 — Dead-letter 重试 + 内容推送 + 安全加固: - erp-core: retry_dead_letters() 定时重试 + PII payload 脱敏 - erp-core: audit_service 哈希链定时验证 + 写入失败告警 - erp-health: article.published 消费者匹配 patient_tag 推送消息 - erp-health: care_plan 事件消费者 (激活通知 + 完成积分) Phase 5 — 患者批量操作 + 咨询增强 + 护理事件: - patient: batch_import_patients + bind_by_phone + refer_patient - consultation: rate_session 满意度评价 (rating + feedback) - consent: patient_sign_consent 患者端签署 - validation: source 枚举 (7值) + relationship 枚举 (7值) + 12 单元测试 Phase 6 — 咨询文件上传 + AI 引用标注: - consultation_message: media_id 附件上传端点 - ai_suggestion: references JSONB + [ref:id] 格式引用标注 - AI system prompt 增加引用指令 + output_parser 提取逻辑 迁移: 000161 (media_id + references) + 000162 (rating + feedback) 集成测试: consultation/follow_up/pii_encryption 新字段同步修复 讨论文档: 2026-05-20-business-process-brainstorm.md (10域审核报告)
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
use crate::audit::AuditLog;
|
||||
use crate::entity::audit_log;
|
||||
use crate::request_info::RequestInfo;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 持久化审计日志到 audit_logs 表。
|
||||
///
|
||||
@@ -39,6 +43,12 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
||||
// 计算当前记录的 record_hash
|
||||
let record_hash = compute_record_hash(&log, prev_hash.as_deref());
|
||||
|
||||
// 保存日志字段用于错误日志(model 构建会 move String 字段)
|
||||
let err_tenant_id = log.tenant_id;
|
||||
let err_action = log.action.clone();
|
||||
let err_resource_type = log.resource_type.clone();
|
||||
let err_resource_id = log.resource_id;
|
||||
|
||||
let model = audit_log::ActiveModel {
|
||||
id: Set(log.id),
|
||||
tenant_id: Set(log.tenant_id),
|
||||
@@ -56,7 +66,14 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
||||
};
|
||||
|
||||
if let Err(e) = model.insert(db).await {
|
||||
tracing::warn!(error = %e, "审计日志写入失败");
|
||||
tracing::error!(
|
||||
error = %e,
|
||||
tenant_id = ?err_tenant_id,
|
||||
action = %err_action,
|
||||
resource_type = %err_resource_type,
|
||||
resource_id = ?err_resource_id,
|
||||
"审计日志写入失败 — 数据完整性风险"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +148,74 @@ pub async fn verify_hash_chain(
|
||||
|
||||
Ok((total, broken))
|
||||
}
|
||||
|
||||
/// 哈希链验证结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChainVerificationResult {
|
||||
pub total: usize,
|
||||
pub passed: usize,
|
||||
pub failed: usize,
|
||||
pub failed_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
/// 验证最近 N 条审计记录的哈希链完整性。
|
||||
pub async fn verify_recent_chain(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
limit: u64,
|
||||
) -> Result<ChainVerificationResult, String> {
|
||||
let records = audit_log::Entity::find()
|
||||
.filter(audit_log::Column::TenantId.eq(tenant_id))
|
||||
.filter(audit_log::Column::RecordHash.is_not_null())
|
||||
.order_by_desc(audit_log::Column::CreatedAt)
|
||||
.limit(limit)
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| format!("查询审计日志失败: {}", e))?;
|
||||
|
||||
let mut records = records;
|
||||
records.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
||||
|
||||
let total = records.len();
|
||||
let mut passed = 0;
|
||||
let mut failed_ids = Vec::new();
|
||||
let mut prev: Option<String> = None;
|
||||
|
||||
for record in &records {
|
||||
let mut record_broken = false;
|
||||
if prev.as_deref() != record.prev_hash.as_deref() {
|
||||
record_broken = true;
|
||||
}
|
||||
let log = AuditLog {
|
||||
id: record.id,
|
||||
tenant_id: record.tenant_id,
|
||||
user_id: record.user_id,
|
||||
action: record.action.clone(),
|
||||
resource_type: record.resource_type.clone(),
|
||||
resource_id: record.resource_id,
|
||||
old_value: record.old_value.clone(),
|
||||
new_value: record.new_value.clone(),
|
||||
ip_address: record.ip_address.clone(),
|
||||
user_agent: record.user_agent.clone(),
|
||||
created_at: record.created_at,
|
||||
};
|
||||
let expected = compute_record_hash(&log, record.prev_hash.as_deref());
|
||||
if Some(expected.as_str()) != record.record_hash.as_deref() {
|
||||
record_broken = true;
|
||||
}
|
||||
if record_broken {
|
||||
failed_ids.push(record.id);
|
||||
} else {
|
||||
passed += 1;
|
||||
}
|
||||
prev = record.record_hash.clone();
|
||||
}
|
||||
|
||||
let failed = total - passed;
|
||||
Ok(ChainVerificationResult {
|
||||
total,
|
||||
passed,
|
||||
failed,
|
||||
failed_ids,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user