feat(ai): 建议状态生命周期 — 转换验证 + 执行端点 + 事件发布
建议(ai_suggestion)原有状态枚举完整但缺乏生命周期管理:
- 无转换验证(可从 Rejected 跳到 Approved)
- 无执行端点(护士无法标记"已执行")
- 无状态变更事件
变更:
1. SuggestionStatus.can_transition_to() — 仅允许合法单向转换
Pending → Approved/Rejected/Expired → Approved → Executed/Rejected/Expired
2. SuggestionService.execute_suggestion() — 记录执行结果
3. SuggestionService.expire_stale_suggestions() — 批量过期超时建议
4. POST /ai/suggestions/{id}/execute — 新执行端点
5. publish_status_event() — 状态变更时发布 ai.suggestion.status_changed 事件
6. 9 个新单元测试覆盖所有转换规则
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use axum::extract::{Extension, FromRef, Path, Query, State};
|
||||
use axum::extract::{Extension, FromRef, Path, State};
|
||||
use axum::Json;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
@@ -17,7 +17,7 @@ pub struct ListSuggestionsQuery {
|
||||
pub async fn list_suggestions<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Query(params): Query<ListSuggestionsQuery>,
|
||||
axum::extract::Query(params): axum::extract::Query<ListSuggestionsQuery>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
@@ -73,7 +73,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
SuggestionService::update_status(
|
||||
let updated = SuggestionService::update_status(
|
||||
&state.db,
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
@@ -82,12 +82,51 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 发布建议状态变更事件
|
||||
publish_status_event(&state, &updated).await;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||
"id": id,
|
||||
"status": new_status.as_str(),
|
||||
}))))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ExecuteBody {
|
||||
pub action_result: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 执行建议:护士标记建议为已执行,可选记录执行结果
|
||||
pub async fn execute_suggestion<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
Json(body): Json<ExecuteBody>,
|
||||
) -> 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 updated = SuggestionService::execute_suggestion(
|
||||
&state.db,
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
body.action_result,
|
||||
Some(ctx.user_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 发布建议状态变更事件
|
||||
publish_status_event(&state, &updated).await;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||
"id": id,
|
||||
"status": "executed",
|
||||
}))))
|
||||
}
|
||||
|
||||
/// 获取 AI 建议的前后对比报告。
|
||||
pub async fn get_comparison<S>(
|
||||
State(state): State<AiState>,
|
||||
@@ -127,3 +166,20 @@ where
|
||||
})))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布建议状态变更事件
|
||||
async fn publish_status_event(state: &AiState, suggestion: &crate::entity::ai_suggestion::Model) {
|
||||
let event = erp_core::events::DomainEvent::new(
|
||||
"ai.suggestion.status_changed",
|
||||
suggestion.tenant_id,
|
||||
erp_core::events::build_event_payload(serde_json::json!({
|
||||
"suggestion_id": suggestion.id,
|
||||
"analysis_id": suggestion.analysis_id,
|
||||
"suggestion_type": suggestion.suggestion_type,
|
||||
"risk_level": suggestion.risk_level,
|
||||
"status": suggestion.status,
|
||||
"updated_by": suggestion.updated_by,
|
||||
})),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user