feat(ci,ai): P2-1 权限注册表 + P2-2 AI utoipa 注解全覆盖

P2-1 权限注册表单一真相源:
- 新增 permissions.yaml: 131 个权限码 × 8 模块,含冻结标记
- 新增 scripts/gen-permissions.js: 生成器脚本
  --sql 输出 seed SQL, --frontend 输出 routeConfig 片段,
  --validate 验证一致性(131/131 = 0 mismatches)

P2-2 AI 模块 utoipa 注解:
- 为 30 个 handler 函数添加 #[utoipa::path] 注解
  (mod.rs 18 + insight 3 + risk 1 + rule 4 + suggestion 4)
- 为 6 个 DTO struct 添加 ToSchema/IntoParams derive
  (AnalyzeBody, CreatePromptBody, CreateRuleBody, UpdateRuleBody,
   ApproveBody, ExecuteBody, DialysisLabInput, ListAnalysisQuery,
   ListPromptsQuery)
- AI handler utoipa 覆盖率: 0/5 → 5/5 (100%)
This commit is contained in:
iven
2026-05-13 17:45:45 +08:00
parent 20d606d21c
commit 02082ccc61
11 changed files with 1500 additions and 9 deletions

View File

@@ -18,7 +18,7 @@ pub mod suggestion_handler;
// === 分析请求 Body ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct AnalyzeBody {
pub report_id: Option<uuid::Uuid>,
pub patient_id: Option<uuid::Uuid>,
@@ -27,6 +27,14 @@ pub struct AnalyzeBody {
// === SSE 分析端点 ===
#[utoipa::path(
post,
path = "/ai/analyze/lab-report",
request_body = AnalyzeBody,
responses((status = 200, description = "SSE 化验报告分析流")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn stream_lab_report<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -101,6 +109,14 @@ where
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
#[utoipa::path(
post,
path = "/ai/analyze/trends",
request_body = AnalyzeBody,
responses((status = 200, description = "SSE 趋势分析流")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn stream_trends<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -190,6 +206,14 @@ where
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
#[utoipa::path(
post,
path = "/ai/analyze/checkup-plan",
request_body = AnalyzeBody,
responses((status = 200, description = "SSE 体检计划分析流")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn stream_checkup_plan<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -258,6 +282,14 @@ where
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
#[utoipa::path(
post,
path = "/ai/analyze/report-summary",
request_body = AnalyzeBody,
responses((status = 200, description = "SSE 报告摘要分析流")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn stream_report_summary<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -335,7 +367,7 @@ where
// === 分析历史 ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct ListAnalysisQuery {
pub patient_id: Option<uuid::Uuid>,
pub analysis_type: Option<String>,
@@ -343,6 +375,14 @@ pub struct ListAnalysisQuery {
pub page_size: Option<u64>,
}
#[utoipa::path(
get,
path = "/ai/analysis/history",
params(ListAnalysisQuery),
responses((status = 200, description = "分析历史列表")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn list_analysis<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -419,6 +459,13 @@ where
}))))
}
#[utoipa::path(
get,
path = "/ai/analysis/{id}",
responses((status = 200, description = "分析详情")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn get_analysis<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -435,13 +482,21 @@ where
// === Prompt 管理 ===
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct ListPromptsQuery {
pub category: Option<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
#[utoipa::path(
get,
path = "/ai/prompts",
params(ListPromptsQuery),
responses((status = 200, description = "Prompt 模板列表")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn list_prompts<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -468,7 +523,7 @@ where
}))))
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct CreatePromptBody {
pub name: String,
pub description: Option<String>,
@@ -478,6 +533,14 @@ pub struct CreatePromptBody {
pub category: String,
}
#[utoipa::path(
post,
path = "/ai/prompts",
request_body = CreatePromptBody,
responses((status = 200, description = "创建 Prompt 模板")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn create_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -505,6 +568,13 @@ where
Ok(Json(ApiResponse::ok(prompt)))
}
#[utoipa::path(
post,
path = "/ai/prompts/{id}/activate",
responses((status = 200, description = "激活 Prompt 模板")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn activate_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -519,6 +589,13 @@ where
Ok(Json(ApiResponse::ok(prompt)))
}
#[utoipa::path(
post,
path = "/ai/prompts/{id}/rollback",
responses((status = 200, description = "回滚 Prompt 模板")),
tag = "AI Prompt",
security(("bearer_auth" = [])),
)]
pub async fn rollback_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -535,6 +612,13 @@ where
// === 用量统计 ===
#[utoipa::path(
get,
path = "/ai/usage/overview",
responses((status = 200, description = "AI 用量概览")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn usage_overview<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -550,6 +634,13 @@ where
}))))
}
#[utoipa::path(
get,
path = "/ai/usage/by-type",
responses((status = 200, description = "按类型用量统计")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn usage_by_type<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -574,6 +665,13 @@ where
// === Provider 管理 ===
#[utoipa::path(
get,
path = "/ai/providers/health",
responses((status = 200, description = "AI Provider 健康检查")),
tag = "AI Provider",
security(("bearer_auth" = [])),
)]
pub async fn provider_health<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -602,6 +700,13 @@ where
Ok(Json(ApiResponse::ok(result)))
}
#[utoipa::path(
get,
path = "/ai/providers",
responses((status = 200, description = "AI Provider 列表")),
tag = "AI Provider",
security(("bearer_auth" = [])),
)]
pub async fn provider_names<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -616,6 +721,13 @@ where
)))
}
#[utoipa::path(
get,
path = "/ai/quota/summary",
responses((status = 200, description = "AI 配额汇总")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn quota_summary<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -631,6 +743,13 @@ where
// === 透析风险评估KDIGO 规则) ===
#[utoipa::path(
post,
path = "/ai/dialysis/risk-assessment",
responses((status = 200, description = "透析风险评估")),
tag = "AI 分析",
security(("bearer_auth" = [])),
)]
pub async fn assess_dialysis_risk<S>(
Extension(ctx): Extension<TenantContext>,
Json(body): Json<crate::service::dialysis_risk_scorer::DialysisLabInput>,
@@ -650,6 +769,13 @@ where
// === 成本与预算 ===
#[utoipa::path(
get,
path = "/ai/budget/status",
responses((status = 200, description = "AI 预算状态")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn budget_status<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
@@ -670,6 +796,13 @@ pub struct CostEstimateQuery {
pub model: Option<String>,
}
#[utoipa::path(
get,
path = "/ai/cost/estimate",
responses((status = 200, description = "AI 成本预估")),
tag = "AI 用量",
security(("bearer_auth" = [])),
)]
pub async fn cost_estimate<S>(
Extension(ctx): Extension<TenantContext>,
Query(params): Query<CostEstimateQuery>,