From ae0cc0b60c327d643ddadfd13676d7bcc6bb4a98 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 27 Nov 2021 17:30:20 +0800 Subject: [PATCH] =?UTF-8?q?[add]Workflow=20=E5=A4=84=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- workflow/process.go | 119 +++++++++++++++++++++ workflow/process_test.go | 222 +++++++++++++++++++++++++++++++++++++++ workflow/workflow.go | 24 +++-- 3 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 workflow/process.go create mode 100644 workflow/process_test.go diff --git a/workflow/process.go b/workflow/process.go new file mode 100644 index 00000000..c9b28d4e --- /dev/null +++ b/workflow/process.go @@ -0,0 +1,119 @@ +package workflow + +import "github.com/yaoapp/gou" + +// Process +// 读取工作流 xiang.workflow.Find(name, workflow_id) +// 读取工作流 xiang.workflow.Open(name, uid, data_id) +// 保存工作流 xiang.workflow.Save(name, uid, node_name, data_id, input, ...output) +// 进入下一个节点 xiang.workflow.Next(name, uid, workflow_id, output) +// 跳转到指定节点 xiang.workflow.Goto(name, uid, workflow_id, node_name, output) +// 更新工作流状态 xiang.workflow.Status(name, uid, workflow_id, status_name, output) +// 标记结束流程 xiang.workflow.Done(name, uid, workflow_id, output) +// 标记关闭流程 xiang.workflow.Close(name, uid, workflow_id, output) +// 标记重置流程 xiang.workflow.Reset(name, uid, workflow_id, output) + +func init() { + // 注册处理器 + gou.RegisterProcessHandler("xiang.workflow.Find", ProcessFind) + gou.RegisterProcessHandler("xiang.workflow.Open", ProcessOpen) + gou.RegisterProcessHandler("xiang.workflow.Save", ProcessSave) + gou.RegisterProcessHandler("xiang.workflow.Next", ProcessNext) + gou.RegisterProcessHandler("xiang.workflow.Goto", ProcessGoto) + gou.RegisterProcessHandler("xiang.workflow.Status", ProcessStatus) + gou.RegisterProcessHandler("xiang.workflow.Done", ProcessDone) + gou.RegisterProcessHandler("xiang.workflow.Close", ProcessClose) + gou.RegisterProcessHandler("xiang.workflow.Reset", ProcessReset) +} + +// ProcessFind xiang.workflow.Find 读取工作流 +// args: [工作流名称*, 工作流ID*] +// return: map[string]interface{} 工作流数据记录 +func ProcessFind(process *gou.Process) interface{} { + process.ValidateArgNums(2) + wflow := Select(process.ArgsString(0)) + return wflow.Find(process.ArgsInt(1)) +} + +// ProcessOpen xiang.workflow.Open 读取工作流 +// args: [工作流名称*, 当前用户ID*, 关联数据ID*] +// return: map[string]interface{} 工作流数据记录 +func ProcessOpen(process *gou.Process) interface{} { + process.ValidateArgNums(3) + wflow := Select(process.ArgsString(0)) + return wflow.Open(process.ArgsInt(1), process.Args[2]) +} + +// ProcessSave xiang.workflow.Save 保存工作流节点信息 +// args: [工作流名称*, 当前用户ID*, 节点名称*, 关联数据ID*, 输入数据*, 输出数据] (输入数据: {"data":{}, "form":{}} data 关联数据记录信息, form 工作流body表单信息, 输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessSave(process *gou.Process) interface{} { + process.ValidateArgNums(5) + wflow := Select(process.ArgsString(0)) + input := InputOf(process.ArgsMap(4)) + output := map[string]interface{}{} + if process.NumOfArgsIs(6) { + output = process.ArgsMap(5) + } + return wflow.Save(process.ArgsInt(1), process.ArgsString(2), process.Args[3], input, output) +} + +// ProcessNext xiang.workflow.Next 进入下一个节点 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessNext(process *gou.Process) interface{} { + process.ValidateArgNums(4) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(3) + return wflow.Next(process.ArgsInt(1), process.ArgsInt(2), output) +} + +// ProcessGoto xiang.workflow.Goto 跳转到指定节点 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 节点名称*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessGoto(process *gou.Process) interface{} { + process.ValidateArgNums(5) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(4) + return wflow.Goto(process.ArgsInt(1), process.ArgsInt(2), process.ArgsString(3), output) +} + +// ProcessStatus xiang.workflow.Status 更新工作流状态 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 状态名称*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessStatus(process *gou.Process) interface{} { + process.ValidateArgNums(5) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(4) + return wflow.Status(process.ArgsInt(1), process.ArgsInt(2), process.ArgsString(3), output) +} + +// ProcessDone xiang.workflow.Done 标记结束流程 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessDone(process *gou.Process) interface{} { + process.ValidateArgNums(4) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(3) + return wflow.Done(process.ArgsInt(1), process.ArgsInt(2), output) +} + +// ProcessClose xiang.workflow.Close 标记关闭流程 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessClose(process *gou.Process) interface{} { + process.ValidateArgNums(4) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(3) + return wflow.Close(process.ArgsInt(1), process.ArgsInt(2), output) +} + +// ProcessReset xiang.workflow.Reset 标记重置流程 +// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} ) +// return: map[string]interface{} 工作流数据记录 +func ProcessReset(process *gou.Process) interface{} { + process.ValidateArgNums(4) + wflow := Select(process.ArgsString(0)) + output := process.ArgsMap(3) + return wflow.Reset(process.ArgsInt(1), process.ArgsInt(2), output) +} diff --git a/workflow/process_test.go b/workflow/process_test.go new file mode 100644 index 00000000..a4bd81bc --- /dev/null +++ b/workflow/process_test.go @@ -0,0 +1,222 @@ +package workflow + +import ( + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/xiang/config" + "github.com/yaoapp/xiang/flow" + "github.com/yaoapp/xiang/model" + "github.com/yaoapp/xiang/query" + "github.com/yaoapp/xiang/share" + "github.com/yaoapp/xun/capsule" +) + +func init() { + share.DBConnect(config.Conf.Database) + share.Load(config.Conf) + model.Load(config.Conf) + engineModels := path.Join(config.Conf.Source, "xiang", "models") + model.LoadFrom(engineModels, "xiang.") + query.Load(config.Conf) + flow.Load(config.Conf) + Load(config.Conf) +} + +func TestProcessFind(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + + args := []interface{}{"assign", wflow["id"]} + res := gou.NewProcess("xiang.workflow.Find", args...).Run() + + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessOpen(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + + args := []interface{}{"assign", 1, 1} + res := gou.NewProcess("xiang.workflow.Open", args...).Run() + + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessSave(t *testing.T) { + args := []interface{}{"assign", + 1, "选择商务负责人", 1, + map[string]interface{}{ + "data": map[string]interface{}{"id": 1, "name": "云主机"}, + "form": map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }, + map[string]interface{}{"foo": "bar"}, + } + res := gou.NewProcess("xiang.workflow.Save", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, int64(1), data.Get("id")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessNext(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + + args := []interface{}{"assign", + 1, wflow["id"], map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }, + } + res := gou.NewProcess("xiang.workflow.Next", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "项目负责人审批", data.Get("node_name")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessGoto(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + id := any.Of(wflow["id"]).CInt() + assignFlow.Next(1, id, map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + args := []interface{}{"assign", + 2, id, "选择商务负责人", + map[string]interface{}{"审批结果": "驳回"}, + } + res := gou.NewProcess("xiang.workflow.Goto", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "选择商务负责人", data.Get("node_name")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessStatus(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + id := any.Of(wflow["id"]).CInt() + assignFlow.Next(1, id, map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + args := []interface{}{"assign", + 2, id, "已完成", map[string]interface{}{"关闭原因": "测试完成"}, + } + res := gou.NewProcess("xiang.workflow.Status", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "已完成", data.Get("status")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessDone(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + id := any.Of(wflow["id"]).CInt() + assignFlow.Next(1, id, map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + args := []interface{}{"assign", + 2, id, map[string]interface{}{"关闭原因": "测试完成"}, + } + res := gou.NewProcess("xiang.workflow.Done", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "已完成", data.Get("status")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessClose(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + id := any.Of(wflow["id"]).CInt() + assignFlow.Next(1, id, map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + args := []interface{}{"assign", + 2, id, map[string]interface{}{"关闭原因": "测试关闭"}, + } + res := gou.NewProcess("xiang.workflow.Close", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "已关闭", data.Get("status")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestProcessReset(t *testing.T) { + assignFlow := Select("assign") + wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云主机"}, + Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, + }) + id := any.Of(wflow["id"]).CInt() + assignFlow.Next(1, id, map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + args := []interface{}{"assign", + 2, id, map[string]interface{}{"驳回原因": "字段填写错误"}, + } + res := gou.NewProcess("xiang.workflow.Reset", args...).Run() + data := any.Of(res).Map().MapStrAny.Dot() + assert.Equal(t, wflow["id"], data.Get("id")) + assert.Equal(t, "进行中", data.Get("status")) + assert.Equal(t, "选择商务负责人", data.Get("node_name")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} diff --git a/workflow/workflow.go b/workflow/workflow.go index b81e6a41..ee2eaf99 100644 --- a/workflow/workflow.go +++ b/workflow/workflow.go @@ -17,19 +17,19 @@ import ( ) // Process -// 读取工作流 xiang.workflow.Open(uid, data_id) // 读取工作流 xiang.workflow.Find(workflow_id) +// 读取工作流 xiang.workflow.Open(uid, data_id) // 保存工作流 xiang.workflow.Save(uid, node_name, data_id, input, ...output) // 进入下一个节点 xiang.workflow.Next(uid, workflow_id, output) // 跳转到指定节点 xiang.workflow.Goto(uid, workflow_id, node_name, output) -// 更新流程状态 xiang.workflow.Status(uid, workflow_id, status_name, output) -// 结束流程 xiang.workflow.Done(uid, workflow_id, output) -// 关闭流程 xiang.workflow.Close(uid, workflow_id, output) -// 重置流程 xiang.workflow.Reset(uid, workflow_id, output) +// 更新工作流状态 xiang.workflow.Status(uid, workflow_id, status_name, output) +// 标记结束流程 xiang.workflow.Done(uid, workflow_id, output) +// 标记关闭流程 xiang.workflow.Close(uid, workflow_id, output) +// 标记重置流程 xiang.workflow.Reset(uid, workflow_id, output) // API: -// 读取工作流 GET /api/xiang/workflow/<工作流名称>/open // 读取工作流 GET /api/xiang/workflow/<工作流名称>/find/:id +// 读取工作流 GET /api/xiang/workflow/<工作流名称>/open // 读取工作流配置 GET /api/xiang/workflow/<工作流名称>/setting // 调用自定义API POST /api/xiang/workflow/<工作流名称>/<自定义API路由> @@ -435,3 +435,15 @@ func (workflow *WorkFlow) IsLastNode(name string) bool { func (workflow *WorkFlow) Len() int { return len(workflow.Nodes) } + +// InputOf 映射表转换为Input +func InputOf(in map[string]interface{}) Input { + input := Input{Data: map[string]interface{}{}, Form: map[string]interface{}{}} + if data, ok := in["data"].(map[string]interface{}); ok { + input.Data = data + } + if form, ok := in["form"].(map[string]interface{}); ok { + input.Form = form + } + return input +}