feat(ai): SuggestionService — 建议记录 CRUD + 状态流转
- create_suggestions: 批量创建建议记录,关联分析 ID 和 baseline 快照 - list_by_analysis: 按 analysis_id 查询建议列表(带 tenant_id 过滤 + 软删除) - list_pending: 查询待审批建议 - update_status: 更新状态(带乐观锁 + tenant_id 过滤) - mark_parse_failed: 解析失败时记录日志 - AiState 新增 suggestion 字段
This commit is contained in:
@@ -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;
|
||||
|
||||
112
crates/erp-ai/src/service/suggestion.rs
Normal file
112
crates/erp-ai/src/service/suggestion.rs
Normal file
@@ -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<Uuid>,
|
||||
) -> AppResult<Vec<uuid::Uuid>> {
|
||||
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<Vec<ai_suggestion::Model>> {
|
||||
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<Vec<ai_suggestion::Model>> {
|
||||
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<Uuid>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
@@ -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<AnalysisService>,
|
||||
pub prompt: Arc<PromptService>,
|
||||
pub usage: Arc<UsageService>,
|
||||
pub suggestion: Arc<SuggestionService>,
|
||||
pub health_provider: Arc<dyn HealthDataProvider>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user