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 过滤(后端+前端同步)
- 停用/激活/回滚/删除操作完整可用
This commit is contained in:
iven
2026-05-26 17:04:26 +08:00
parent 3972db4f98
commit 3c7b48b6f6
10 changed files with 549 additions and 165 deletions

View File

@@ -95,7 +95,7 @@ where
let prompt = state
.prompt
.get_active_prompt(ctx.tenant_id, "lab_report_interpretation")
.get_active_prompt(ctx.tenant_id, AnalysisType::LabReport.prompt_name())
.await?;
let model_config = &prompt.model_config;
@@ -190,7 +190,7 @@ where
let prompt = state
.prompt
.get_active_prompt(ctx.tenant_id, "health_trend_analysis")
.get_active_prompt(ctx.tenant_id, AnalysisType::Trends.prompt_name())
.await?;
let model_config = &prompt.model_config;
@@ -262,7 +262,7 @@ where
let prompt = state
.prompt
.get_active_prompt(ctx.tenant_id, "personalized_checkup_plan")
.get_active_prompt(ctx.tenant_id, AnalysisType::CheckupPlan.prompt_name())
.await?;
let model_config = &prompt.model_config;
@@ -341,7 +341,7 @@ where
let prompt = state
.prompt
.get_active_prompt(ctx.tenant_id, "report_summary_generation")
.get_active_prompt(ctx.tenant_id, AnalysisType::ReportSummary.prompt_name())
.await?;
let model_config = &prompt.model_config;
@@ -417,7 +417,7 @@ where
let prompt = state
.prompt
.get_active_prompt(ctx.tenant_id, "follow_up_summary_generation")
.get_active_prompt(ctx.tenant_id, AnalysisType::FollowUpSummary.prompt_name())
.await?;
let model_config = &prompt.model_config;
@@ -577,6 +577,7 @@ where
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct ListPromptsQuery {
pub category: Option<String>,
pub analysis_type: Option<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
@@ -605,7 +606,11 @@ where
};
let (items, total) = state
.prompt
.list_prompts(ctx.tenant_id, params.category, &pagination)
.list_prompts(
ctx.tenant_id,
params.analysis_type.or(params.category),
&pagination,
)
.await?;
Ok(Json(ApiResponse::ok(serde_json::json!({
"data": items,
@@ -623,6 +628,7 @@ pub struct CreatePromptBody {
pub user_prompt_template: String,
pub model_config: serde_json::Value,
pub category: String,
pub analysis_type: String,
}
#[utoipa::path(
@@ -655,6 +661,7 @@ where
body.user_prompt_template,
body.model_config,
body.category,
body.analysis_type,
)
.await?;
Ok(Json(ApiResponse::ok(prompt)))
@@ -702,6 +709,48 @@ where
Ok(Json(ApiResponse::ok(prompt)))
}
#[utoipa::path(
post,
path = "/ai/prompts/{id}/deactivate",
responses((status = 200, description = "停用 Prompt 模板")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn deactivate_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<crate::entity::ai_prompt::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.prompt.manage")?;
let prompt = state.prompt.deactivate_prompt(id, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(prompt)))
}
#[utoipa::path(
delete,
path = "/ai/prompts/{id}",
responses((status = 200, description = "删除 Prompt 模板")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn delete_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.prompt.manage")?;
state.prompt.delete_prompt(id, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(())))
}
// === 用量统计 ===
#[utoipa::path(