feat(ai): 补全 Prompt CRUD + 分析历史 + 用量统计 handler 和路由
- 替换 list_analysis/get_analysis 空壳为真实查询 - 新增 list_prompts/create_prompt/activate_prompt/rollback_prompt - 新增 usage_overview/usage_by_type - 注册 6 个新路由到 AiModule
This commit is contained in:
@@ -270,29 +270,180 @@ pub struct ListAnalysisQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_analysis<S>(
|
pub async fn list_analysis<S>(
|
||||||
State(_state): State<AiState>,
|
State(state): State<AiState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(_params): Query<ListAnalysisQuery>,
|
Query(params): Query<ListAnalysisQuery>,
|
||||||
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
|
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||||
where
|
where
|
||||||
AiState: FromRef<S>,
|
AiState: FromRef<S>,
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.list")?;
|
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>(
|
pub async fn get_analysis<S>(
|
||||||
State(_state): State<AiState>,
|
State(state): State<AiState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path(_id): Path<uuid::Uuid>,
|
Path(id): Path<uuid::Uuid>,
|
||||||
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
|
) -> Result<Json<ApiResponse<crate::entity::ai_analysis::Model>>, erp_core::error::AppError>
|
||||||
where
|
where
|
||||||
AiState: FromRef<S>,
|
AiState: FromRef<S>,
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.list")?;
|
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 流构建辅助 ===
|
// === SSE 流构建辅助 ===
|
||||||
|
|||||||
@@ -104,5 +104,29 @@ impl AiModule {
|
|||||||
"/ai/analysis/{id}",
|
"/ai/analysis/{id}",
|
||||||
axum::routing::get(crate::handler::get_analysis),
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user