Files
hms/crates/erp-ai/src/service/prompt.rs
iven 3c7b48b6f6 feat(ai): Prompt 管理 Phase 2 — analysis_type 后端选择键 + 筛选修复
- 新增 ai_prompt.analysis_type 列作为后端按链路选择 Prompt 的唯一键
- name 回归显示标识符用途,不再承担选择键角色
- 迁移 000164: 新增 analysis_type 列 + 从 name 回填 + 索引
- 迁移 000165: 修复旧数据从 category 错误回填的问题
- AiPromptList 页面重构: 分析类型/调用链路列、详情抽屉、新建表单
- DrawerForm 组件新增 onValuesChange 回调支持跨字段联动
- 新建表单选择分析类型后自动填充标识符
- 筛选过滤器改为按 analysis_type 而非 category 过滤(后端+前端同步)
- 停用/激活/回滚/删除操作完整可用
2026-05-26 17:04:26 +08:00

227 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use sea_orm::ActiveModelTrait;
use sea_orm::QuerySelect;
use sea_orm::Set;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
use uuid::Uuid;
use crate::entity::ai_prompt;
use crate::error::{AiError, AiResult};
use erp_core::types::Pagination;
pub struct PromptService {
pub db: sea_orm::DatabaseConnection,
}
impl PromptService {
pub fn new(db: sea_orm::DatabaseConnection) -> Self {
Self { db }
}
/// 获取当前激活的 Prompt 模板(按 analysis_type 查找)
pub async fn get_active_prompt(
&self,
tenant_id: Uuid,
analysis_type: &str,
) -> AiResult<ai_prompt::Model> {
ai_prompt::Entity::find()
.filter(ai_prompt::Column::TenantId.eq(tenant_id))
.filter(ai_prompt::Column::AnalysisType.eq(analysis_type))
.filter(ai_prompt::Column::IsActive.eq(true))
.filter(ai_prompt::Column::DeletedAt.is_null())
.one(&self.db)
.await?
.ok_or_else(|| AiError::PromptNotFound(analysis_type.into()))
}
/// 新建 Prompt
#[allow(clippy::too_many_arguments)]
pub async fn create_prompt(
&self,
tenant_id: Uuid,
user_id: Uuid,
name: String,
system_prompt: String,
user_prompt_template: String,
model_config: serde_json::Value,
category: String,
analysis_type: String,
) -> AiResult<ai_prompt::Model> {
let id = Uuid::now_v7();
let now = chrono::Utc::now();
let active = ai_prompt::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
name: Set(name),
description: Set(String::new()),
system_prompt: Set(system_prompt),
user_prompt_template: Set(user_prompt_template),
variables_schema: Set(None),
model_config: Set(model_config),
version: Set(1),
is_active: Set(true),
category: Set(category),
analysis_type: Set(analysis_type),
tags: Set(None),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(Some(user_id)),
updated_by: Set(Some(user_id)),
deleted_at: Set(None),
version_lock: Set(1),
};
Ok(active.insert(&self.db).await?)
}
/// 分页列出 Prompt 模板
pub async fn list_prompts(
&self,
tenant_id: Uuid,
analysis_type: Option<String>,
pagination: &Pagination,
) -> AiResult<(Vec<ai_prompt::Model>, u64)> {
let mut query = ai_prompt::Entity::find()
.filter(ai_prompt::Column::TenantId.eq(tenant_id))
.filter(ai_prompt::Column::DeletedAt.is_null());
if let Some(at) = &analysis_type {
query = query.filter(ai_prompt::Column::AnalysisType.eq(at.as_str()));
}
let total = query.clone().count(&self.db).await?;
let items = query
.order_by_desc(ai_prompt::Column::UpdatedAt)
.offset(pagination.offset())
.limit(pagination.limit())
.all(&self.db)
.await?;
Ok((items, total))
}
/// 更新 Prompt创建新版本
#[allow(clippy::too_many_arguments)]
pub async fn update_prompt(
&self,
id: Uuid,
tenant_id: Uuid,
user_id: Uuid,
system_prompt: Option<String>,
user_prompt_template: Option<String>,
model_config: Option<serde_json::Value>,
description: Option<String>,
) -> AiResult<ai_prompt::Model> {
let entity = ai_prompt::Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AiError::PromptNotFound(id.to_string()))?;
if entity.tenant_id != tenant_id {
return Err(AiError::Validation("跨租户操作".into()));
}
let new_id = Uuid::now_v7();
let now = chrono::Utc::now();
let active = ai_prompt::ActiveModel {
id: Set(new_id),
tenant_id: Set(tenant_id),
name: Set(entity.name.clone()),
description: Set(description.unwrap_or(entity.description.clone())),
system_prompt: Set(system_prompt.unwrap_or(entity.system_prompt.clone())),
user_prompt_template: Set(
user_prompt_template.unwrap_or(entity.user_prompt_template.clone())
),
variables_schema: Set(entity.variables_schema.clone()),
model_config: Set(model_config.unwrap_or(entity.model_config.clone())),
version: Set(entity.version + 1),
is_active: Set(entity.is_active),
category: Set(entity.category.clone()),
analysis_type: Set(entity.analysis_type.clone()),
tags: Set(entity.tags.clone()),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(Some(user_id)),
updated_by: Set(Some(user_id)),
deleted_at: Set(None),
version_lock: Set(1),
};
Ok(active.insert(&self.db).await?)
}
/// 激活指定 Prompt停用同 analysis_type 的其他版本,原子操作)
pub async fn activate_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_prompt::Model> {
let entity = ai_prompt::Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AiError::PromptNotFound(id.to_string()))?;
if entity.tenant_id != tenant_id {
return Err(AiError::Validation("跨租户操作".into()));
}
// 原子操作:停用同 analysis_type 的其他版本
ai_prompt::Entity::update_many()
.col_expr(
ai_prompt::Column::IsActive,
sea_orm::sea_query::Expr::value(false),
)
.col_expr(
ai_prompt::Column::UpdatedAt,
sea_orm::sea_query::Expr::value(chrono::Utc::now()),
)
.filter(ai_prompt::Column::TenantId.eq(tenant_id))
.filter(ai_prompt::Column::AnalysisType.eq(&entity.analysis_type))
.filter(ai_prompt::Column::Id.ne(id))
.filter(ai_prompt::Column::DeletedAt.is_null())
.exec(&self.db)
.await?;
// 激活目标
let mut active: ai_prompt::ActiveModel = entity.into();
active.is_active = Set(true);
active.updated_at = Set(chrono::Utc::now());
active.version_lock = Set(active.version_lock.take().unwrap_or(0) + 1);
Ok(active.update(&self.db).await?)
}
/// 回滚(= 激活指定旧版本)
pub async fn rollback_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_prompt::Model> {
self.activate_prompt(id, tenant_id).await
}
/// 停用 Prompt
pub async fn deactivate_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_prompt::Model> {
let entity = ai_prompt::Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AiError::PromptNotFound(id.to_string()))?;
if entity.tenant_id != tenant_id {
return Err(AiError::Validation("跨租户操作".into()));
}
let mut active: ai_prompt::ActiveModel = entity.into();
active.is_active = Set(false);
active.updated_at = Set(chrono::Utc::now());
active.version_lock = Set(active.version_lock.take().unwrap_or(0) + 1);
Ok(active.update(&self.db).await?)
}
/// 删除 Prompt软删除
pub async fn delete_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<()> {
let entity = ai_prompt::Entity::find_by_id(id)
.one(&self.db)
.await?
.ok_or_else(|| AiError::PromptNotFound(id.to_string()))?;
if entity.tenant_id != tenant_id {
return Err(AiError::Validation("跨租户操作".into()));
}
let mut active: ai_prompt::ActiveModel = entity.into();
active.deleted_at = Set(Some(chrono::Utc::now()));
active.updated_at = Set(chrono::Utc::now());
active.version_lock = Set(active.version_lock.take().unwrap_or(0) + 1);
active.update(&self.db).await?;
Ok(())
}
}