This commit is contained in:
Max 2021-11-27 11:25:00 +08:00
parent ffd2ea4320
commit 72b1108ec5
5 changed files with 232 additions and 32 deletions

View file

@ -15,5 +15,5 @@
"orders": [":RAND()"]
}
],
"output": "{{$res.负责人.id}}"
"output": "{{$in.0}}"
}

View file

@ -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": ["关闭"],

View file

@ -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 工作流相关用户读取条件

View file

@ -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)
}

View file

@ -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 {