diff --git a/config/config.go b/config/config.go index 06389054..468063db 100644 --- a/config/config.go +++ b/config/config.go @@ -164,6 +164,10 @@ func (cfg *Config) SetDefaults() { cfg.RootPage = cfg.Root + "/pages" } + if cfg.RootWorkFlow == "" { + cfg.RootWorkFlow = cfg.Root + "/workflows" + } + if cfg.RootData == "" { cfg.RootData = cfg.Root + "/data" } @@ -240,6 +244,7 @@ func SetAppPath(root string, envfile ...string) { Conf.RootTable = filepath.Join(fullpath, "/tables") Conf.RootChart = filepath.Join(fullpath, "/charts") Conf.RootPage = filepath.Join(fullpath, "/pages") + Conf.RootWorkFlow = filepath.Join(fullpath, "/workflows") Conf.RootData = filepath.Join(fullpath, "/data") Conf.RootUI = filepath.Join(fullpath, "/ui") Conf.RootDB = filepath.Join(fullpath, "/db") diff --git a/tests/libs/assign.json b/tests/libs/assign.json index 7dcf8dad..81e0fcfb 100644 --- a/tests/libs/assign.json +++ b/tests/libs/assign.json @@ -1,30 +1,51 @@ { "保存": { - "label": "保存", - "api": "/api/xiang/workflow/assign/save" + "type": "button", + "props": { + "label": "保存", + "api": "/api/xiang/workflow/assign/save" + } }, "提交": { - "label": "提交", - "api": "/api/xiang/workflow/assign/submit" + "type": "button", + "props": { + "label": "提交", + "api": "/api/xiang/workflow/assign/submit" + } }, "同意": { - "label": "同意", - "api": "/api/xiang/workflow/assign/resolve" + "type": "button", + "props": { + "label": "同意", + "api": "/api/xiang/workflow/assign/resolve" + } }, "驳回并重填": { - "label": "驳回并重填", - "api": "/api/xiang/workflow/assign/reject/reset" + "type": "button", + "props": { + "label": "驳回并重填", + "api": "/api/xiang/workflow/assign/reject/reset" + } }, "驳回并关闭": { - "label": "驳回并关闭", - "api": "/api/xiang/workflow/assign/reject/reset" + "type": "button", + "props": { + "label": "驳回并关闭", + "api": "/api/xiang/workflow/assign/reject/reset" + } }, "审批同意": { - "label": "审批同意", - "api": "/api/xiang/workflow/assign/terminal" + "type": "button", + "props": { + "label": "审批同意", + "api": "/api/xiang/workflow/assign/terminal" + } }, "关闭": { - "label": "关闭", - "api": "/api/xiang/workflow/terminal" + "type": "button", + "props": { + "label": "关闭", + "api": "/api/xiang/workflow/terminal" + } } } diff --git a/workflow/types.go b/workflow/types.go index ccc6646b..341288da 100644 --- a/workflow/types.go +++ b/workflow/types.go @@ -4,13 +4,14 @@ import "github.com/yaoapp/xiang/share" // WorkFlow 工作流配置结构 type WorkFlow struct { - Name string `json:"-"` - Source string `json:"-"` - Version string `json:"version"` - Label string `json:"label,omitempty"` - Decription string `json:"decription,omitempty"` - Nodes []Node `json:"nodes"` - APIs []API `json:"apis"` + Name string `json:"-"` + Source []byte `json:"-"` + Version string `json:"version"` + Label string `json:"label,omitempty"` + Decription string `json:"decription,omitempty"` + Nodes []Node `json:"nodes"` + APIs map[string]API `json:"apis"` + Actions map[string]share.Render `json:"actions"` } // Node 工作流节点 @@ -21,7 +22,7 @@ type Node struct { User User `json:"user,omitempty"` } -// User 工作相关用户读取条件 +// User 工作流相关用户读取条件 type User struct { Process string `json:"process"` Args []interface{} `json:"args"` @@ -33,3 +34,9 @@ type API struct { Process string `json:"process"` Args []interface{} `json:"args"` } + +// Input 用户输入数据 +type Input struct { + Data map[string]interface{} `json:"data"` // 记录数据 + Form map[string]interface{} `json:"form"` // 表单数据 +} diff --git a/workflow/workflow.go b/workflow/workflow.go index e94d68bf..6ba6225e 100644 --- a/workflow/workflow.go +++ b/workflow/workflow.go @@ -1,6 +1,15 @@ package workflow -import "github.com/yaoapp/xiang/config" +import ( + "fmt" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/xiang/config" + "github.com/yaoapp/xiang/share" + "github.com/yaoapp/xiang/xlog" +) // WorkFlows 工作流列表 var WorkFlows = map[string]*WorkFlow{} @@ -11,11 +20,57 @@ func Load(cfg config.Config) { } // LoadFrom 从特定目录加载 -func LoadFrom(dir string, prefix string) {} +func LoadFrom(dir string, prefix string) { + if share.DirNotExists(dir) { + return + } + share.Walk(dir, ".json", func(root, filename string) { + name := prefix + share.SpecName(root, filename) + content := share.ReadFile(filename) + _, err := LoadWorkFlow(content, name) + if err != nil { + exception.New("%s 工作流格式错误", 400, name).Ctx(filename).Throw() + } + }) +} + +// LoadWorkFlow 载入工作流 +func LoadWorkFlow(source []byte, name string) (*WorkFlow, error) { + workflow := WorkFlow{Name: name, Source: source} + err := jsoniter.Unmarshal(source, &workflow) + if err != nil { + xlog.Println(name) + xlog.Println(err.Error()) + xlog.Println(string(source)) + return nil, err + } + WorkFlows[workflow.Name] = &workflow + return WorkFlows[workflow.Name], nil +} // Select 读取已加载图表 func Select(name string) *WorkFlow { - return WorkFlows[name] + workflow, has := WorkFlows[name] + if !has { + exception.New( + fmt.Sprintf("工作流:%s; 尚未加载", name), + 400, + ).Throw() + } + return workflow +} + +// Reload 重新载入工作流 +func (workflow *WorkFlow) Reload() *WorkFlow { + new, err := LoadWorkFlow(workflow.Source, workflow.Name) + if err != nil { + exception.New( + fmt.Sprintf("工作流:%s; 加载失败", workflow.Name), + 400, + ).Throw() + } + WorkFlows[workflow.Name] = new + return new } // Process @@ -36,10 +91,60 @@ func (workflow *WorkFlow) Setting(id int) {} func (workflow *WorkFlow) SetupAPIs(id int) {} // Get 读取当前工作流(未完成的) -func (workflow *WorkFlow) Get(uid int, name string, dataID interface{}) {} +func (workflow *WorkFlow) Get(uid int, name string, id interface{}) map[string]interface{} { + wflow := gou.Select("xiang.workflow") + params := gou.QueryParam{ + Select: []interface{}{"*"}, + Wheres: []gou.QueryWhere{ + {Column: "name", Value: workflow.Name}, + {Column: "data_id", Value: id}, + {Column: "user_id", Value: uid}, + {Column: "status", Value: "进行中"}, + }, + } + rows := wflow.MustGet(params) + if len(rows) > 0 { + return rows[0] + } + return map[string]interface{}{ + "name": workflow.Name, + "data_id": id, + "node_name": name, + "user_id": uid, + "status": "进行中", + "node_status": "进行中", + } +} // Save 保存工作流节点数据 -func (workflow *WorkFlow) Save(uid int, name string, node string, dataID interface{}, input map[string]interface{}) { +func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input) map[string]interface{} { + wflow := gou.Select("xiang.workflow") + params := gou.QueryParam{ + Select: []interface{}{"id"}, + Wheres: []gou.QueryWhere{ + {Column: "name", Value: workflow.Name}, + {Column: "data_id", Value: id}, + {Column: "user_id", Value: uid}, + {Column: "status", Value: "进行中"}, + }, + } + rows := wflow.MustGet(params) + data := map[string]interface{}{ + "name": workflow.Name, + "data_id": id, + "node_name": name, + "user_id": uid, + "input": input, + } + if len(rows) > 0 { + data["id"] = rows[0].Get("id") + } else { + data["status"] = "进行中" + data["node_status"] = "进行中" + } + + id = wflow.MustSave(data) + return wflow.MustFind(id, gou.QueryParam{}) } // Next 下一个工作流 diff --git a/workflow/workflow_test.go b/workflow/workflow_test.go new file mode 100644 index 00000000..deca2b6b --- /dev/null +++ b/workflow/workflow_test.go @@ -0,0 +1,88 @@ +package workflow + +import ( + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/xiang/config" + "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) + Load(config.Conf) +} + +func TestLoad(t *testing.T) { + share.DBConnect(config.Conf.Database) + share.Load(config.Conf) + model.Load(config.Conf) + query.Load(config.Conf) + Load(config.Conf) + LoadFrom("not a path", "404.") + check(t) +} + +func TestSave(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": "张良明"}, + }) + data := maps.Of(wflow).Dot() + assert.Equal(t, int64(1), data.Get("id")) + assert.Equal(t, "选择商务负责人", data.Get("node_name")) + assert.Equal(t, "进行中", data.Get("node_status")) + assert.Equal(t, "进行中", data.Get("status")) + assert.Equal(t, float64(1), data.Get("input.data.id")) + assert.Equal(t, "云主机", data.Get("input.data.name")) + assert.Equal(t, float64(1), data.Get("input.form.biz_id")) + assert.Equal(t, "张良明", data.Get("input.form.name")) + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func TestSaveUpdate(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.Save(1, "选择商务负责人", 1, Input{ + Data: map[string]interface{}{"id": 1, "name": "云存储"}, + Form: map[string]interface{}{"biz_id": 1, "name": "李明博"}, + }) + + data := maps.Of(wflow).Dot() + assert.Equal(t, int64(1), data.Get("id")) + assert.Equal(t, "选择商务负责人", data.Get("node_name")) + assert.Equal(t, "进行中", data.Get("node_status")) + assert.Equal(t, "进行中", data.Get("status")) + assert.Equal(t, float64(1), data.Get("input.data.id")) + assert.Equal(t, "云存储", data.Get("input.data.name")) + assert.Equal(t, float64(1), data.Get("input.form.biz_id")) + assert.Equal(t, "李明博", data.Get("input.form.name")) + + // 清理数据 + capsule.Query().From("xiang_workflow").Truncate() +} + +func check(t *testing.T) { + keys := []string{} + for key, workflow := range WorkFlows { + keys = append(keys, key) + workflow.Reload() + } + assert.Equal(t, 1, len(keys)) +}