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:
@@ -8,6 +8,13 @@ use serde::Deserialize;
|
||||
use crate::dto::copilot::ListInsightsQuery;
|
||||
use crate::state::AiState;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/copilot/insights",
|
||||
responses((status = 200, description = "Copilot 洞察列表")),
|
||||
tag = "Copilot 洞察",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_insights<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -41,6 +48,13 @@ where
|
||||
}))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/copilot/insights/{id}",
|
||||
responses((status = 200, description = "洞察详情")),
|
||||
tag = "Copilot 洞察",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn get_insight<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -68,6 +82,13 @@ pub struct DismissBody {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/copilot/insights/{id}/dismiss",
|
||||
responses((status = 200, description = "忽略洞察")),
|
||||
tag = "Copilot 洞察",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn dismiss_insight<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -5,6 +5,13 @@ use erp_core::types::{ApiResponse, TenantContext};
|
||||
|
||||
use crate::state::AiState;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/copilot/patients/{id}/risk",
|
||||
responses((status = 200, description = "患者风险评分")),
|
||||
tag = "Copilot 风险",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn get_patient_risk<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
@@ -8,6 +8,13 @@ use crate::dto::copilot::{CreateRuleBody, UpdateRuleBody};
|
||||
use crate::entity::copilot_rules;
|
||||
use crate::state::AiState;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/copilot/rules",
|
||||
responses((status = 200, description = "Copilot 规则列表")),
|
||||
tag = "Copilot 规则",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_rules<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -40,6 +47,13 @@ where
|
||||
}))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/copilot/rules",
|
||||
responses((status = 200, description = "创建 Copilot 规则")),
|
||||
tag = "Copilot 规则",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn create_rule<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -75,6 +89,13 @@ where
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/copilot/rules/{id}",
|
||||
responses((status = 200, description = "更新 Copilot 规则")),
|
||||
tag = "Copilot 规则",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn update_rule<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -128,6 +149,13 @@ where
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/copilot/rules/{id}",
|
||||
responses((status = 200, description = "删除 Copilot 规则")),
|
||||
tag = "Copilot 规则",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn delete_rule<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -151,6 +179,7 @@ where
|
||||
active.deleted_at = Set(Some(chrono::Utc::now()));
|
||||
active.updated_at = Set(chrono::Utc::now());
|
||||
active.updated_by = Set(Some(ctx.user_id));
|
||||
active.version_lock = Set(active.version_lock.unwrap() + 1);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({"deleted": true}))))
|
||||
|
||||
@@ -14,6 +14,13 @@ pub struct ListSuggestionsQuery {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ai/suggestions",
|
||||
responses((status = 200, description = "AI 建议列表")),
|
||||
tag = "AI 建议",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_suggestions<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -41,11 +48,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ApproveBody {
|
||||
pub action: String, // "approve" or "reject"
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/suggestions/{id}/approve",
|
||||
responses((status = 200, description = "审批 AI 建议")),
|
||||
tag = "AI 建议",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn approve_suggestion<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -86,12 +100,19 @@ where
|
||||
}))))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ExecuteBody {
|
||||
pub action_result: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 执行建议:护士标记建议为已执行,可选记录执行结果
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/suggestions/{id}/execute",
|
||||
responses((status = 200, description = "执行 AI 建议")),
|
||||
tag = "AI 建议",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn execute_suggestion<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -123,6 +144,13 @@ where
|
||||
}
|
||||
|
||||
/// 获取 AI 建议的前后对比报告。
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ai/suggestions/{id}/comparison",
|
||||
responses((status = 200, description = "AI 建议对比")),
|
||||
tag = "AI 建议",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn get_comparison<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
Reference in New Issue
Block a user