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 local_rules;
|
||||||
pub mod output_parser;
|
pub mod output_parser;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
|
pub mod suggestion;
|
||||||
pub mod usage;
|
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::analysis::AnalysisService;
|
||||||
use crate::service::prompt::PromptService;
|
use crate::service::prompt::PromptService;
|
||||||
|
use crate::service::suggestion::SuggestionService;
|
||||||
use crate::service::usage::UsageService;
|
use crate::service::usage::UsageService;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -15,5 +16,6 @@ pub struct AiState {
|
|||||||
pub analysis: Arc<AnalysisService>,
|
pub analysis: Arc<AnalysisService>,
|
||||||
pub prompt: Arc<PromptService>,
|
pub prompt: Arc<PromptService>,
|
||||||
pub usage: Arc<UsageService>,
|
pub usage: Arc<UsageService>,
|
||||||
|
pub suggestion: Arc<SuggestionService>,
|
||||||
pub health_provider: Arc<dyn HealthDataProvider>,
|
pub health_provider: Arc<dyn HealthDataProvider>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user