feat(ai): 建议查询/审批 API 端点 + 权限注册

- GET /ai/suggestions?analysis_id=xxx — 查看建议列表(ai.suggestion.list)
- POST /ai/suggestions/{id}/approve — 批准/拒绝建议(ai.suggestion.manage)
- 新增 ai.suggestion.list 和 ai.suggestion.manage 权限码
This commit is contained in:
iven
2026-05-01 08:12:29 +08:00
parent 6e761ae22b
commit 415d7617c8
3 changed files with 111 additions and 0 deletions

View File

@@ -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)]

View File

@@ -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<uuid::Uuid>,
pub status: Option<String>,
}
pub async fn list_suggestions<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<ListSuggestionsQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
Json(body): Json<ApproveBody>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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(),
}))))
}

View File

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