# sally **Repository Path**: leonscript/sally ## Basic Information - **Project Name**: sally - **Description**: 孵化阶段的workflow - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2024-10-30 - **Last Updated**: 2025-09-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Sally v2 一个轻量级的工作流引擎,专注于工作流的使用而非配置。 [![Go Version](https://img.shields.io/badge/Go-1.23.2+-blue.svg)](https://golang.org/) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) ## 📖 背景 国内项目普遍会遇到"工作流"业务,比如请假、项目审核等。 但是这种业务本质上分为两个部分:配置工作流和使用工作流。 配置工作流这部分,只是提到这个词,我们脑中立刻会出现节点、连线这样的场景,这部分业务对于前端和后端来讲都是比较复杂的。 当前市面上主流的workflow框架,都包含了配置工作流功能,功能一个比一个强劲,但是也一个比一个复杂。 研究了多个go语言编写的workflow框架后,我发现了以下问题: 1. 对于只想"使用工作流"的场景,如果引入了开源workflow,需要耗费大量精力在理解"配置工作流"这部分逻辑。 2. 开源workflow框架,大多混淆了“业务”和“BPM引擎”,造成了学习成本的增加!有可能我们学习了很久,只是为了实现消息推送····说到底BPM只是为了实现node的流动而已。 3. BPM引擎在业务中应该处于最底层并提供简单的方式和业务交互,然而市面上的一些开源工具缺乏这种能力···· ## ✨ v2版本新特性 - 🔄 **自动审批策略**:支持相邻策略和任意策略的自动审批 - 📝 **草稿功能**:支持工作流草稿的保存和管理 - 🎯 **智能条件节点**:支持条件类型节点,实现流程分支控制 - 🔗 **点号路径支持**:支持使用点号路径访问嵌套JSON数据(如 `user.profile.level`、`items[0].price`) - 🏷️ **类型化参数**:支持string、int、float、bool等多种数据类型的条件判断 - 🧠 **复杂条件组合**:支持AND/OR逻辑操作符和多种比较操作符(==、!=、>、>=、<、<=、⊇、!⊇) - 👥 **动态审批人**:支持运行时添加和更换审批人 - 📊 **工作流进度**:提供完整的工作流进度查询功能 - 🔔 **事件订阅**:支持工作流事件的实时订阅 - ❌ **工作流撤销**:支持工作流的撤销操作 - ⚡ **高性能路径解析**:使用sonic库优化JSON路径访问性能 ## 🔍 概念介绍 | 概念 | 说明 | |------|------| | **WorkflowTemplate** | 工作流模板,定义了完整的审批流程结构 | | **NodeTemplate** | 节点模板,工作流中的一个步骤,支持审批类型和条件类型 | | **Workflow** | 工作流实例,从模板启动的具体审批流程 | | **Node** | 节点实例,工作流运行时的具体节点 | | **NodeReviewTask** | 审批任务,分配给具体审批人的任务 | | **Hook** | 钩子函数,用于获取审批人和处理业务逻辑 | | **AutoApprovalStrategy** | 自动审批策略,支持相邻(ADJACENT)和任意(ANY)策略 | | **ApprovalStrategy** | 审批策略,支持或批(OR)和与批(AND)策略 | | **BusinessParams** | 业务参数,用于传递业务相关的数据 | | **Draft** | 草稿,未正式启动的工作流 | | **ConditionGroup** | 条件组,包含多个条件表达式和逻辑操作符,用于流程分支判断 | | **Expression** | 条件表达式,定义参数、操作符、值和类型的条件判断规则 | | **LogicalOperator** | 逻辑操作符,支持AND和OR,用于组合多个条件表达式 | ## 🚀 快速开始 ### 安装 ```bash go get gitee.com/leonscript/sally/v2 ``` ### 初始化项目 ```go package main import ( "context" "gitee.com/leonscript/sally/v2" "gitee.com/leonscript/sally/v2/domain/aggregate" ) // 实现Hook接口 type MyHook struct{} // GetReviewerIDs 获取审批人ID列表 func (h *MyHook) GetReviewerIDs(ctx context.Context, roleKind int, reviewerNos []string, formContent string, params aggregate.BusinessParams) (userIDs []string, err error) { // 根据角色类型和审批人编号获取实际的用户ID switch roleKind { case 1: // 直接指定的用户ID return reviewerNos, nil case 2: // 角色类型,需要根据角色编号查询用户 // 这里实现你的业务逻辑 return []string{"user1", "user2"}, nil default: return []string{}, nil } } // Hook接口只需要实现GetReviewerIDs方法 // 其他废弃的Hook方法已移除 func main() { // 连接数据库 db, err := sally.Connect(sally.Config{ Host: "localhost", Port: "3306", User: "root", Password: "password", Schema: "sally", }) if err != nil { panic(err) } // 初始化服务 clerkService, err := sally.Init(db, &MyHook{}) if err != nil { panic(err) } // 现在可以使用clerkService了 _ = clerkService } ``` ### 工作流模板管理 #### 创建工作流模板 ```go // 创建工作流模板 workflowTemplateConfig := aggregate.WorkflowTemplateConfig{ Name: "月度报告审批流程", // 配置自动审批策略 AutoApprovalConfig: aggregate.AutoApprovalConfig{ Enable: true, Strategy: aggregate.AutoApprovalStrategyAdjacent, // 相邻节点自动审批 }, // 配置超时告警时间(单位:小时) TimeoutAlertHours: 24, // 24小时后发送超时告警 // 配置节点模板 NodeTemplateConfigs: []aggregate.NodeTemplateConfig{ { NodeTemplateName: "部门经理审批", NodeTemplateKind: 1, // 审批类型节点 ReviewerConfig: aggregate.NodeTemplateReviewerConfig{ Kind: 2, // 角色类型 Reviewers: []string{"role1"}, // 角色编号 ApprovalStrategy: aggregate.ApprovalStrategyOr, // 或批策略 }, }, { NodeTemplateName: "总经理审批", NodeTemplateKind: 1, // 审批类型节点 ReviewerConfig: aggregate.NodeTemplateReviewerConfig{ Kind: 2, // 角色类型 Reviewers: []string{"role2"}, // 角色编号 ApprovalStrategy: aggregate.ApprovalStrategyAnd, // 与批策略 }, }, }, } // 添加工作流模板 workflowTemplateID, nodeTemplateIDs, err := clerkService.AddWorkflowTemplate(context.Background(), workflowTemplateConfig) if err != nil { // 处理错误 } ``` ### 工作流操作 #### 启动工作流 ```go // 启动工作流 startWorkFlowRequest := aggregate.StartWorkFlowRequest{ BusinessCode: "agenda", // 业务编码 BusinessID: "agenda-001", // 业务ID FormContent: `{"name":"meng"}`, // 表单内容 SponsorID: "admin", // 发起人ID WorkflowTemplateID: "MonthlyReport", // 工作流模板ID BusinessParams: map[string]any{ // 业务参数 "department": "技术部", "amount": 5000, }, // 可选:设置全局条件参数 GlobalConditionParams: map[string]any{ "level": "high", }, } // 启动工作流 err = clerkService.StartWorkflow(context.Background(), startWorkFlowRequest) if err != nil { // 处理错误 } ``` #### 保存工作流草稿 ```go // 保存工作流草稿 err = clerkService.SaveWorkflowDraft(context.Background(), startWorkFlowRequest) if err != nil { // 处理错误 } ``` #### 重启工作流 ```go // 重启工作流 restartWorkFlowRequest := aggregate.RestartWorkFlowRequest{ BusinessCode: "agenda", BusinessID: "agenda-001", FormContent: `{"name":"fu"}`, SponsorID: "admin", WorkflowID: workflowID, BusinessParams: map[string]any{ "department": "技术部", }, } err := clerkService.RestartWorkflow(context.Background(), restartWorkFlowRequest) if err != nil { // 处理错误 } ``` #### 审批 ```go // 审批通过 reviewRequest := aggregate.ReviewRequest{ Comment: "同意申请", NodeReviewerTaskID: "0192f5ca-0ad9-7ad9-b7d6-5e7f3a0eab53", // 审批任务ID Status: aggregate.ReviewPassed, // 通过 } err = clerkService.Review(context.Background(), reviewRequest) if err != nil { // 处理错误 } // 审批拒绝 rejectRequest := aggregate.ReviewRequest{ Comment: "预算超出限额", NodeReviewerTaskID: "0192f5ca-0ad9-7ad9-b7d6-5e7f3a0eab53", Status: aggregate.ReviewRejected, // 拒绝 } err = clerkService.Review(context.Background(), rejectRequest) if err != nil { // 处理错误 } ``` #### 撤销工作流 ```go // 撤销工作流 err = clerkService.CancelWorkflow(context.Background(), workflowID) if err != nil { // 处理错误 } ``` ### 查询功能 #### 查询工作流列表 ```go request := aggregate.GetWorkflowsRequest{ SponsorID: "admin", BusinessCode: "agenda", BusinessID: "agenda-001", PageCond: aggregate.PageCond{ PageNo: 1, PageSize: 10, }, CommonSearchCond: aggregate.CommonSearchCond{ Search: "meng", }, } result, total, err := clerkService.GetWorkflows(context.Background(), request) if err != nil { // 处理错误 } ``` #### 查询单个工作流 ```go result, err := clerkService.GetWorkflow(context.Background(), workflowID) if err != nil { // 处理错误 } ``` #### 查询审批任务 ```go request := aggregate.GetNodeReviewTasksRequest{ ReviewerID: "admin", BusinessCode: "agenda", BusinessID: "agenda-001", PageCond: aggregate.PageCond{ PageNo: 1, PageSize: 10, }, CommonSearchCond: aggregate.CommonSearchCond{ Search: "meng", }, } result, total, err := clerkService.GetNodeReviewTasks(context.Background(), request) if err != nil { // 处理错误 } ``` #### 查询工作流进度 ```go progress, err := clerkService.GetWorkflowProgress(context.Background(), workflowID) if err != nil { // 处理错误 } ``` #### 查询工作流模板 ```go // 查询所有工作流模板 templates, err := clerkService.GetWorkflowTemplates(context.Background()) if err != nil { // 处理错误 } // 查询特定工作流模板 template, err := clerkService.GetWorkflowTemplate(context.Background(), "MonthlyReport") if err != nil { // 处理错误 } ``` #### 查询工作流草稿 ```go drafts, err := clerkService.GetWorkflowDrafts(context.Background(), "admin") if err != nil { // 处理错误 } ``` ### 其他操作 #### 更新工作流内容 ```go // 更新工作流表单内容 err = clerkService.UpdateWorkflowContent(context.Background(), workflowID, `{"name":"updated", "amount": 8000}`) if err != nil { // 处理错误 } ``` #### 删除工作流 ```go // 删除工作流 err = clerkService.RemoveWorkflow(context.Background(), workflowID) if err != nil { // 处理错误 } ``` #### 任务管理 ```go // 添加审批人到现有任务 err = clerkService.AddNodeReviewTaskReviewer(context.Background(), nodeReviewTaskID, []string{"user1", "user2"}) if err != nil { // 处理错误 } // 更改任务审批人 err = clerkService.ChangeNodeReviewTaskReviewer(context.Background(), nodeReviewTaskID, []string{"user3", "user4"}) if err != nil { // 处理错误 } ``` #### 工作流模板管理 ```go // 删除工作流模板 err = clerkService.RemoveWorkflowTemplate(context.Background(), workflowTemplateID) if err != nil { // 处理错误 } // 更新工作流模板 updatedConfig := aggregate.WorkflowTemplateConfig{ Name: "更新后的月度报告审批流程", AutoApprovalConfig: aggregate.AutoApprovalConfig{ Enable: false, // 禁用自动审批 Strategy: aggregate.AutoApprovalStrategyAny, }, TimeoutAlertHours: 48, // 48小时后发送超时告警 // ... 其他配置 } err = clerkService.UpdateWorkflowTemplate(context.Background(), workflowTemplateID, updatedConfig) if err != nil { // 处理错误 } ``` ## 高级功能 ### 事件订阅与Hook机制 Sally v2 提供了强大的事件订阅机制,允许你在工作流的关键节点执行自定义逻辑: ```go type MyAdvancedHook struct { // 可以注入其他服务 notificationService NotificationService auditService AuditService } // 获取动态审批人 func (h *MyAdvancedHook) GetReviewerIDs(ctx context.Context, businessCode, businessID string, nodeTemplateName string, businessParams aggregate.BusinessParams) ([]string, error) { // 根据业务参数动态确定审批人 if amount, ok := businessParams["amount"].(float64); ok { if amount > 10000 { // 金额超过1万需要总经理审批 return []string{"ceo"}, nil } } return []string{"manager"}, nil } // 工作流通过后的处理 func (h *MyAdvancedHook) HandlePassedWorkflow(ctx context.Context, workflowID string, businessCode, businessID string, businessParams aggregate.BusinessParams) error { // 发送通知 return h.notificationService.SendApprovalNotification(businessID, "approved") } // 工作流拒绝后的处理 func (h *MyAdvancedHook) HandleRejectedWorkflow(ctx context.Context, workflowID string, businessCode, businessID string, businessParams aggregate.BusinessParams) error { // 记录审计日志 return h.auditService.LogRejection(businessID, "workflow_rejected") } // 节点通过后的处理 func (h *MyAdvancedHook) HandlePassedNode(ctx context.Context, nodeID string, businessCode, businessID string, businessParams aggregate.BusinessParams) error { // 节点级别的处理逻辑 return nil } // 节点拒绝后的处理 func (h *MyAdvancedHook) HandleRejectedNode(ctx context.Context, nodeID string, businessCode, businessID string, businessParams aggregate.BusinessParams) error { // 节点级别的拒绝处理 return nil } // 创建审批任务后的处理 func (h *MyAdvancedHook) HandleCrearedReviewTasks(ctx context.Context, nodeReviewTaskIDs []string, businessCode, businessID string, businessParams aggregate.BusinessParams) error { // 发送待办通知 return h.notificationService.SendPendingTaskNotification(nodeReviewTaskIDs) } ``` ### 条件节点与智能路由 Sally v2 支持强大的条件节点功能,可以根据表单数据和业务参数智能路由工作流: #### 基础条件判断 ```go // 创建包含条件判断的工作流模板 workflowTemplateConfig := aggregate.WorkflowTemplateConfig{ Name: "费用报销审批流程", NodeTemplateConfigs: []aggregate.NodeTemplateConfig{ { NodeTemplateID: "condition-node", NodeTemplateName: "条件判断节点", NodeTemplateKind: entity.NodeTemplateKindCondition, ReviewerConfig: aggregate.NodeTemplateReviewerConfig{ ConditionGroups: []aggregate.ConditionGroup{ { // 高额费用:金额>=10000 AND 部门=财务部 -> 总监审批 Conditions: []aggregate.Expression{ {Param: "amount", Operator: ">=", Value: 10000, Type: "int"}, {Param: "department", Operator: "==", Value: "财务部", Type: "string"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "director-approval", }, { // 紧急申请:urgent=true OR priority=high -> 快速审批 Conditions: []aggregate.Expression{ {Param: "urgent", Operator: "==", Value: true, Type: "bool"}, {Param: "priority", Operator: "==", Value: "high", Type: "string"}, }, LogicalOperator: aggregate.LogicalOperatorOr, NextNodeTemplateID: "fast-approval", }, { // 小额费用:amount<1000 -> 自动审批 Conditions: []aggregate.Expression{ {Param: "amount", Operator: "<", Value: 1000, Type: "int"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "end-node", }, }, }, }, // 其他节点配置... }, } ``` #### 点号路径支持 Sally v2 支持使用点号路径访问嵌套的JSON数据: ```go // 支持复杂嵌套数据的条件判断 ConditionGroups: []aggregate.ConditionGroup{ { // 嵌套对象访问:user.profile.level=VIP -> VIP审批 Conditions: []aggregate.Expression{ {Param: "user.profile.level", Operator: "==", Value: "VIP", Type: "string"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "vip-approval", }, { // 数组访问:request.items[0].price>1000 -> 高价值审批 Conditions: []aggregate.Expression{ {Param: "request.items[0].price", Operator: ">", Value: 1000, Type: "float"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "high-value-approval", }, { // 复杂嵌套:config.settings.approval.autoApprove=true -> 自动审批 Conditions: []aggregate.Expression{ {Param: "config.settings.approval.autoApprove", Operator: "==", Value: true, Type: "bool"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "auto-approval", }, } ``` #### 支持的数据类型 - `string`: 字符串类型 - `int`: 整数类型 - `float`: 浮点数类型 - `bool`: 布尔类型 #### 支持的操作符 - `==`: 等于 - `!=`: 不等于 - `>`: 大于 - `>=`: 大于等于 - `<`: 小于 - `<=`: 小于等于 - `⊇`: 包含(字符串包含) - `!⊇`: 不包含 #### 复杂条件组合 ```go // 复杂条件组合示例 ConditionGroups: []aggregate.ConditionGroup{ { // 条件组1:VIP客户 OR 金额>50000 -> VIP审批 Conditions: []aggregate.Expression{ {Param: "customerLevel", Operator: "==", Value: "VIP", Type: "string"}, {Param: "amount", Operator: ">", Value: 50000, Type: "int"}, }, LogicalOperator: aggregate.LogicalOperatorOr, NextNodeTemplateID: "vip-approval", }, { // 条件组2:部门包含"财务" AND 类型="预算" -> 财务审批 Conditions: []aggregate.Expression{ {Param: "department", Operator: "⊇", Value: "财务", Type: "string"}, {Param: "type", Operator: "==", Value: "预算", Type: "string"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "finance-approval", }, { // 条件组3:环境不包含"test" AND 不包含"dev" -> 生产审批 Conditions: []aggregate.Expression{ {Param: "environment", Operator: "!⊇", Value: "test", Type: "string"}, {Param: "environment", Operator: "!⊇", Value: "dev", Type: "string"}, }, LogicalOperator: aggregate.LogicalOperatorAnd, NextNodeTemplateID: "production-approval", }, } ``` #### 启动带条件的工作流 ```go // 启动工作流时提供复杂的表单数据 startWorkFlowRequest := aggregate.StartWorkFlowRequest{ BusinessCode: "expense", BusinessID: "expense-001", // 支持复杂嵌套的JSON数据 FormContent: `{ "amount": 15000, "department": "财务部", "user": { "profile": { "level": "VIP" } }, "request": { "items": [ {"price": 1500, "name": "设备"} ] }, "config": { "settings": { "approval": { "autoApprove": false } } } }`, SponsorID: "user001", WorkflowTemplateID: workflowTemplateID, BusinessParams: map[string]any{ "category": "equipment", }, } err := clerkService.StartWorkflow(context.Background(), startWorkFlowRequest) ``` #### 在Hook中使用业务参数 ```go // 在Hook中根据条件动态路由 func (h *MyHook) GetReviewerIDs(ctx context.Context, businessCode, businessID string, nodeTemplateName string, businessParams aggregate.BusinessParams) ([]string, error) { switch nodeTemplateName { case "部门审批": dept := businessParams.GetString("department") if dept == "技术部" { return []string{"tech_manager"}, nil } return []string{"general_manager"}, nil case "金额审批": amount := businessParams.GetFloat64("amount") if amount > 50000 { return []string{"ceo", "cfo"}, nil // 需要CEO和CFO同时审批 } else if amount > 10000 { return []string{"finance_manager"}, nil } return []string{"team_lead"}, nil default: return []string{"default_reviewer"}, nil } } ``` ### 自动审批策略 Sally v2 支持多种自动审批策略: ```go // 相邻节点自动审批:当相邻节点的审批人相同时自动通过 autoConfig := aggregate.AutoApprovalConfig{ Enable: true, Strategy: aggregate.AutoApprovalStrategyAdjacent, } // 任意节点自动审批:当任意已审批节点的审批人相同时自动通过 autoConfig := aggregate.AutoApprovalConfig{ Enable: true, Strategy: aggregate.AutoApprovalStrategyAny, } ``` ### 草稿功能 支持保存工作流草稿,允许用户稍后继续编辑: ```go // 保存草稿 draftRequest := aggregate.StartWorkFlowRequest{ BusinessCode: "expense", BusinessID: "exp-draft-001", FormContent: `{"amount": 5000, "description": "差旅费"}`, SponsorID: "user123", WorkflowTemplateID: "ExpenseApproval", } err := clerkService.SaveWorkflowDraft(context.Background(), draftRequest) if err != nil { // 处理错误 } // 查询用户的草稿 drafts, err := clerkService.GetWorkflowDrafts(context.Background(), "user123") if err != nil { // 处理错误 } // 从草稿启动工作流 for _, draft := range drafts { if draft.BusinessID == "exp-draft-001" { err := clerkService.StartWorkflow(context.Background(), draft.ToStartRequest()) if err != nil { // 处理错误 } break } } ``` ## 最佳实践 ### 1. 错误处理 ```go // 始终检查错误 if err := clerkService.StartWorkflow(ctx, request); err != nil { // 记录日志 log.Printf("启动工作流失败: %v", err) // 返回用户友好的错误信息 return fmt.Errorf("工作流启动失败,请稍后重试") } ``` ### 2. 事务处理 ```go // 在数据库事务中使用Sally tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 执行业务逻辑 if err := businessLogic(tx); err != nil { tx.Rollback() return err } // 启动工作流 if err := clerkService.StartWorkflow(ctx, request); err != nil { tx.Rollback() return err } tx.Commit() ``` ### 3. 性能优化 ```go // 使用连接池 dbConfig := aggregate.DBConfig{ MaxOpenConns: 100, MaxIdleConns: 10, // 其他配置... } // 批量查询 requests := []aggregate.GetNodeReviewTasksRequest{ {ReviewerID: "user1"}, {ReviewerID: "user2"}, } // 并发处理 var wg sync.WaitGroup for _, req := range requests { wg.Add(1) go func(r aggregate.GetNodeReviewTasksRequest) { defer wg.Done() tasks, _, err := clerkService.GetNodeReviewTasks(ctx, r) // 处理结果 }(req) } wg.Wait() ``` ## 贡献 欢迎提交 Issue 和 Pull Request 来改进 Sally。 ## 许可证 MIT License