feat: initialize Nuanji (Warm Notes) project

- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin)
- Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs)
- Integrated erp-diary into workspace and erp-server
- Added DiaryModule registration in main.rs
- Added DiaryState FromRef in state.rs
- Diary routes mounted (empty routes, ready for implementation)
- Product design spec v1.2 preserved in docs/
- Implementation plan preserved in plans/

Cargo check: OK
Cargo test: OK (78+ base tests passing)
This commit is contained in:
iven
2026-05-31 20:52:19 +08:00
commit c539e6fd83
285 changed files with 59156 additions and 0 deletions

View File

@@ -0,0 +1,549 @@
use axum::Router;
use axum::routing::{get, post, put};
use std::time::Duration;
use uuid::Uuid;
use erp_core::error::AppResult;
use erp_core::events::EventBus;
use erp_core::module::{ErpModule, PermissionDescriptor};
use crate::handler::{definition_handler, instance_handler, task_handler};
/// Workflow module implementing the `ErpModule` trait.
///
/// Manages workflow definitions, process instances, tasks,
/// and the token-driven execution engine.
pub struct WorkflowModule;
impl WorkflowModule {
pub fn new() -> Self {
Self
}
/// Build protected (authenticated) routes for the workflow module.
pub fn protected_routes<S>() -> Router<S>
where
crate::workflow_state::WorkflowState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
// Definition routes
.route(
"/workflow/definitions",
get(definition_handler::list_definitions)
.post(definition_handler::create_definition),
)
.route(
"/workflow/definitions/{id}",
get(definition_handler::get_definition).put(definition_handler::update_definition),
)
.route(
"/workflow/definitions/{id}/publish",
post(definition_handler::publish_definition),
)
.route(
"/workflow/definitions/{id}/deprecate",
post(definition_handler::deprecate_definition),
)
// Instance routes
.route(
"/workflow/instances",
post(instance_handler::start_instance).get(instance_handler::list_instances),
)
.route(
"/workflow/instances/{id}",
get(instance_handler::get_instance),
)
.route(
"/workflow/instances/{id}/suspend",
post(instance_handler::suspend_instance),
)
.route(
"/workflow/instances/{id}/resume",
post(instance_handler::resume_instance),
)
.route(
"/workflow/instances/{id}/terminate",
post(instance_handler::terminate_instance),
)
// Task routes
.route(
"/workflow/tasks/pending",
get(task_handler::list_pending_tasks),
)
.route(
"/workflow/tasks/completed",
get(task_handler::list_completed_tasks),
)
.route(
"/workflow/tasks/{id}/complete",
post(task_handler::complete_task),
)
.route(
"/workflow/tasks/{id}/delegate",
post(task_handler::delegate_task),
)
.route("/workflow/tasks/{id}/claim", put(task_handler::claim_task))
}
/// 启动超时检查后台任务。
///
/// 每 60 秒扫描一次 tasks 表,查找 due_date 已过期但仍处于 pending 状态的任务。
/// 发现超时任务时发布 `task.timeout` 事件到事件总线,并记录 warning 日志。
pub fn start_timeout_checker(db: sea_orm::DatabaseConnection, event_bus: EventBus) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
// 首次跳过,等一个完整间隔再执行
interval.tick().await;
loop {
interval.tick().await;
match crate::engine::timeout::TimeoutChecker::find_all_overdue_tasks_with_details(
&db,
)
.await
{
Ok(overdue) => {
if !overdue.is_empty() {
tracing::warn!(
count = overdue.len(),
"发现超时未完成的任务,发布 task.timeout 事件"
);
for (task_id, tenant_id, instance_id, assignee_id) in &overdue {
// 发布超时事件
let event = erp_core::events::DomainEvent::new(
"task.timeout",
*tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"task_id": task_id,
"instance_id": instance_id,
"assignee_id": assignee_id,
})),
);
event_bus.publish(event, &db).await;
}
}
}
Err(e) => {
tracing::warn!(error = %e, "超时检查任务执行失败");
}
}
}
});
}
}
/// 处理 AI 行动工作流启动请求
async fn handle_ai_action_start(
db: &sea_orm::DatabaseConnection,
event_bus: &EventBus,
event: &erp_core::events::DomainEvent,
) {
let workflow_key = match event.payload.get("workflow_key").and_then(|v| v.as_str()) {
Some(k) => k,
None => {
tracing::warn!("AI 行动工作流事件缺少 workflow_key跳过");
return;
}
};
let tenant_id = event.tenant_id;
// 查找对应的流程定义
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
let def = crate::entity::process_definition::Entity::find()
.filter(crate::entity::process_definition::Column::TenantId.eq(tenant_id))
.filter(crate::entity::process_definition::Column::Key.eq(workflow_key))
.filter(crate::entity::process_definition::Column::DeletedAt.is_null())
.filter(crate::entity::process_definition::Column::Status.eq("published"))
.one(db)
.await;
let def = match def {
Ok(Some(d)) => d,
Ok(None) => {
tracing::warn!(
key = %workflow_key,
tenant_id = %tenant_id,
"AI 行动工作流定义未找到或未发布,跳过"
);
return;
}
Err(e) => {
tracing::warn!(error = %e, "查询工作流定义失败");
return;
}
};
// 构造启动变量
let risk_level = event
.payload
.get("risk_level")
.and_then(|v| v.as_str())
.unwrap_or("medium")
.to_string();
let variables = vec![
crate::dto::SetVariableReq {
name: "risk_level".into(),
var_type: Some("string".into()),
value: serde_json::Value::String(risk_level.clone()),
},
crate::dto::SetVariableReq {
name: "patient_id".into(),
var_type: Some("string".into()),
value: event
.payload
.get("patient_id")
.cloned()
.unwrap_or(serde_json::Value::Null),
},
crate::dto::SetVariableReq {
name: "action_type".into(),
var_type: Some("string".into()),
value: event
.payload
.get("action_type")
.and_then(|v| v.as_str())
.map(|s| serde_json::Value::String(s.to_string()))
.unwrap_or(serde_json::Value::Null),
},
crate::dto::SetVariableReq {
name: "params".into(),
var_type: Some("string".into()),
value: event
.payload
.get("params")
.cloned()
.unwrap_or(serde_json::Value::Null),
},
];
let req = crate::dto::StartInstanceReq {
definition_id: def.id,
business_key: Some(format!(
"ai_action_{}",
chrono::Utc::now().timestamp_millis()
)),
variables: Some(variables),
};
let system_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
match crate::service::instance_service::InstanceService::start(
tenant_id, system_id, &req, db, event_bus,
)
.await
{
Ok(instance) => {
tracing::info!(
key = %workflow_key,
instance_id = %instance.id,
tenant_id = %tenant_id,
risk_level = %risk_level,
"AI 行动工作流实例已启动"
);
}
Err(e) => {
tracing::warn!(
key = %workflow_key,
error = %e,
"AI 行动工作流实例启动失败"
);
}
}
}
impl Default for WorkflowModule {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl ErpModule for WorkflowModule {
fn name(&self) -> &str {
"workflow"
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn dependencies(&self) -> Vec<&str> {
vec!["auth"]
}
fn register_event_handlers(&self, _bus: &EventBus) {
// 事件处理器已迁移到 on_startup需要 DB 连接),此处保留空实现以兼容 trait 签名
}
async fn on_startup(
&self,
ctx: &erp_core::module::ModuleContext,
) -> erp_core::error::AppResult<()> {
let db = ctx.db.clone();
let bus = ctx.event_bus.clone();
// 订阅 user. 前缀事件,处理 user.deleted
let (mut receiver, _handle) = bus.subscribe_filtered("user.".to_string());
tokio::spawn(async move {
loop {
match receiver.recv().await {
Some(event) if event.event_type == "user.deleted" => {
let user_id = match event.payload.get("user_id").and_then(|v| v.as_str()) {
Some(id) => match Uuid::parse_str(id) {
Ok(u) => u,
Err(e) => {
tracing::warn!(
error = %e,
"user.deleted 事件的 user_id 解析失败,跳过"
);
continue;
}
},
_ => {
tracing::warn!("user.deleted 事件缺少 user_id 字段,跳过");
continue;
}
};
tracing::info!(
user_id = %user_id,
tenant_id = %event.tenant_id,
"收到 user.deleted 事件,查找并终止相关流程实例"
);
// 查找该用户有活跃任务的流程实例
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set,
};
// 查找该用户作为 assignee 的 pending 任务
let active_tasks = crate::entity::task::Entity::find()
.filter(crate::entity::task::Column::TenantId.eq(event.tenant_id))
.filter(crate::entity::task::Column::AssigneeId.eq(user_id))
.filter(crate::entity::task::Column::Status.eq("pending"))
.filter(crate::entity::task::Column::DeletedAt.is_null())
.all(&db)
.await;
match active_tasks {
Ok(tasks) if tasks.is_empty() => {
tracing::info!(
user_id = %user_id,
"该用户没有活跃的待办任务,无需终止流程"
);
}
Ok(tasks) => {
// 收集需要终止的实例 ID
let instance_ids: std::collections::HashSet<Uuid> =
tasks.iter().map(|t| t.instance_id).collect();
for instance_id in &instance_ids {
// 将实例状态设置为 terminated
let instance =
crate::entity::process_instance::Entity::find_by_id(
*instance_id,
)
.one(&db)
.await;
if let Ok(Some(inst)) = instance
&& inst.tenant_id == event.tenant_id
&& inst.deleted_at.is_none()
&& inst.status == "running"
{
let ver = inst.version;
let mut active: crate::entity::process_instance::ActiveModel = inst.into();
active.status = Set("terminated".to_string());
active.updated_at = Set(Utc::now());
active.version = Set(ver + 1);
match active.update(&db).await {
Ok(_) => {
tracing::info!(
instance_id = %instance_id,
"流程实例已终止(用户被删除)"
);
}
Err(e) => {
tracing::warn!(
instance_id = %instance_id,
error = %e,
"终止流程实例失败"
);
}
}
}
}
tracing::info!(
user_id = %user_id,
instance_count = instance_ids.len(),
task_count = tasks.len(),
"用户删除事件处理完成"
);
}
Err(e) => {
tracing::warn!(
error = %e,
"查询用户活跃任务失败"
);
}
}
}
Some(event) => {
// 其他 user. 前缀事件,忽略
tracing::debug!(
event_type = %event.event_type,
"忽略非 user.deleted 事件"
);
}
None => {
// 通道关闭,退出循环
tracing::info!("Workflow 事件订阅通道已关闭");
break;
}
}
}
});
tracing::info!(
module = "workflow",
"Workflow 事件处理器已注册(监听 user.deleted"
);
// 订阅 AI 行动工作流启动请求
let (mut ai_rx, _ai_handle) = bus.subscribe_filtered("workflow.ai_action.".to_string());
let ai_db = ctx.db.clone();
let ai_bus = bus.clone();
tokio::spawn(async move {
loop {
match ai_rx.recv().await {
Some(event) if event.event_type == "workflow.ai_action.start_requested" => {
handle_ai_action_start(&ai_db, &ai_bus, &event).await;
}
Some(_) => {}
None => {
tracing::info!("AI 行动工作流事件订阅通道已关闭");
break;
}
}
}
});
Ok(())
}
async fn on_tenant_created(
&self,
_tenant_id: Uuid,
_db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
Ok(())
}
async fn on_tenant_deleted(
&self,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
// Delete in dependency order: variables → tasks → tokens → instances → definitions
// process_variables
crate::entity::process_variable::Entity::delete_many()
.filter(crate::entity::process_variable::Column::TenantId.eq(tenant_id))
.exec(db)
.await?;
// tasks
crate::entity::task::Entity::delete_many()
.filter(crate::entity::task::Column::TenantId.eq(tenant_id))
.exec(db)
.await?;
// tokens
crate::entity::token::Entity::delete_many()
.filter(crate::entity::token::Column::TenantId.eq(tenant_id))
.exec(db)
.await?;
// process_instances
crate::entity::process_instance::Entity::delete_many()
.filter(crate::entity::process_instance::Column::TenantId.eq(tenant_id))
.exec(db)
.await?;
// process_definitions
crate::entity::process_definition::Entity::delete_many()
.filter(crate::entity::process_definition::Column::TenantId.eq(tenant_id))
.exec(db)
.await?;
tracing::info!(%tenant_id, "Workflow data cleaned up for deleted tenant");
Ok(())
}
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor {
code: "workflow.create".into(),
name: "创建流程".into(),
description: "创建流程定义".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.list".into(),
name: "查看流程".into(),
description: "查看流程列表".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.read".into(),
name: "查看流程详情".into(),
description: "查看流程定义详情".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.update".into(),
name: "编辑流程".into(),
description: "编辑流程定义".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.publish".into(),
name: "发布流程".into(),
description: "发布流程定义".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.start".into(),
name: "发起流程".into(),
description: "发起流程实例".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.approve".into(),
name: "审批任务".into(),
description: "审批流程任务".into(),
module: "workflow".into(),
},
PermissionDescriptor {
code: "workflow.delegate".into(),
name: "委派任务".into(),
description: "委派流程任务".into(),
module: "workflow".into(),
},
]
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}