diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 5ac0408..b655b8a 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -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 } } diff --git a/crates/erp-workflow/src/service/ai_workflow_seed.rs b/crates/erp-workflow/src/service/ai_workflow_seed.rs new file mode 100644 index 0000000..c069e37 --- /dev/null +++ b/crates/erp-workflow/src/service/ai_workflow_seed.rs @@ -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::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::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::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::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::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::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, + edges: Vec, +} + +fn all_templates() -> Vec { + 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(()) +} diff --git a/crates/erp-workflow/src/service/mod.rs b/crates/erp-workflow/src/service/mod.rs index e752f00..838e944 100644 --- a/crates/erp-workflow/src/service/mod.rs +++ b/crates/erp-workflow/src/service/mod.rs @@ -1,3 +1,4 @@ +pub mod ai_workflow_seed; pub mod definition_service; pub mod instance_service; pub mod task_service;