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))?;
|
.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");
|
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
|
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 definition_service;
|
||||||
pub mod instance_service;
|
pub mod instance_service;
|
||||||
pub mod task_service;
|
pub mod task_service;
|
||||||
|
|||||||
Reference in New Issue
Block a user