diff --git a/tests/flows/select/manager.flow.json b/tests/flows/select/manager.flow.json index 73e990a2ec..38004b58be 100644 --- a/tests/flows/select/manager.flow.json +++ b/tests/flows/select/manager.flow.json @@ -15,5 +15,5 @@ "orders": [":RAND()"] } ], - "output": "{{$res.负责人.id}}" + "output": "{{$in.0}}" } diff --git a/tests/workflows/assign.wflow.json b/tests/workflows/assign.wflow.json index c37020334c..5d94a53f76 100644 --- a/tests/workflows/assign.wflow.json +++ b/tests/workflows/assign.wflow.json @@ -53,15 +53,15 @@ "props": { "src": "/workflows/assign/approve.html" } }, "actions": ["驳回并重填", "驳回并关闭", "同意"], - "user": { "process": "flows.select.manager", "args": ["id"] } + "user": { "process": "flows.select.manager", "args": [2] } }, { "name": "审批通过", - "when": [{ "审批通过": "{{$data.审批状态}}", "=": "通过" }], + "when": [{ "审批通过": "{{$out.审批结果}}", "=": "通过" }], "body": { "type": "markdown", "props": { - "content": "审批通过, {{$data.项目名称}}商务负责人为: **{{$data.商务负责人名称}}**" + "content": "审批通过, {{$out.项目名称}}商务负责人为: **{{$out.商务负责人名称}}**" } }, "actions": ["关闭"], @@ -69,11 +69,11 @@ }, { "name": "审批驳回", - "when": [{ "审批通过": "{{$data.审批状态}}", "=": "驳回" }], + "when": [{ "审批通过": "{{$out.审批结果}}", "=": "驳回" }], "body": { "type": "markdown", "props": { - "content": "您的请求被驳回, 驳回原因: **{{$data.驳回原因}}**" + "content": "您的请求被驳回, 驳回原因: **{{$out.驳回原因}}**" } }, "actions": ["关闭"], diff --git a/workflow/types.go b/workflow/types.go index 341288da7c..4e00d38a43 100644 --- a/workflow/types.go +++ b/workflow/types.go @@ -1,6 +1,9 @@ package workflow -import "github.com/yaoapp/xiang/share" +import ( + "github.com/yaoapp/xiang/helper" + "github.com/yaoapp/xiang/share" +) // WorkFlow 工作流配置结构 type WorkFlow struct { @@ -16,10 +19,11 @@ type WorkFlow struct { // Node 工作流节点 type Node struct { - Name string `json:"name"` - Body share.Render `json:"body,omitempty"` - Actions []string `json:"actions,omitempty"` - User User `json:"user,omitempty"` + Name string `json:"name"` + Body share.Render `json:"body,omitempty"` + Conditions []helper.Condition `json:"when,omitempty"` + Actions []string `json:"actions,omitempty"` + User User `json:"user,omitempty"` } // User 工作流相关用户读取条件 diff --git a/workflow/workflow.go b/workflow/workflow.go index 60ff5b2319..a32c7cf457 100644 --- a/workflow/workflow.go +++ b/workflow/workflow.go @@ -6,7 +6,10 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou" + gshare "github.com/yaoapp/gou/query/share" + "github.com/yaoapp/kun/any" "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/maps" "github.com/yaoapp/xiang/config" "github.com/yaoapp/xiang/helper" "github.com/yaoapp/xiang/share" @@ -76,13 +79,15 @@ func (workflow *WorkFlow) Reload() *WorkFlow { } // Process -// 读取工作流 xiang.workflow.Get(uid, name, data_id) +// 读取工作流 xiang.workflow.Open(uid, name, data_id) +// 读取工作流 xiang.workflow.Find(id) // 保存工作流 xiang.workflow.Save(uid, name, node, input) // 进入下一个节点 xiang.workflow.Next(uid, id, input) // 跳转到指定节点 xiang.workflow.Goto(uid, id, node, input) // API: -// 读取工作流 GET /api/xiang/workflow/<工作流名称>/get +// 读取工作流 GET /api/xiang/workflow/<工作流名称>/open +// 读取工作流 GET /api/xiang/workflow/<工作流名称>/find/:id // 读取工作流配置 GET /api/xiang/workflow/<工作流名称>/setting // 调用自定义API POST /api/xiang/workflow/<工作流名称>/<自定义API路由> @@ -92,12 +97,28 @@ func (workflow *WorkFlow) Setting(id int) {} // SetupAPIs 注册API func (workflow *WorkFlow) SetupAPIs(id int) {} -// Get 读取当前工作流(未完成的) -func (workflow *WorkFlow) Get(uid int, id interface{}) map[string]interface{} { +// Find 读取给定ID的工作流 +// uid 当前处理人ID, id 数据ID +func (workflow *WorkFlow) Find(id int) map[string]interface{} { + wflow := gou.Select("xiang.workflow") + res := wflow.MustFind(id, gou.QueryParam{ + Select: []interface{}{ + "data_id", "id", "input", "output", "name", + "user_id", "users", + "node_name", "node_status", + "status", + "updated_at", "created_at", + }}) + return res +} + +// Open 读取当前工作流(未完成的) +// uid 当前处理人ID, id 数据ID +func (workflow *WorkFlow) Open(uid int, id interface{}) map[string]interface{} { wflow := gou.Select("xiang.workflow") params := gou.QueryParam{ Select: []interface{}{ - "data_id", "id", "input", "name", + "data_id", "id", "input", "output", "name", "user_id", "users", "node_name", "node_status", "status", @@ -109,6 +130,7 @@ func (workflow *WorkFlow) Get(uid int, id interface{}) map[string]interface{} { {Column: "user_ids", OP: "like", Value: fmt.Sprintf("%%|%d|%%", uid)}, {Column: "status", Value: "进行中"}, }, + Limit: 1, } rows := wflow.MustGet(params) if len(rows) > 0 { @@ -123,14 +145,16 @@ func (workflow *WorkFlow) Get(uid int, id interface{}) map[string]interface{} { "status": "进行中", "node_status": "进行中", "input": map[string]interface{}{}, + "output": map[string]interface{}{}, } } // Save 保存工作流节点数据 此版本使用Like实现 -func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input) map[string]interface{} { +// uid 当前处理人ID, id 数据ID +func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input, outputs ...map[string]interface{}) map[string]interface{} { wflow := gou.Select("xiang.workflow") params := gou.QueryParam{ - Select: []interface{}{"id", "input", "users"}, + Select: []interface{}{"id", "input", "output", "users"}, Wheres: []gou.QueryWhere{ {Column: "name", Value: workflow.Name}, {Column: "data_id", Value: id}, @@ -148,9 +172,15 @@ func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input "user_id": uid, } users := []interface{}{uid} + output := map[string]interface{}{} + nodeInput := map[string]interface{}{} + nodeInput[name] = input + if len(outputs) > 0 { + output = outputs[0] + } if len(rows) > 0 { - nodeInput := map[string]interface{}{} if history, ok := rows[0].Get("input").(map[string]interface{}); ok { + history[name] = input nodeInput = history } if last, ok := rows[0].Get("users").([]interface{}); ok { @@ -158,30 +188,138 @@ func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input users = append(users, uid) users = helper.ArrayUnique(users) } - nodeInput[name] = input + if out, ok := rows[0].Get("output").(map[string]interface{}); ok { + for k, v := range output { + out[k] = v + } + output = out + } data["id"] = rows[0].Get("id") - data["input"] = nodeInput - data["users"] = users } else { - nodeInput := map[string]interface{}{} - nodeInput[name] = input data["status"] = "进行中" data["node_status"] = "进行中" - data["input"] = nodeInput - data["users"] = users } userIDs := []string{} for _, u := range users { userIDs = append(userIDs, fmt.Sprintf("|%d|", u)) } + data["users"] = users data["user_ids"] = strings.Join(userIDs, ",") + data["input"] = nodeInput + data["output"] = output id = wflow.MustSave(data) return wflow.MustFind(id, gou.QueryParam{}) } +// Status 标记工作流状态 +// uid 当前处理人ID, id 工作流ID +func (workflow *WorkFlow) Status(uid int, id int, output map[string]interface{}) { +} + // Next 下一个工作流 -func (workflow *WorkFlow) Next(uid int, id int, input map[string]interface{}) {} +// uid 当前处理人ID, id 工作流ID +func (workflow *WorkFlow) Next(uid int, id int, output map[string]interface{}) map[string]interface{} { + wflow := workflow.Find(id) + currNode, ok := wflow["node_name"].(string) + if !ok { + exception.New("流程数据异常: 当前节点信息错误", 500).Ctx(currNode).Throw() + } + + // 合并数据输出 + if out, ok := wflow["output"].(map[string]interface{}); ok { + for key, value := range output { + out[key] = value + } + output = out + } + + // 读取下一个节点 + data := map[string]interface{}{ + "$in": wflow["input"], + "$input": wflow["input"], + "$out": output, + "$outupt": output, + } + nextNode := workflow.nextNode(currNode, data) + nextUID := nextNode.GetUID() + + // 读取关联用户数据 + users := []interface{}{uid} + userIDs := []string{} + if last, ok := wflow["users"].([]interface{}); ok { + users = last + users = append(users, nextUID, uid) + users = helper.ArrayUnique(users) + } + for _, u := range users { + userIDs = append(userIDs, fmt.Sprintf("|%d|", u)) + } + + // 更新数据 + mod := gou.Select("xiang.workflow") + mod.Save(map[string]interface{}{ + "id": wflow["id"], + "output": wflow["output"], + "node_name": nextNode.Name, + "node_status": "进行中", + "user_id": nextUID, + "users": users, + "user_ids": strings.Join(userIDs, ","), + }) + return workflow.Find(id) +} + +// GetUID 根据条件选择节点处理人 +func (node *Node) GetUID() int { + res := gou.NewProcess(node.User.Process, node.User.Args...).Run() + return any.Of(res).CInt() +} + +// nextNode 查找下一个节点 +func (workflow *WorkFlow) nextNode(currentNode string, data map[string]interface{}) *Node { + next := -1 + for i, node := range workflow.Nodes { + if node.Name == currentNode { + next = i + 1 + break + } + } + if next < 0 { + exception.New("流程数据异常: 未找到工作流节点", 500).Ctx(currentNode).Throw() + } + data = maps.Of(data).Dot() + for i := next; i < workflow.Len(); i++ { + node := workflow.Nodes[i] + if node.Conditions == nil || len(node.Conditions) == 0 { + return &node + } + // 替换数据中的变量 + conditions := []helper.Condition{} + for _, cond := range node.Conditions { + if left, ok := cond.Left.(string); ok { + cond.Left = gshare.Bind(left, data) + } + if right, ok := cond.Right.(string); ok { + cond.Right = gshare.Bind(right, data) + } + conditions = append(conditions, cond) + } + if helper.When(conditions) { + return &node + } + } + + exception.New("流程数据异常: 未找到符合条件的工作流节点", 500).Ctx(currentNode).Throw() + return nil +} + +func (workflow *WorkFlow) isLastNode() {} // Goto 工作流跳转 -func (workflow *WorkFlow) Goto(uid int, id int, node string, input map[string]interface{}) {} +func (workflow *WorkFlow) Goto(uid int, id int, node string, output map[string]interface{}) {} + +// Len 节点数量 +func (workflow *WorkFlow) Len() int { + return len(workflow.Nodes) +} diff --git a/workflow/workflow_test.go b/workflow/workflow_test.go index badb8d23cd..dfd7d13b42 100644 --- a/workflow/workflow_test.go +++ b/workflow/workflow_test.go @@ -5,8 +5,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/yaoapp/kun/any" "github.com/yaoapp/kun/maps" "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" @@ -20,6 +22,7 @@ func init() { engineModels := path.Join(config.Conf.Source, "xiang", "models") model.LoadFrom(engineModels, "xiang.") query.Load(config.Conf) + flow.Load(config.Conf) Load(config.Conf) } @@ -81,13 +84,13 @@ func TestSaveUpdate(t *testing.T) { capsule.Query().From("xiang_workflow").Truncate() } -func TestGet(t *testing.T) { +func TestOpen(t *testing.T) { assignFlow := Select("assign") assignFlow.Save(1, "选择商务负责人", 1, Input{ Data: map[string]interface{}{"id": 1, "name": "云主机"}, Form: map[string]interface{}{"biz_id": 1, "name": "张良明"}, }) - wflow := assignFlow.Get(1, 1) + wflow := assignFlow.Open(1, 1) data := maps.Of(wflow).Dot() assert.Equal(t, int64(1), data.Get("id")) assert.Equal(t, "assign", data.Get("name")) @@ -101,9 +104,9 @@ func TestGet(t *testing.T) { capsule.Query().From("xiang_workflow").Truncate() } -func TestGetEmpty(t *testing.T) { +func TestOpenEmpty(t *testing.T) { assignFlow := Select("assign") - wflow := assignFlow.Get(1, 1) + wflow := assignFlow.Open(1, 1) data := maps.Of(wflow).Dot() assert.Equal(t, false, data.Has("id")) assert.Equal(t, "assign", data.Get("name")) @@ -114,6 +117,61 @@ func TestGetEmpty(t *testing.T) { assert.Equal(t, []interface{}{1}, data.Get("users")) } +func TestNext(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": "张良明"}, + }) + wflow = assignFlow.Next(1, any.Of(wflow["id"]).CInt(), map[string]interface{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + data := maps.Of(wflow).Dot() + assert.Equal(t, int64(1), data.Get("id")) + assert.Equal(t, "assign", data.Get("name")) + assert.Equal(t, "项目负责人审批", data.Get("node_name")) + assert.Equal(t, "进行中", data.Get("node_status")) + assert.Equal(t, "进行中", data.Get("status")) + assert.Equal(t, int64(2), data.Get("user_id")) + assert.Equal(t, true, data.Has("users")) + assert.Equal(t, "测试项目", data.Get("output.项目名称")) + assert.Equal(t, "林明波", data.Get("output.商务负责人名称")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestNextWhen(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{}{ + "项目名称": "测试项目", + "商务负责人名称": "林明波", + }) + + // Next When + wflow = assignFlow.Next(2, id, map[string]interface{}{"审批结果": "通过"}) + data := maps.Of(wflow).Dot() + assert.Equal(t, int64(1), data.Get("id")) + assert.Equal(t, "assign", data.Get("name")) + assert.Equal(t, "审批通过", data.Get("node_name")) + assert.Equal(t, "进行中", data.Get("node_status")) + assert.Equal(t, "进行中", data.Get("status")) + assert.Equal(t, int64(1), data.Get("user_id")) + assert.Equal(t, true, data.Has("users")) + assert.Equal(t, "测试项目", data.Get("output.项目名称")) + assert.Equal(t, "林明波", data.Get("output.商务负责人名称")) + assert.Equal(t, "通过", data.Get("output.审批结果")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + func check(t *testing.T) { keys := []string{} for key, workflow := range WorkFlows {