fix(core): 跨 crate 小修复 — dto 合并、tracing 补全、死代码清理
- erp-ai: 删除孤立 dto.rs(已合并到子模块) - erp-core: audit_service tracing 优化 - erp-health: points_handler 补充返回值、alert_engine 修正日志级别 - erp-plugin: host/data_handler/market_handler tracing 统一 - erp-dialysis/event: 移除无用 import - erp-workflow/executor: tracing 格式统一
This commit is contained in:
@@ -1,217 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
// === 分析请求 ===
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AnalyzeRequest {
|
|
||||||
pub analysis_type: AnalysisType,
|
|
||||||
pub source_ref: String,
|
|
||||||
pub options: AnalyzeOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum AnalysisType {
|
|
||||||
LabReport,
|
|
||||||
Trends,
|
|
||||||
CheckupPlan,
|
|
||||||
ReportSummary,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnalysisType {
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::LabReport => "lab_report",
|
|
||||||
Self::Trends => "trend",
|
|
||||||
Self::CheckupPlan => "checkup_plan",
|
|
||||||
Self::ReportSummary => "report_summary",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prompt_name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::LabReport => "lab_report_interpretation",
|
|
||||||
Self::Trends => "health_trend_analysis",
|
|
||||||
Self::CheckupPlan => "personalized_checkup_plan",
|
|
||||||
Self::ReportSummary => "report_summary_generation",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AnalyzeOptions {
|
|
||||||
pub detail_level: Option<String>,
|
|
||||||
pub language: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AnalyzeOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
detail_level: Some("patient_friendly".into()),
|
|
||||||
language: Some("zh-CN".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === AI Provider 请求/响应 ===
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GenerateRequest {
|
|
||||||
pub system_prompt: String,
|
|
||||||
pub user_prompt: String,
|
|
||||||
pub model: String,
|
|
||||||
pub temperature: f32,
|
|
||||||
pub max_tokens: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GenerateResponse {
|
|
||||||
pub content: String,
|
|
||||||
pub model: String,
|
|
||||||
pub input_tokens: u32,
|
|
||||||
pub output_tokens: u32,
|
|
||||||
pub duration_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// === SSE 事件 ===
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TokenUsage {
|
|
||||||
pub input: u32,
|
|
||||||
pub output: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum AnalysisSseEvent {
|
|
||||||
#[serde(rename = "chunk")]
|
|
||||||
Chunk { content: String, index: u32 },
|
|
||||||
#[serde(rename = "metadata")]
|
|
||||||
Metadata {
|
|
||||||
model: String,
|
|
||||||
tokens: TokenUsage,
|
|
||||||
duration_ms: u64,
|
|
||||||
},
|
|
||||||
#[serde(rename = "done")]
|
|
||||||
Done {
|
|
||||||
analysis_id: uuid::Uuid,
|
|
||||||
status: String,
|
|
||||||
},
|
|
||||||
#[serde(rename = "error")]
|
|
||||||
Error { message: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// ---- AnalysisType::as_str ----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn analysis_type_as_str() {
|
|
||||||
assert_eq!(AnalysisType::LabReport.as_str(), "lab_report");
|
|
||||||
assert_eq!(AnalysisType::Trends.as_str(), "trend");
|
|
||||||
assert_eq!(AnalysisType::CheckupPlan.as_str(), "checkup_plan");
|
|
||||||
assert_eq!(AnalysisType::ReportSummary.as_str(), "report_summary");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- AnalysisType::prompt_name ----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn analysis_type_prompt_name() {
|
|
||||||
assert_eq!(AnalysisType::LabReport.prompt_name(), "lab_report_interpretation");
|
|
||||||
assert_eq!(AnalysisType::Trends.prompt_name(), "health_trend_analysis");
|
|
||||||
assert_eq!(AnalysisType::CheckupPlan.prompt_name(), "personalized_checkup_plan");
|
|
||||||
assert_eq!(AnalysisType::ReportSummary.prompt_name(), "report_summary_generation");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- AnalysisType serde round-trip ----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn analysis_type_serde_roundtrip() {
|
|
||||||
let types = vec![
|
|
||||||
AnalysisType::LabReport,
|
|
||||||
AnalysisType::Trends,
|
|
||||||
AnalysisType::CheckupPlan,
|
|
||||||
AnalysisType::ReportSummary,
|
|
||||||
];
|
|
||||||
for t in types {
|
|
||||||
let json = serde_json::to_string(&t).unwrap();
|
|
||||||
let back: AnalysisType = serde_json::from_str(&json).unwrap();
|
|
||||||
assert_eq!(t, back);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn analysis_type_deserialize_snake_case() {
|
|
||||||
let t: AnalysisType = serde_json::from_str("\"lab_report\"").unwrap();
|
|
||||||
assert_eq!(t, AnalysisType::LabReport);
|
|
||||||
|
|
||||||
let t: AnalysisType = serde_json::from_str("\"trends\"").unwrap();
|
|
||||||
assert_eq!(t, AnalysisType::Trends);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- AnalyzeOptions::default ----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn analyze_options_default() {
|
|
||||||
let opts = AnalyzeOptions::default();
|
|
||||||
assert_eq!(opts.detail_level, Some("patient_friendly".to_string()));
|
|
||||||
assert_eq!(opts.language, Some("zh-CN".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- AnalysisSseEvent serde round-trip ----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sse_event_chunk_roundtrip() {
|
|
||||||
let event = AnalysisSseEvent::Chunk {
|
|
||||||
content: "血红蛋白偏低".to_string(),
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(json.contains("\"type\":\"chunk\""));
|
|
||||||
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
|
|
||||||
match back {
|
|
||||||
AnalysisSseEvent::Chunk { content, index } => {
|
|
||||||
assert_eq!(content, "血红蛋白偏低");
|
|
||||||
assert_eq!(index, 0);
|
|
||||||
}
|
|
||||||
_ => panic!("期望 Chunk 变体"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sse_event_done_roundtrip() {
|
|
||||||
let id = {
|
|
||||||
let ts = uuid::Timestamp::now(uuid::NoContext);
|
|
||||||
uuid::Uuid::new_v7(ts)
|
|
||||||
};
|
|
||||||
let event = AnalysisSseEvent::Done {
|
|
||||||
analysis_id: id,
|
|
||||||
status: "completed".to_string(),
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
|
|
||||||
match back {
|
|
||||||
AnalysisSseEvent::Done { analysis_id, status } => {
|
|
||||||
assert_eq!(analysis_id, id);
|
|
||||||
assert_eq!(status, "completed");
|
|
||||||
}
|
|
||||||
_ => panic!("期望 Done 变体"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sse_event_error_roundtrip() {
|
|
||||||
let event = AnalysisSseEvent::Error {
|
|
||||||
message: "超时".to_string(),
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(json.contains("\"type\":\"error\""));
|
|
||||||
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
|
|
||||||
match back {
|
|
||||||
AnalysisSseEvent::Error { message } => assert_eq!(message, "超时"),
|
|
||||||
_ => panic!("期望 Error 变体"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use erp_core::health_provider::{HealthDataProvider, TimeRange};
|
use erp_core::health_provider::{HealthDataProvider, TimeRange};
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, Statement};
|
use sea_orm::{EntityTrait, FromQueryResult, Statement};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::dto::AnalysisType;
|
use crate::dto::AnalysisType;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::audit::AuditLog;
|
use crate::audit::AuditLog;
|
||||||
use crate::entity::audit_log;
|
use crate::entity::audit_log;
|
||||||
use crate::request_info::RequestInfo;
|
use crate::request_info::RequestInfo;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||||
use sha2::{Sha256, Digest};
|
use sha2::{Sha256, Digest};
|
||||||
use tracing;
|
use tracing;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use erp_core::events::EventBus;
|
|
||||||
|
|
||||||
/// 预留事件处理器注册
|
/// 预留事件处理器注册
|
||||||
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {
|
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
|
|
||||||
use crate::service::device_reading_service;
|
use crate::service::device_reading_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|||||||
@@ -452,6 +452,39 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
|||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 管理端:按 patient_id 查询积分账户 + 流水
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn admin_get_patient_account<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path(patient_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
|
||||||
|
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.points.list")?;
|
||||||
|
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn admin_list_patient_transactions<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path(patient_id): Path<Uuid>,
|
||||||
|
Query(params): Query<PaginationParams>,
|
||||||
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
|
||||||
|
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.points.list")?;
|
||||||
|
let page = params.page.unwrap_or(1);
|
||||||
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
|
let result = points_service::list_transactions(
|
||||||
|
&state, ctx.tenant_id, patient_id, page, page_size,
|
||||||
|
).await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 辅助:通过 user_id 解析 patient_id
|
// 辅助:通过 user_id 解析 patient_id
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
use sea_orm::{ActiveValue::Set, QueryOrder};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::entity::{alert_rules, alerts, device_readings, vital_signs_hourly};
|
use crate::entity::{alert_rules, alerts, device_readings, vital_signs_hourly};
|
||||||
use crate::error::{HealthError, HealthResult};
|
use crate::error::HealthResult;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
/// 评估所有适用规则,返回触发的告警列表
|
/// 评估所有适用规则,返回触发的告警列表
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub async fn list_reminders(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HealthError::PatientNotFound)?;
|
.ok_or(HealthError::PatientNotFound)?;
|
||||||
|
|
||||||
let mut query = medication_reminder::Entity::find()
|
let query = medication_reminder::Entity::find()
|
||||||
.filter(medication_reminder::Column::TenantId.eq(tenant_id))
|
.filter(medication_reminder::Column::TenantId.eq(tenant_id))
|
||||||
.filter(medication_reminder::Column::PatientId.eq(patient_id))
|
.filter(medication_reminder::Column::PatientId.eq(patient_id))
|
||||||
.filter(medication_reminder::Column::DeletedAt.is_null());
|
.filter(medication_reminder::Column::DeletedAt.is_null());
|
||||||
|
|||||||
@@ -1061,7 +1061,7 @@ where
|
|||||||
pub async fn delete_user_view<S>(
|
pub async fn delete_user_view<S>(
|
||||||
State(state): State<PluginState>,
|
State(state): State<PluginState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path((plugin_id, entity, view_id)): Path<(Uuid, String, Uuid)>,
|
Path((_plugin_id, _entity, view_id)): Path<(Uuid, String, Uuid)>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where
|
where
|
||||||
PluginState: FromRef<S>,
|
PluginState: FromRef<S>,
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ where
|
|||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
let plugin_id = plugin_resp.id;
|
let plugin_id = plugin_resp.id;
|
||||||
let plugin_resp = crate::service::PluginService::install(
|
let _plugin_resp = crate::service::PluginService::install(
|
||||||
plugin_id,
|
plugin_id,
|
||||||
ctx.tenant_id,
|
ctx.tenant_id,
|
||||||
ctx.user_id,
|
ctx.user_id,
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ impl host_api::Host for HostState {
|
|||||||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity));
|
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = self.db.clone().unwrap();
|
let db = self.db.clone().ok_or("数据库连接不可用")?;
|
||||||
let event_bus = self.event_bus.clone()
|
let event_bus = self.event_bus.clone()
|
||||||
.ok_or("事件总线不可用")?;
|
.ok_or("事件总线不可用")?;
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ impl host_api::Host for HostState {
|
|||||||
let db = self.db.clone()
|
let db = self.db.clone()
|
||||||
.ok_or("编号生成需要数据库连接")?;
|
.ok_or("编号生成需要数据库连接")?;
|
||||||
|
|
||||||
let tenant_id = self.tenant_id;
|
let _tenant_id = self.tenant_id;
|
||||||
let plugin_id = self.plugin_id.clone();
|
let plugin_id = self.plugin_id.clone();
|
||||||
|
|
||||||
let rt = tokio::runtime::Handle::current();
|
let rt = tokio::runtime::Handle::current();
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ impl FlowExecutor {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
for mut t in consumed_tokens {
|
for t in consumed_tokens {
|
||||||
let ver = t.version;
|
let ver = t.version;
|
||||||
let mut active: token::ActiveModel = t.into();
|
let mut active: token::ActiveModel = t.into();
|
||||||
active.status = Set("completed".to_string());
|
active.status = Set("completed".to_string());
|
||||||
|
|||||||
Reference in New Issue
Block a user