feat(workflow): AI 行动闭环 BPMN 流程定义 — 随访/预约/预警三条审批流程
- ai_followup_workflow: 随访建议风险分级 + 医生审批 - ai_appointment_workflow: 预约建议风险分级 + 医生确认 - ai_alert_workflow: 预警确认风险分级 + 医生确认 - 启动时自动 seed 三条 published 状态的流程定义
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
199
crates/erp-workflow/src/service/ai_workflow_seed.rs
Normal file
199
crates/erp-workflow/src/service/ai_workflow_seed.rs
Normal 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(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod ai_workflow_seed;
|
||||
pub mod definition_service;
|
||||
pub mod instance_service;
|
||||
pub mod task_service;
|
||||
|
||||
Reference in New Issue
Block a user