From 876308596a851c5d1fe7bd22c7459215a59964e9 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 23:21:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E8=A1=A5=E5=85=A8=20Prompt=20CRUD?= =?UTF-8?q?=20+=20=E5=88=86=E6=9E=90=E5=8E=86=E5=8F=B2=20+=20=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E7=BB=9F=E8=AE=A1=20handler=20=E5=92=8C=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 list_analysis/get_analysis 空壳为真实查询 - 新增 list_prompts/create_prompt/activate_prompt/rollback_prompt - 新增 usage_overview/usage_by_type - 注册 6 个新路由到 AiModule --- crates/erp-ai/src/handler/mod.rs | 167 +++++++++++++++++++++++++++++-- crates/erp-ai/src/module.rs | 24 +++++ 2 files changed, 183 insertions(+), 8 deletions(-) diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index e3bc6ef..d36d3a0 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -270,29 +270,180 @@ pub struct ListAnalysisQuery { } pub async fn list_analysis( - State(_state): State, + State(state): State, Extension(ctx): Extension, - Query(_params): Query, -) -> Result>, erp_core::error::AppError> + Query(params): Query, +) -> Result>, erp_core::error::AppError> where AiState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "ai.analysis.list")?; - Ok(Json(ApiResponse::ok(()))) + let pagination = erp_core::types::Pagination { + page: params.page, + page_size: params.page_size, + }; + let (items, total) = state + .analysis + .list_analysis(ctx.tenant_id, params.patient_id, params.analysis_type, &pagination) + .await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, + "total": total, + "page": pagination.page.unwrap_or(1), + "page_size": pagination.limit(), + })))) } pub async fn get_analysis( - State(_state): State, + State(state): State, Extension(ctx): Extension, - Path(_id): Path, -) -> Result>, erp_core::error::AppError> + Path(id): Path, +) -> Result>, erp_core::error::AppError> where AiState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "ai.analysis.list")?; - Ok(Json(ApiResponse::ok(()))) + let analysis = state.analysis.get_analysis(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(analysis))) +} + +// === Prompt 管理 === + +#[derive(Debug, Deserialize)] +pub struct ListPromptsQuery { + pub category: Option, + pub page: Option, + pub page_size: Option, +} + +pub async fn list_prompts( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.list")?; + let pagination = erp_core::types::Pagination { + page: params.page, + page_size: params.page_size, + }; + let (items, total) = state + .prompt + .list_prompts(ctx.tenant_id, params.category, &pagination) + .await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, + "total": total, + "page": pagination.page.unwrap_or(1), + "page_size": pagination.limit(), + })))) +} + +#[derive(Debug, Deserialize)] +pub struct CreatePromptBody { + pub name: String, + pub description: Option, + pub system_prompt: String, + pub user_prompt_template: String, + pub model_config: serde_json::Value, + pub category: String, +} + +pub async fn create_prompt( + State(state): State, + Extension(ctx): Extension, + Json(body): Json, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state + .prompt + .create_prompt( + ctx.tenant_id, + ctx.user_id, + body.name, + body.system_prompt, + body.user_prompt_template, + body.model_config, + body.category, + ) + .await?; + Ok(Json(ApiResponse::ok(prompt))) +} + +pub async fn activate_prompt( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state.prompt.activate_prompt(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(prompt))) +} + +pub async fn rollback_prompt( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state.prompt.rollback_prompt(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(prompt))) +} + +// === 用量统计 === + +pub async fn usage_overview( + State(state): State, + Extension(ctx): Extension, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.usage.list")?; + let overview = state.usage.get_overview(ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "total_count": overview.total_count, + })))) +} + +pub async fn usage_by_type( + State(state): State, + Extension(ctx): Extension, +) -> Result>>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.usage.list")?; + let types = state.usage.get_by_type(ctx.tenant_id).await?; + let result: Vec = types + .into_iter() + .map(|t| { + serde_json::json!({ + "analysis_type": t.analysis_type, + "count": t.count, + }) + }) + .collect(); + Ok(Json(ApiResponse::ok(result))) } // === SSE 流构建辅助 === diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index 7be464b..4f64bb3 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -104,5 +104,29 @@ impl AiModule { "/ai/analysis/{id}", axum::routing::get(crate::handler::get_analysis), ) + .route( + "/ai/prompts", + axum::routing::get(crate::handler::list_prompts), + ) + .route( + "/ai/prompts", + axum::routing::post(crate::handler::create_prompt), + ) + .route( + "/ai/prompts/{id}/activate", + axum::routing::post(crate::handler::activate_prompt), + ) + .route( + "/ai/prompts/{id}/rollback", + axum::routing::post(crate::handler::rollback_prompt), + ) + .route( + "/ai/usage/overview", + axum::routing::get(crate::handler::usage_overview), + ) + .route( + "/ai/usage/by-type", + axum::routing::get(crate::handler::usage_by_type), + ) } }