diff --git a/crates/erp-ai/src/service/mod.rs b/crates/erp-ai/src/service/mod.rs index 3dcf9a6..59544e1 100644 --- a/crates/erp-ai/src/service/mod.rs +++ b/crates/erp-ai/src/service/mod.rs @@ -3,4 +3,5 @@ pub mod auto_analysis; pub mod local_rules; pub mod output_parser; pub mod prompt; +pub mod suggestion; pub mod usage; diff --git a/crates/erp-ai/src/service/suggestion.rs b/crates/erp-ai/src/service/suggestion.rs new file mode 100644 index 0000000..73e9281 --- /dev/null +++ b/crates/erp-ai/src/service/suggestion.rs @@ -0,0 +1,112 @@ +use uuid::Uuid; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use erp_core::error::AppResult; +use crate::dto::suggestion::*; +use crate::entity::ai_suggestion; + +pub struct SuggestionService; + +impl SuggestionService { + /// 批量创建建议记录 + pub async fn create_suggestions( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + analysis_id: Uuid, + suggestions: &[StructuredSuggestion], + risk_level: RiskLevel, + baseline_snapshot: &serde_json::Value, + created_by: Option, + ) -> AppResult> { + let mut ids = Vec::new(); + for s in suggestions { + let id = Uuid::now_v7(); + let model = ai_suggestion::ActiveModel { + id: Set(id), + tenant_id: Set(tenant_id), + analysis_id: Set(analysis_id), + suggestion_type: Set(s.suggestion_type.as_str().to_string()), + risk_level: Set(risk_level.as_str().to_string()), + params: Set(s.params.clone()), + status: Set(SuggestionStatus::Pending.as_str().to_string()), + workflow_instance_id: Set(None), + action_result: Set(None), + baseline_snapshot: Set(Some(baseline_snapshot.clone())), + reanalysis_id: Set(None), + created_by: Set(created_by), + updated_by: Set(created_by), + ..Default::default() + }; + model.insert(db).await?; + ids.push(id); + } + Ok(ids) + } + + /// 查询某次分析的所有建议(带 tenant_id 过滤 + 软删除) + pub async fn list_by_analysis( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + analysis_id: Uuid, + ) -> AppResult> { + let items = ai_suggestion::Entity::find() + .filter(ai_suggestion::Column::TenantId.eq(tenant_id)) + .filter(ai_suggestion::Column::AnalysisId.eq(analysis_id)) + .filter(ai_suggestion::Column::DeletedAt.is_null()) + .all(db) + .await?; + Ok(items) + } + + /// 查询待审批建议(带 tenant_id 过滤) + pub async fn list_pending( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + ) -> AppResult> { + let items = ai_suggestion::Entity::find() + .filter(ai_suggestion::Column::TenantId.eq(tenant_id)) + .filter(ai_suggestion::Column::Status.eq(SuggestionStatus::Pending.as_str())) + .filter(ai_suggestion::Column::DeletedAt.is_null()) + .all(db) + .await?; + Ok(items) + } + + /// 更新建议状态(带乐观锁 + tenant_id 过滤) + pub async fn update_status( + db: &sea_orm::DatabaseConnection, + suggestion_id: Uuid, + tenant_id: Uuid, + new_status: SuggestionStatus, + updated_by: Option, + ) -> AppResult<()> { + let item = ai_suggestion::Entity::find() + .filter(ai_suggestion::Column::Id.eq(suggestion_id)) + .filter(ai_suggestion::Column::TenantId.eq(tenant_id)) + .filter(ai_suggestion::Column::DeletedAt.is_null()) + .one(db) + .await? + .ok_or_else(|| { + crate::error::AiError::AnalysisNotFound("建议不存在".into()) + })?; + + let current_version = item.version_lock; + let mut active: ai_suggestion::ActiveModel = item.into(); + active.status = Set(new_status.as_str().to_string()); + active.updated_by = Set(updated_by); + active.version_lock = Set(current_version + 1); + active.update(db).await?; + Ok(()) + } + + /// 标记为解析失败(仅记录日志,不创建建议记录) + pub async fn mark_parse_failed( + _db: &sea_orm::DatabaseConnection, + analysis_id: Uuid, + ) -> AppResult<()> { + tracing::warn!( + analysis_id = %analysis_id, + "AI 结构化输出解析失败,降级为纯文本" + ); + Ok(()) + } +} diff --git a/crates/erp-ai/src/state.rs b/crates/erp-ai/src/state.rs index 8f11351..a1c7646 100644 --- a/crates/erp-ai/src/state.rs +++ b/crates/erp-ai/src/state.rs @@ -6,6 +6,7 @@ use sea_orm::DatabaseConnection; use crate::service::analysis::AnalysisService; use crate::service::prompt::PromptService; +use crate::service::suggestion::SuggestionService; use crate::service::usage::UsageService; #[derive(Clone)] @@ -15,5 +16,6 @@ pub struct AiState { pub analysis: Arc, pub prompt: Arc, pub usage: Arc, + pub suggestion: Arc, pub health_provider: Arc, }