From 415d7617c83e1e1451d667c1b7d047beb7b205d6 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 1 May 2026 08:12:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E5=BB=BA=E8=AE=AE=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2/=E5=AE=A1=E6=89=B9=20API=20=E7=AB=AF=E7=82=B9=20+=20?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /ai/suggestions?analysis_id=xxx — 查看建议列表(ai.suggestion.list) - POST /ai/suggestions/{id}/approve — 批准/拒绝建议(ai.suggestion.manage) - 新增 ai.suggestion.list 和 ai.suggestion.manage 权限码 --- crates/erp-ai/src/handler/mod.rs | 2 + .../erp-ai/src/handler/suggestion_handler.rs | 89 +++++++++++++++++++ crates/erp-ai/src/module.rs | 20 +++++ 3 files changed, 111 insertions(+) create mode 100644 crates/erp-ai/src/handler/suggestion_handler.rs diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index b0ffcc7..b85bb0b 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -12,6 +12,8 @@ use crate::dto::{AnalysisSseEvent, AnalysisType}; use crate::service::suggestion::SuggestionService; use crate::state::AiState; +pub mod suggestion_handler; + // === 分析请求 Body === #[derive(Debug, Deserialize)] diff --git a/crates/erp-ai/src/handler/suggestion_handler.rs b/crates/erp-ai/src/handler/suggestion_handler.rs new file mode 100644 index 0000000..7f782ba --- /dev/null +++ b/crates/erp-ai/src/handler/suggestion_handler.rs @@ -0,0 +1,89 @@ +use axum::extract::{Extension, FromRef, Path, Query, State}; +use axum::Json; +use erp_core::rbac::require_permission; +use erp_core::types::{ApiResponse, TenantContext}; +use serde::Deserialize; + +use crate::dto::suggestion::SuggestionStatus; +use crate::service::suggestion::SuggestionService; +use crate::state::AiState; + +#[derive(Debug, Deserialize)] +pub struct ListSuggestionsQuery { + pub analysis_id: Option, + pub status: Option, +} + +pub async fn list_suggestions( + 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.suggestion.list")?; + + if let Some(analysis_id) = params.analysis_id { + let items = SuggestionService::list_by_analysis( + &state.db, + ctx.tenant_id, + analysis_id, + ) + .await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, + "total": items.len(), + })))) + } else { + let items = + SuggestionService::list_pending(&state.db, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, + "total": items.len(), + })))) + } +} + +#[derive(Debug, Deserialize)] +pub struct ApproveBody { + pub action: String, // "approve" or "reject" +} + +pub async fn approve_suggestion( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(body): Json, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.suggestion.manage")?; + + let new_status = match body.action.as_str() { + "approve" => SuggestionStatus::Approved, + "reject" => SuggestionStatus::Rejected, + _ => { + return Err(erp_core::error::AppError::Validation( + "action 必须为 approve 或 reject".into(), + )) + } + }; + + SuggestionService::update_status( + &state.db, + id, + ctx.tenant_id, + new_status, + Some(ctx.user_id), + ) + .await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({ + "id": id, + "status": new_status.as_str(), + })))) +} diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index 4f64bb3..6d78682 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -57,6 +57,18 @@ impl ErpModule for AiModule { description: "管理 AI 提供商配置".into(), module: "ai".into(), }, + PermissionDescriptor { + code: "ai.suggestion.list".into(), + name: "查看 AI 建议".into(), + description: "查看 AI 分析生成的建议列表".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.suggestion.manage".into(), + name: "审批 AI 建议".into(), + description: "批准或拒绝 AI 建议".into(), + module: "ai".into(), + }, ] } @@ -128,5 +140,13 @@ impl AiModule { "/ai/usage/by-type", axum::routing::get(crate::handler::usage_by_type), ) + .route( + "/ai/suggestions", + axum::routing::get(crate::handler::suggestion_handler::list_suggestions), + ) + .route( + "/ai/suggestions/{id}/approve", + axum::routing::post(crate::handler::suggestion_handler::approve_suggestion), + ) } }