feat(ai): 补全 Prompt CRUD + 分析历史 + 用量统计 handler 和路由
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 替换 list_analysis/get_analysis 空壳为真实查询
- 新增 list_prompts/create_prompt/activate_prompt/rollback_prompt
- 新增 usage_overview/usage_by_type
- 注册 6 个新路由到 AiModule
This commit is contained in:
iven
2026-04-25 23:21:11 +08:00
parent 48d1a84c77
commit 876308596a
2 changed files with 183 additions and 8 deletions

View File

@@ -270,29 +270,180 @@ pub struct ListAnalysisQuery {
}
pub async fn list_analysis<S>(
State(_state): State<AiState>,
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(_params): Query<ListAnalysisQuery>,
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
Query(params): Query<ListAnalysisQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(_state): State<AiState>,
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(_id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<crate::entity::ai_analysis::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
pub async fn list_prompts<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<ListPromptsQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<String>,
pub system_prompt: String,
pub user_prompt_template: String,
pub model_config: serde_json::Value,
pub category: String,
}
pub async fn create_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Json(body): Json<CreatePromptBody>,
) -> 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
.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<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.activate_prompt(id, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(prompt)))
}
pub async fn rollback_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.rollback_prompt(id, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(prompt)))
}
// === 用量统计 ===
pub async fn usage_overview<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<Vec<serde_json::Value>>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<serde_json::Value> = types
.into_iter()
.map(|t| {
serde_json::json!({
"analysis_type": t.analysis_type,
"count": t.count,
})
})
.collect();
Ok(Json(ApiResponse::ok(result)))
}
// === SSE 流构建辅助 ===

View File

@@ -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),
)
}
}