feat(workflow): AI 行动闭环 BPMN 流程定义 — 随访/预约/预警三条审批流程

- ai_followup_workflow: 随访建议风险分级 + 医生审批
- ai_appointment_workflow: 预约建议风险分级 + 医生确认
- ai_alert_workflow: 预警确认风险分级 + 医生确认
- 启动时自动 seed 三条 published 状态的流程定义
This commit is contained in:
iven
2026-05-01 08:49:49 +08:00
parent 5053908444
commit 388948e348
3 changed files with 206 additions and 0 deletions

View File

@@ -289,6 +289,12 @@ async fn main() -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!("Failed to seed auth data: {}", e))?;
tracing::info!(tenant_id = %new_tenant_id, "Default tenant ready with auth seed data");
// Seed AI workflow definitions
if let Err(e) = erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id).await {
tracing::warn!(error = %e, "Failed to seed AI workflow definitions");
}
new_tenant_id
}
}

View File

@@ -0,0 +1,199 @@
//! AI 行动闭环 BPMN 流程定义种子数据
//!
//! 三条流程:
//! - ai_followup_workflow — AI 随访建议审批
//! - ai_appointment_workflow — AI 预约建议审批
//! - ai_alert_workflow — AI 预警确认
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use uuid::Uuid;
use crate::entity::process_definition;
/// AI 随访审批流程
///
/// ```text
/// Start → ExclusiveGateway(风险分级)
/// → [low] → End (自动执行,由分发器直接处理)
/// → [medium] → UserTask(医生审批) → ExclusiveGateway → [approved] → End
/// → [rejected] → End
/// → [high] → UserTask(紧急确认) → ExclusiveGateway → [approved] → End
/// → [rejected] → End
/// ```
fn followup_nodes() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "start", "type": "StartEvent", "name": "AI 随访建议"},
{"id": "gw_risk", "type": "ExclusiveGateway", "name": "风险分级"},
{"id": "end_auto", "type": "EndEvent", "name": "自动完成"},
{"id": "doctor_review", "type": "UserTask", "name": "医生审批随访建议",
"candidate_groups": ["doctor"]},
{"id": "gw_outcome", "type": "ExclusiveGateway", "name": "审批结果"},
{"id": "end_approved", "type": "EndEvent", "name": "已批准"},
{"id": "end_rejected", "type": "EndEvent", "name": "已拒绝"}
])).unwrap()
}
fn followup_edges() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "e1", "source": "start", "target": "gw_risk"},
{"id": "e2", "source": "gw_risk", "target": "end_auto",
"condition": "risk_level == \"low\"", "label": "低风险"},
{"id": "e3", "source": "gw_risk", "target": "doctor_review",
"label": "中/高风险"},
{"id": "e4", "source": "doctor_review", "target": "gw_outcome"},
{"id": "e5", "source": "gw_outcome", "target": "end_approved",
"condition": "outcome == \"approved\"", "label": "批准"},
{"id": "e6", "source": "gw_outcome", "target": "end_rejected",
"condition": "outcome == \"rejected\"", "label": "拒绝"}
])).unwrap()
}
/// AI 预约审批流程
fn appointment_nodes() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "start", "type": "StartEvent", "name": "AI 预约建议"},
{"id": "gw_risk", "type": "ExclusiveGateway", "name": "风险分级"},
{"id": "end_auto", "type": "EndEvent", "name": "自动完成"},
{"id": "doctor_confirm", "type": "UserTask", "name": "医生确认预约建议",
"candidate_groups": ["doctor"]},
{"id": "gw_outcome", "type": "ExclusiveGateway", "name": "确认结果"},
{"id": "end_approved", "type": "EndEvent", "name": "已确认"},
{"id": "end_rejected", "type": "EndEvent", "name": "已拒绝"}
])).unwrap()
}
fn appointment_edges() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "e1", "source": "start", "target": "gw_risk"},
{"id": "e2", "source": "gw_risk", "target": "end_auto",
"condition": "risk_level == \"low\"", "label": "低风险"},
{"id": "e3", "source": "gw_risk", "target": "doctor_confirm",
"label": "中/高风险"},
{"id": "e4", "source": "doctor_confirm", "target": "gw_outcome"},
{"id": "e5", "source": "gw_outcome", "target": "end_approved",
"condition": "outcome == \"approved\"", "label": "确认"},
{"id": "e6", "source": "gw_outcome", "target": "end_rejected",
"condition": "outcome == \"rejected\"", "label": "拒绝"}
])).unwrap()
}
/// AI 预警确认流程
fn alert_nodes() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "start", "type": "StartEvent", "name": "AI 预警"},
{"id": "gw_risk", "type": "ExclusiveGateway", "name": "风险分级"},
{"id": "end_auto", "type": "EndEvent", "name": "已发送"},
{"id": "doctor_ack", "type": "UserTask", "name": "医生确认预警",
"candidate_groups": ["doctor"]},
{"id": "gw_outcome", "type": "ExclusiveGateway", "name": "确认结果"},
{"id": "end_acknowledged", "type": "EndEvent", "name": "已确认"},
{"id": "end_escalated", "type": "EndEvent", "name": "已升级"}
])).unwrap()
}
fn alert_edges() -> Vec<serde_json::Value> {
serde_json::from_value(serde_json::json!([
{"id": "e1", "source": "start", "target": "gw_risk"},
{"id": "e2", "source": "gw_risk", "target": "end_auto",
"condition": "risk_level == \"low\"", "label": "低风险"},
{"id": "e3", "source": "gw_risk", "target": "doctor_ack",
"label": "中/高风险"},
{"id": "e4", "source": "doctor_ack", "target": "gw_outcome"},
{"id": "e5", "source": "gw_outcome", "target": "end_acknowledged",
"condition": "outcome == \"approved\"", "label": "确认"},
{"id": "e6", "source": "gw_outcome", "target": "end_escalated",
"condition": "outcome == \"rejected\"", "label": "升级"}
])).unwrap()
}
struct WorkflowTemplate {
key: &'static str,
name: &'static str,
category: &'static str,
description: &'static str,
nodes: Vec<serde_json::Value>,
edges: Vec<serde_json::Value>,
}
fn all_templates() -> Vec<WorkflowTemplate> {
vec![
WorkflowTemplate {
key: "ai_followup_workflow",
name: "AI 随访建议审批",
category: "ai_action",
description: "AI 分析生成的随访建议,按风险等级自动执行或提交医生审批",
nodes: followup_nodes(),
edges: followup_edges(),
},
WorkflowTemplate {
key: "ai_appointment_workflow",
name: "AI 预约建议审批",
category: "ai_action",
description: "AI 分析生成的预约建议,按风险等级自动执行或提交医生确认",
nodes: appointment_nodes(),
edges: appointment_edges(),
},
WorkflowTemplate {
key: "ai_alert_workflow",
name: "AI 预警确认",
category: "ai_action",
description: "AI 分析生成的预警通知,按风险等级自动发送或提交医生确认",
nodes: alert_nodes(),
edges: alert_edges(),
},
]
}
/// 确保 AI 行动闭环的工作流定义存在(幂等)。
///
/// 对每个 tenant_id 检查 key 是否已存在,不存在则创建并发布。
pub async fn ensure_ai_workflows(
db: &sea_orm::DatabaseConnection,
tenant_id: Uuid,
) -> Result<(), sea_orm::DbErr> {
let system_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
for tmpl in all_templates() {
let exists = process_definition::Entity::find()
.filter(process_definition::Column::TenantId.eq(tenant_id))
.filter(process_definition::Column::Key.eq(tmpl.key))
.filter(process_definition::Column::DeletedAt.is_null())
.one(db)
.await?
.is_some();
if exists {
continue;
}
let now = Utc::now();
let id = Uuid::now_v7();
let active = process_definition::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
name: Set(tmpl.name.to_string()),
key: Set(tmpl.key.to_string()),
version: Set(1),
category: Set(Some(tmpl.category.to_string())),
description: Set(Some(tmpl.description.to_string())),
nodes: Set(serde_json::json!(tmpl.nodes)),
edges: Set(serde_json::json!(tmpl.edges)),
status: Set("published".to_string()),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(system_id),
updated_by: Set(system_id),
deleted_at: Set(None),
version_field: Set(1),
};
active.insert(db).await?;
tracing::info!(
key = %tmpl.key,
tenant_id = %tenant_id,
"AI 工作流定义已创建"
);
}
Ok(())
}

View File

@@ -1,3 +1,4 @@
pub mod ai_workflow_seed;
pub mod definition_service;
pub mod instance_service;
pub mod task_service;