From b30897119be8f16be1fb9f2424088ae0efdbded5 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 1 May 2026 08:09:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20SuggestionService=20=E2=80=94=20?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE=E8=AE=B0=E5=BD=95=20CRUD=20+=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=B5=81=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_suggestions: 批量创建建议记录,关联分析 ID 和 baseline 快照 - list_by_analysis: 按 analysis_id 查询建议列表(带 tenant_id 过滤 + 软删除) - list_pending: 查询待审批建议 - update_status: 更新状态(带乐观锁 + tenant_id 过滤) - mark_parse_failed: 解析失败时记录日志 - AiState 新增 suggestion 字段 --- crates/erp-ai/src/service/mod.rs | 1 + crates/erp-ai/src/service/suggestion.rs | 112 ++++++++++++++++++++++++ crates/erp-ai/src/state.rs | 2 + 3 files changed, 115 insertions(+) create mode 100644 crates/erp-ai/src/service/suggestion.rs 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, }