├── .dockerignore ├── images ├── go.jpg └── processConfig.png ├── Dockerfile ├── .gitignore ├── workflow-engine ├── model │ ├── ProcdefHistory.go │ ├── redis_test.go │ ├── TaskHistory.go │ ├── ExecutionHistory.go │ ├── IdentitylinkHistory.go │ ├── redis.go │ ├── ACT_RU_TASK.go │ ├── ACT_RU_EXECUTION.go │ ├── ACT_RE_PROCDEF.go │ ├── ACT_RU_IDENTITYLINK.go │ ├── database.go │ ├── ProcInstHistory.go │ └── ACT_HI_PROCINST.go ├── service │ ├── IdentitylinkHistoryService.go │ ├── cronJobService.go │ ├── redisService.go │ ├── procInstHistoryService.go │ ├── executionService.go │ ├── identitylinkService.go │ ├── procdefService.go │ ├── procInstService.go │ └── taskService.go └── flow │ ├── node_test.go │ └── node.go ├── config.json ├── workflow-controller ├── index.go ├── identitylinkController.go ├── IdentitylinkHistoryController.go ├── procdefController.go ├── ProcHistoryController.go ├── taskController.go └── processController.go ├── main.go ├── workflow-config └── config.go ├── k8s.yaml ├── README.md ├── processConfig2.json ├── ProcessConfig流程定义配置详解.md ├── EXAMPLE.md ├── workflow-router └── router.go └── processConfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | go-workflow.exe 2 | server.crt 3 | server.key -------------------------------------------------------------------------------- /images/go.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/go-workflow/master/images/go.jpg -------------------------------------------------------------------------------- /images/processConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/go-workflow/master/images/processConfig.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD /go-workflow // 3 | ADD /config.json // 4 | EXPOSE 8080 5 | ENTRYPOINT [ "/go-workflow" ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | go-workflow.exe 2 | go-workflow 3 | 测试 4 | STAR.fznews.com.cn.crt 5 | STAR.fznews.com.cn.key 6 | server.crt 7 | server.key -------------------------------------------------------------------------------- /workflow-engine/model/ProcdefHistory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // ProcdefHistory 历史流程定义 4 | type ProcdefHistory struct { 5 | Procdef 6 | } 7 | 8 | // Save Save 9 | func (p *ProcdefHistory) Save() (ID int, err error) { 10 | err = db.Create(p).Error 11 | if err != nil { 12 | return 0, err 13 | } 14 | return p.ID, nil 15 | } 16 | -------------------------------------------------------------------------------- /workflow-engine/model/redis_test.go: -------------------------------------------------------------------------------- 1 | package model_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestClient(t *testing.T) { 8 | // err := model.RedisClient.Set("key", "value", 0).Err() 9 | // if err != nil { 10 | // panic(err) 11 | // } 12 | // val, err := model.RedisClient.Get("key").Result() 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // fmt.Println("key:", val) 17 | } 18 | -------------------------------------------------------------------------------- /workflow-engine/model/TaskHistory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // TaskHistory TaskHistory 8 | type TaskHistory struct { 9 | Task 10 | } 11 | 12 | // CopyTaskToHistoryByProInstID CopyTaskToHistoryByProInstID 13 | // 根据procInstID查询结果,并将结果复制到task_history表 14 | func CopyTaskToHistoryByProInstID(procInstID int, tx *gorm.DB) error { 15 | return tx.Exec("insert into task_history select * from task where proc_inst_id=?", procInstID).Error 16 | } 17 | -------------------------------------------------------------------------------- /workflow-engine/model/ExecutionHistory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // ExecutionHistory ExecutionHistory 8 | // 执行流历史纪录 9 | type ExecutionHistory struct { 10 | Execution 11 | } 12 | 13 | // CopyExecutionToHistoryByProcInstIDTx CopyExecutionToHistoryByProcInstIDTx 14 | func CopyExecutionToHistoryByProcInstIDTx(procInstID int, tx *gorm.DB) error { 15 | return tx.Exec("insert into execution_history select * from execution where proc_inst_id=?", procInstID).Error 16 | } 17 | -------------------------------------------------------------------------------- /workflow-engine/service/IdentitylinkHistoryService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-workflow/go-workflow/workflow-engine/model" 5 | "github.com/mumushuiding/util" 6 | ) 7 | 8 | // FindParticipantHistoryByProcInstID 历史纪录查询 9 | func FindParticipantHistoryByProcInstID(procInstID int) (string, error) { 10 | datas, err := model.FindParticipantHistoryByProcInstID(procInstID) 11 | if err != nil { 12 | return "", err 13 | } 14 | str, err := util.ToJSONStr(datas) 15 | if err != nil { 16 | return "", err 17 | } 18 | return str, nil 19 | } 20 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Port": "8080", 3 | "ReadTimeout": "5", 4 | "WriteTimeout": "5", 5 | "DbLogMode": "true", 6 | "DbType": "mysql", 7 | "DbName": "activiti", 8 | "DbHost": "localhost", 9 | "DbPort": "3306", 10 | "DbUser": "root", 11 | "DbPassword": "123", 12 | "DbMaxIdleConns": "5", 13 | "DbMaxOpenConns": "100", 14 | "RedisCluster": "false", 15 | "RedisHost": "localhost", 16 | "RedisPort": "6379", 17 | "RedisPassword": "", 18 | "TLSOpen": "false", 19 | "TLSCrt": "server.crt", 20 | "TLSKey": "server.key", 21 | "AccessControlAllowOrigin": "*", 22 | "AccessControlAllowHeaders": "*", 23 | "AccessControlAllowMethods": "POST, GET, PUT, OPTIONS, DELETE, PATCH" 24 | } -------------------------------------------------------------------------------- /workflow-controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Index 首页 10 | func Index(writer http.ResponseWriter, request *http.Request) { 11 | writer.Header().Set("Access-Control-Allow-Origin", "*") 12 | fmt.Fprintf(writer, "Hello world!") 13 | } 14 | 15 | // GetToken 获取token 16 | func GetToken(request *http.Request) (string, error) { 17 | token := request.Header.Get("Authorization") 18 | if len(token) == 0 { 19 | request.ParseForm() 20 | if len(request.Form["token"]) == 0 { 21 | return "", errors.New("header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 22 | } 23 | token = request.Form["token"][0] 24 | } 25 | return token,nil 26 | } 27 | -------------------------------------------------------------------------------- /workflow-engine/model/IdentitylinkHistory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // IdentitylinkHistory IdentitylinkHistory 8 | type IdentitylinkHistory struct { 9 | Identitylink 10 | } 11 | 12 | // CopyIdentitylinkToHistoryByProcInstID CopyIdentitylinkToHistoryByProcInstID 13 | func CopyIdentitylinkToHistoryByProcInstID(procInstID int, tx *gorm.DB) error { 14 | return tx.Exec("insert into identitylink_history select * from identitylink where proc_inst_id=?", procInstID).Error 15 | } 16 | 17 | // FindParticipantHistoryByProcInstID FindParticipantHistoryByProcInstID 18 | func FindParticipantHistoryByProcInstID(procInstID int) ([]*IdentitylinkHistory, error) { 19 | var datas []*IdentitylinkHistory 20 | err := db.Select("id,user_id,step,comment").Where("proc_inst_id=? and type=?", procInstID, IdentityTypes[PARTICIPANT]).Order("id asc").Find(&datas).Error 21 | return datas, err 22 | } 23 | -------------------------------------------------------------------------------- /workflow-controller/identitylinkController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/service" 9 | "github.com/mumushuiding/util" 10 | ) 11 | 12 | // FindParticipantByProcInstID 根据流程id查询流程参与者 13 | func FindParticipantByProcInstID(writer http.ResponseWriter, request *http.Request) { 14 | if request.Method != "GET" { 15 | util.ResponseErr(writer, "只支持get方法!!") 16 | return 17 | } 18 | request.ParseForm() 19 | if len(request.Form["procInstID"]) == 0 { 20 | util.ResponseErr(writer, "流程 procInstID 不能为空") 21 | return 22 | } 23 | procInstID, err := strconv.Atoi(request.Form["procInstID"][0]) 24 | if err != nil { 25 | util.ResponseErr(writer, err) 26 | return 27 | } 28 | result, err := service.FindParticipantByProcInstID(procInstID) 29 | if err != nil { 30 | util.ResponseErr(writer, err) 31 | return 32 | } 33 | fmt.Fprintf(writer, result) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /workflow-controller/IdentitylinkHistoryController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/service" 9 | 10 | "github.com/mumushuiding/util" 11 | ) 12 | 13 | // FindParticipantHistoryByProcInstID 历史纪录查询 14 | func FindParticipantHistoryByProcInstID(writer http.ResponseWriter, request *http.Request) { 15 | if request.Method != "GET" { 16 | util.ResponseErr(writer, "只支持get方法!!") 17 | return 18 | } 19 | request.ParseForm() 20 | if len(request.Form["procInstID"]) == 0 { 21 | util.ResponseErr(writer, "流程 procInstID 不能为空") 22 | return 23 | } 24 | procInstID, err := strconv.Atoi(request.Form["procInstID"][0]) 25 | if err != nil { 26 | util.ResponseErr(writer, err) 27 | return 28 | } 29 | result, err := service.FindParticipantHistoryByProcInstID(procInstID) 30 | if err != nil { 31 | util.ResponseErr(writer, err) 32 | return 33 | } 34 | fmt.Fprintf(writer, result) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /workflow-engine/service/cronJobService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/robfig/cron" 7 | ) 8 | 9 | // CronJobs CronJobs 10 | // 所有定时任务,在启动时会执行 11 | func CronJobs() { 12 | go func() { 13 | c := cron.New() 14 | // 每隔 20 秒执行 15 | spec := "*/20 * * * * ?" 16 | //每隔20秒将已经结束的流程数据迁移至历史数据表 17 | c.AddFunc(spec, func() { 18 | MoveFinishedProcInstToHistory() 19 | }) 20 | // c.AddFunc("*/5 * * * * ?", func() { 21 | // log.Println("cron running") 22 | // }) 23 | // 启动 24 | c.Start() 25 | log.Println("----------启动定时任务------------") 26 | defer c.Stop() 27 | select {} 28 | }() 29 | // c := cron.New() 30 | // // 每天0点执行 31 | // spec := "0 0 0 * * ?" 32 | // //每天0点时将已经结束的流程数据迁移至历史数据表 33 | // c.AddFunc(spec, func() { 34 | // MoveFinishedProcInstToHistory() 35 | // }) 36 | // c.AddFunc("*/5 * * * * ?", func() { 37 | // log.Println("cron running") 38 | // }) 39 | // // 启动 40 | // c.Start() 41 | // log.Println("----------启动定时任务------------") 42 | // defer c.Stop() 43 | // select {} 44 | } 45 | -------------------------------------------------------------------------------- /workflow-engine/service/redisService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/mumushuiding/util" 5 | 6 | "github.com/go-workflow/go-workflow/workflow-engine/model" 7 | ) 8 | 9 | // UserInfo 用户信息 10 | type UserInfo struct { 11 | Company string `json:"company"` 12 | // 用户所属部门 13 | Department string `json:"department"` 14 | Username string `json:"username"` 15 | ID string `json:"ID"` 16 | // 用户的角色 17 | Roles []string `json:"roles"` 18 | // 用户负责的部门 19 | Departments []string `json:"departments"` 20 | } 21 | 22 | // GetUserinfoFromRedis GetUserinfoFromRedis 23 | func GetUserinfoFromRedis(token string) (*UserInfo, error) { 24 | result, err := GetValFromRedis(token) 25 | if err != nil { 26 | return nil, err 27 | } 28 | // fmt.Println(result) 29 | var userinfo = &UserInfo{} 30 | err = util.Str2Struct(result, userinfo) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return userinfo, nil 35 | } 36 | 37 | // GetValFromRedis 从redis获取值 38 | func GetValFromRedis(key string) (string, error) { 39 | return model.RedisGetVal(key) 40 | } 41 | -------------------------------------------------------------------------------- /workflow-engine/flow/node_test.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // func TestSome(t *testing.T) { 9 | // var step int 10 | // fmt.Println(step) 11 | // step++ 12 | // fmt.Println(step) 13 | // } 14 | 15 | func TestNode(t *testing.T) { 16 | for k, v := range NodeTypes { 17 | fmt.Printf("key,value: %d,%s\n", k, v) 18 | } 19 | } 20 | 21 | // func TestNodeGenerateNodeInfos(t *testing.T) { 22 | // var node = Node{} 23 | // node.GetProcessConfigFromJSONFile() 24 | // // result, _ := util.ToJSONStr(node) 25 | // // fmt.Println(result) 26 | // maps := make(map[string]string) 27 | // maps["DDHolidayField-J2BWEN12__duration"] = "8" 28 | // maps["DDHolidayField-J2BWEN12__options"] = "年假" 29 | // list, err := ParseProcessConfig(&node, &maps) 30 | // if err != nil { 31 | // log.Printf("err:%v", err) 32 | // } 33 | // str, _ := util.ToJSONStr(util.List2Array(list)) 34 | // fmt.Println(str) 35 | // } 36 | 37 | // func BenchmarkTest(b *testing.B) { 38 | // for i := 0; i < b.N; i++ { 39 | // var node = Node{} 40 | // node.GetProcessConfigFromJSONFile() 41 | // // result, _ := util.ToJSONStr(node) 42 | // // fmt.Println(result) 43 | // maps := make(map[string]string) 44 | // maps["DDHolidayField-J2BWEN12__duration"] = "8" 45 | // maps["DDHolidayField-J2BWEN12__options"] = "年假" 46 | // list, err := ParseProcessConfig(&node, &maps) 47 | // if err != nil { 48 | // log.Printf("err:%v", err) 49 | // } 50 | // str, _ := util.ToJSONStr(util.List2Array(list)) 51 | // fmt.Println(str) 52 | // } 53 | // } 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | router "github.com/go-workflow/go-workflow/workflow-router" 11 | 12 | config "github.com/go-workflow/go-workflow/workflow-config" 13 | 14 | model "github.com/go-workflow/go-workflow/workflow-engine/model" 15 | "github.com/go-workflow/go-workflow/workflow-engine/service" 16 | ) 17 | 18 | // 配置 19 | var conf = *config.Config 20 | 21 | func crossOrigin(h http.HandlerFunc) http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Set("Access-Control-Allow-Origin", conf.AccessControlAllowOrigin) 24 | w.Header().Set("Access-Control-Allow-Methods", conf.AccessControlAllowMethods) 25 | w.Header().Set("Access-Control-Allow-Headers", conf.AccessControlAllowHeaders) 26 | h(w, r) 27 | } 28 | } 29 | func main() { 30 | mux := router.Mux 31 | // 启动数据库连接 32 | model.Setup() 33 | // 启动redis连接 34 | model.SetRedis() 35 | // 启动定时任务 36 | service.CronJobs() 37 | // 启动服务 38 | readTimeout, err := strconv.Atoi(conf.ReadTimeout) 39 | if err != nil { 40 | panic(err) 41 | } 42 | writeTimeout, err := strconv.Atoi(conf.WriteTimeout) 43 | if err != nil { 44 | panic(err) 45 | } 46 | server := &http.Server{ 47 | Addr: fmt.Sprintf(":%s", conf.Port), 48 | Handler: mux, 49 | ReadTimeout: time.Duration(readTimeout * int(time.Second)), 50 | WriteTimeout: time.Duration(writeTimeout * int(time.Second)), 51 | MaxHeaderBytes: 1 << 20, 52 | } 53 | log.Printf("the application start up at port%s", server.Addr) 54 | if conf.TLSOpen == "true" { 55 | err = server.ListenAndServeTLS(conf.TLSCrt, conf.TLSKey) 56 | } else { 57 | err = server.ListenAndServe() 58 | } 59 | if err != nil { 60 | log.Printf("Server err: %v", err) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /workflow-engine/model/redis.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | redis "github.com/go-redis/redis" 8 | ) 9 | 10 | var redisClusterClient *redis.ClusterClient 11 | var redisClient *redis.Client 12 | var clusterIsOpen = false 13 | 14 | // RedisOpen 是否连接 redis 15 | var RedisOpen = false 16 | 17 | // SetRedis 设置redis 18 | func SetRedis() { 19 | fmt.Println("-------启动redis--------") 20 | if conf.RedisCluster == "true" { 21 | clusterIsOpen = true 22 | redisClusterClient = redis.NewClusterClient(&redis.ClusterOptions{ 23 | Addrs: []string{conf.RedisHost + ":" + conf.RedisPort}, 24 | Password: conf.RedisPassword, 25 | }) 26 | pong, err := redisClusterClient.Ping().Result() 27 | if err != nil { 28 | fmt.Printf("------------连接 redis cluster:%s 失败,原因:%v\n", conf.RedisHost+":"+conf.RedisPort, err) 29 | } 30 | RedisOpen = true 31 | fmt.Printf("---------连接 redis cluster 成功, %v\n", pong) 32 | } else { 33 | redisClient = redis.NewClient(&redis.Options{ 34 | Addr: conf.RedisHost + ":" + conf.RedisPort, 35 | Password: conf.RedisPassword, 36 | }) 37 | pong, err := redisClient.Ping().Result() 38 | if err != nil { 39 | fmt.Printf("------------连接 redis:%s 失败,原因:%v\n", conf.RedisHost+":"+conf.RedisPort, err) 40 | } 41 | RedisOpen = true 42 | fmt.Printf("---------连接 redis 成功, %v\n", pong) 43 | } 44 | } 45 | 46 | // RedisSetVal 将值保存到redis 47 | func RedisSetVal(key, value string, expiration time.Duration) error { 48 | if clusterIsOpen { 49 | return redisClusterClient.Set(key, value, expiration).Err() 50 | } 51 | return redisClient.Set(key, value, expiration).Err() 52 | } 53 | 54 | // RedisGetVal 从redis获取值 55 | func RedisGetVal(key string) (string, error) { 56 | if clusterIsOpen { 57 | return redisClusterClient.Get(key).Result() 58 | } 59 | return redisClient.Get(key).Result() 60 | } 61 | -------------------------------------------------------------------------------- /workflow-config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/mumushuiding/util" 9 | ) 10 | 11 | // Configuration 数据库配置结构 12 | type Configuration struct { 13 | Port string 14 | ReadTimeout string 15 | WriteTimeout string 16 | // 数据库设置 17 | DbLogMode string 18 | DbType string 19 | DbName string 20 | DbHost string 21 | DbPort string 22 | DbUser string 23 | DbPassword string 24 | DbMaxIdleConns string 25 | DbMaxOpenConns string 26 | // redis 设置 27 | RedisCluster string 28 | RedisHost string 29 | RedisPort string 30 | RedisPassword string 31 | TLSOpen string 32 | TLSCrt string 33 | TLSKey string 34 | // 跨域设置 35 | AccessControlAllowOrigin string 36 | AccessControlAllowHeaders string 37 | AccessControlAllowMethods string 38 | } 39 | 40 | // Config 数据库配置 41 | var Config = &Configuration{} 42 | 43 | func init() { 44 | LoadConfig() 45 | } 46 | 47 | // LoadConfig LoadConfig 48 | func LoadConfig() { 49 | // 获取配置信息config 50 | Config.getConf() 51 | // 环境变量覆盖config 52 | err := Config.setFromEnv() 53 | if err != nil { 54 | panic(err) 55 | } 56 | // 打印配置信息 57 | config, _ := json.Marshal(&Config) 58 | log.Printf("configuration:%s", string(config)) 59 | } 60 | func (c *Configuration) setFromEnv() error { 61 | fieldStream, err := util.GetFieldChannelFromStruct(&Configuration{}) 62 | if err != nil { 63 | return err 64 | } 65 | for fieldname := range fieldStream { 66 | if len(os.Getenv(fieldname)) > 0 { 67 | err = util.StructSetValByReflect(c, fieldname, os.Getenv(fieldname)) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | return nil 74 | } 75 | func (c *Configuration) getConf() *Configuration { 76 | file, err := os.Open("config.json") 77 | if err != nil { 78 | log.Printf("cannot open file config.json:%v", err) 79 | panic(err) 80 | } 81 | decoder := json.NewDecoder(file) 82 | err = decoder.Decode(c) 83 | if err != nil { 84 | log.Printf("decode config.json failed:%v", err) 85 | panic(err) 86 | } 87 | return c 88 | } 89 | -------------------------------------------------------------------------------- /k8s.yaml: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # 工作流引擎 workflow 3 | ################################################## 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: workflow 8 | spec: 9 | selector: 10 | app: workflow 11 | ports: 12 | - port: 8080 13 | name: http 14 | --- 15 | # deployment 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: workflow 20 | spec: 21 | replicas: 2 22 | selector: 23 | matchLabels: 24 | app: workflow 25 | template: 26 | metadata: 27 | labels: 28 | app: workflow 29 | spec: 30 | containers: 31 | - name: workflow 32 | imagePullPolicy: Always 33 | image: registry.cn-hangzhou.aliyuncs.com/mumushuiding/go-workflow 34 | resources: 35 | limits: 36 | memory: "128Mi" 37 | cpu: "500m" 38 | ports: 39 | - containerPort: 8080 40 | env: 41 | - name: DbType 42 | value: "mysql" 43 | - name: DbLogMode 44 | value: "false" 45 | - name: DbName 46 | value: workflow 47 | - name: DbHost 48 | value: localhost 49 | - name: DbUser 50 | value: root 51 | - name: DbPassword 52 | value: "123" 53 | - name: RedisCluster # 是否是redis集群 54 | value: "true" 55 | - name: RedisHost 56 | value: redis-service # redis 集群ip 57 | - name: RedisPort 58 | value: "6379" 59 | - name: RedisPassword 60 | value: "" 61 | --- 62 | apiVersion: networking.istio.io/v1alpha3 63 | kind: Gateway 64 | metadata: 65 | name: workflow 66 | spec: 67 | selector: 68 | istio: ingressgateway 69 | servers: 70 | - port: 71 | number: 80 72 | name: http 73 | protocol: http 74 | hosts: 75 | - workflow.prod.svc.cluster.local 76 | --- 77 | apiVersion: networking.istio.io/v1alpha3 78 | kind: VirtualService 79 | metadata: 80 | name: workflow 81 | spec: 82 | hosts: 83 | - "*" 84 | gateways: 85 | - workflow 86 | http: 87 | - match: 88 | - uri: 89 | prefix: /workflow 90 | route: 91 | - destination: 92 | host: workflow 93 | port: 94 | number: 8080 95 | 96 | -------------------------------------------------------------------------------- /workflow-engine/model/ACT_RU_TASK.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // import _ "github.com/jinzhu/gorm" 8 | 9 | // Task 流程任务表 10 | // ExecutionID 执行流ID 11 | // Name 任务名称,在流程文件中定义 12 | // TaskDefKey 任务定义的ID值 13 | // Assignee 被指派执行该任务的人 14 | // Owner 任务拥有人 15 | type Task struct { 16 | Model 17 | // Company 任务创建人对应的公司 18 | // Company string `json:"company"` 19 | // ExecutionID string `json:"executionID"` 20 | // 当前执行流所在的节点 21 | NodeID string `json:"nodeId"` 22 | Step int `json:"step"` 23 | // 流程实例id 24 | ProcInstID int `json:"procInstID"` 25 | Assignee string `json:"assignee"` 26 | CreateTime string `json:"createTime"` 27 | ClaimTime string `json:"claimTime"` 28 | // 还未审批的用户数,等于0代表会签已经全部审批结束,默认值为1 29 | MemberCount int8 `json:"memberCount" gorm:"default:1"` 30 | UnCompleteNum int8 `json:"unCompleteNum" gorm:"default:1"` 31 | //审批通过数 32 | AgreeNum int8 `json:"agreeNum"` 33 | // and 为会签,or为或签,默认为or 34 | ActType string `json:"actType" gorm:"default:'or'"` 35 | IsFinished bool `gorm:"default:false" json:"isFinished"` 36 | } 37 | 38 | // NewTask 新建任务 39 | func (t *Task) NewTask() (int, error) { 40 | err := db.Create(t).Error 41 | if err != nil { 42 | return 0, err 43 | } 44 | return t.ID, nil 45 | } 46 | 47 | // UpdateTx UpdateTx 48 | func (t *Task) UpdateTx(tx *gorm.DB) error { 49 | err := tx.Model(&Task{}).Updates(t).Error 50 | return err 51 | } 52 | 53 | // GetTaskByID GetTaskById 54 | func GetTaskByID(id int) (*Task, error) { 55 | var t = &Task{} 56 | err := db.Where("id=?", id).Find(t).Error 57 | return t, err 58 | } 59 | 60 | // GetTaskLastByProInstID GetTaskLastByProInstID 61 | // 根据流程实例id获取上一个任务 62 | func GetTaskLastByProInstID(procInstID int) (*Task, error) { 63 | var t = &Task{} 64 | err := db.Where("proc_inst_id=? and is_finished=1", procInstID).Order("claim_time desc").First(t).Error 65 | return t, err 66 | } 67 | 68 | // NewTaskTx begin tx 69 | // 开启事务 70 | func (t *Task) NewTaskTx(tx *gorm.DB) (int, error) { 71 | // str, _ := util.ToJSONStr(t) 72 | // fmt.Printf("newTask:%s", str) 73 | err := tx.Create(t).Error 74 | if err != nil { 75 | return 0, err 76 | } 77 | return t.ID, nil 78 | } 79 | 80 | // DeleteTask 删除任务 81 | func DeleteTask(id int) error { 82 | err := db.Where("id=?", id).Delete(&Task{}).Error 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /workflow-engine/model/ACT_RU_EXECUTION.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | "github.com/mumushuiding/util" 8 | ) 9 | 10 | // Execution 流程实例(执行流)表 11 | // ProcInstID 流程实例ID 12 | // BusinessKey 启动业务时指定的业务主键 13 | // ProcDefID 流程定义数据的ID 14 | type Execution struct { 15 | Model 16 | Rev int `json:"rev"` 17 | ProcInstID int `json:"procInstID"` 18 | ProcDefID int `json:"procDefID"` 19 | ProcDefName string `json:"procDefName"` 20 | // NodeInfos 执行流经过的所有节点 21 | NodeInfos string `gorm:"size:4000" json:"nodeInfos"` 22 | IsActive int8 `json:"isActive"` 23 | StartTime string `json:"startTime"` 24 | } 25 | 26 | // Save save 27 | func (p *Execution) Save() (ID int, err error) { 28 | err = db.Create(p).Error 29 | if err != nil { 30 | return 0, err 31 | } 32 | return p.ID, nil 33 | } 34 | 35 | // SaveTx SaveTx 36 | // 接收外部事务 37 | func (p *Execution) SaveTx(tx *gorm.DB) (ID int, err error) { 38 | p.StartTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 39 | if err := tx.Create(p).Error; err != nil { 40 | return 0, err 41 | } 42 | return p.ID, nil 43 | } 44 | 45 | // GetExecByProcInst GetExecByProcInst 46 | // 根据流程实例id查询执行流 47 | func GetExecByProcInst(procInstID int) (*Execution, error) { 48 | var p = &Execution{} 49 | err := db.Where("proc_inst_id=?", procInstID).Find(p).Error 50 | // log.Printf("procdef:%v,err:%v", p, err) 51 | if err == gorm.ErrRecordNotFound { 52 | return nil, nil 53 | } 54 | if err != nil || p == nil { 55 | return nil, err 56 | } 57 | return p, nil 58 | } 59 | 60 | // GetExecNodeInfosByProcInstID GetExecNodeInfosByProcInstID 61 | // 根据流程实例procInstID查询执行流经过的所有节点信息 62 | func GetExecNodeInfosByProcInstID(procInstID int) (string, error) { 63 | var e = &Execution{} 64 | err := db.Select("node_infos").Where("proc_inst_id=?", procInstID).Find(e).Error 65 | // fmt.Println(e) 66 | if err != nil { 67 | return "", err 68 | } 69 | return e.NodeInfos, nil 70 | } 71 | 72 | // ExistsExecByProcInst ExistsExecByProcInst 73 | // 指定流程实例的执行流是否已经存在 74 | func ExistsExecByProcInst(procInst int) (bool, error) { 75 | e, err := GetExecByProcInst(procInst) 76 | // var p = &Execution{} 77 | // err := db.Where("proc_inst_id=?", procInst).Find(p).RecordNotFound 78 | // log.Printf("errnotfound:%v", err) 79 | if e != nil { 80 | return true, nil 81 | } 82 | if err != nil { 83 | return false, err 84 | } 85 | return false, nil 86 | } 87 | -------------------------------------------------------------------------------- /workflow-engine/service/procInstHistoryService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/go-workflow/go-workflow/workflow-engine/model" 7 | "github.com/mumushuiding/util" 8 | ) 9 | 10 | // FindProcHistory 查询我的审批 11 | func FindProcHistory(receiver *ProcessPageReceiver) (string, error) { 12 | datas, count, err := findAllProcHistory(receiver) 13 | if err != nil { 14 | return "", err 15 | } 16 | return util.ToPageJSON(datas, count, receiver.PageIndex, receiver.PageSize) 17 | } 18 | 19 | // FindProcHistoryByToken 查询我的审批纪录 20 | func FindProcHistoryByToken(token string, receiver *ProcessPageReceiver) (string, error) { 21 | userinfo, err := GetUserinfoFromRedis(token) 22 | if err != nil { 23 | return "", err 24 | } 25 | if len(userinfo.Company) == 0 { 26 | return "", errors.New("保存在redis中的【用户信息 userinfo】字段 company 不能为空") 27 | } 28 | if len(userinfo.ID) == 0 { 29 | return "", errors.New("保存在redis中的【用户信息 userinfo】字段 ID 不能为空") 30 | } 31 | receiver.Company = userinfo.Company 32 | receiver.UserID = userinfo.ID 33 | // receiver.Username = userinfo.Username 34 | return FindProcHistory(receiver) 35 | } 36 | func findAllProcHistory(receiver *ProcessPageReceiver) ([]*model.ProcInstHistory, int, error) { 37 | var page = util.Page{} 38 | page.PageRequest(receiver.PageIndex, receiver.PageSize) 39 | return model.FindProcHistory(receiver.UserID, receiver.Company, receiver.PageIndex, receiver.PageSize) 40 | } 41 | 42 | // DelProcInstHistoryByID DelProcInstHistoryByID 43 | func DelProcInstHistoryByID(id int) error { 44 | return model.DelProcInstHistoryByID(id) 45 | } 46 | 47 | // StartHistoryByMyself 查询我发起的流程 48 | func StartHistoryByMyself(receiver *ProcessPageReceiver) (string, error) { 49 | var page = util.Page{} 50 | page.PageRequest(receiver.PageIndex, receiver.PageSize) 51 | datas, count, err := model.StartHistoryByMyself(receiver.UserID, receiver.Company, receiver.PageIndex, receiver.PageSize) 52 | if err != nil { 53 | return "", err 54 | } 55 | return util.ToPageJSON(datas, count, receiver.PageIndex, receiver.PageSize) 56 | } 57 | 58 | // FindProcHistoryNotify 查询抄送我的流程 59 | func FindProcHistoryNotify(receiver *ProcessPageReceiver) (string, error) { 60 | var page = util.Page{} 61 | page.PageRequest(receiver.PageIndex, receiver.PageSize) 62 | datas, count, err := model.FindProcHistoryNotify(receiver.UserID, receiver.Company, receiver.Groups, receiver.PageIndex, receiver.PageSize) 63 | if err != nil { 64 | return "", err 65 | } 66 | return util.ToPageJSON(datas, count, receiver.PageIndex, receiver.PageSize) 67 | } 68 | -------------------------------------------------------------------------------- /workflow-engine/service/executionService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/jinzhu/gorm" 9 | 10 | "github.com/mumushuiding/util" 11 | 12 | "github.com/go-workflow/go-workflow/workflow-engine/flow" 13 | "github.com/go-workflow/go-workflow/workflow-engine/model" 14 | ) 15 | 16 | var execLock sync.Mutex 17 | 18 | // SaveExecution SaveExecution 19 | func SaveExecution(e *model.Execution) (ID int, err error) { 20 | execLock.Lock() 21 | defer execLock.Unlock() 22 | // check if exists by procInst 23 | yes, err := model.ExistsExecByProcInst(e.ProcInstID) 24 | if err != nil { 25 | return 0, err 26 | } 27 | if yes { 28 | return 0, errors.New("流程实例【" + fmt.Sprintf("%d", e.ProcInstID) + "】已经存在执行流") 29 | } 30 | // save 31 | return e.Save() 32 | } 33 | 34 | // SaveExecTx SaveExecTx 35 | func SaveExecTx(e *model.Execution, tx *gorm.DB) (ID int, err error) { 36 | execLock.Lock() 37 | defer execLock.Unlock() 38 | // check if exists by procInst 39 | yes, err := model.ExistsExecByProcInst(e.ProcInstID) 40 | if err != nil { 41 | return 0, err 42 | } 43 | if yes { 44 | return 0, errors.New("流程实例【" + fmt.Sprintf("%d", e.ProcInstID) + "】已经存在执行流") 45 | } 46 | // save 47 | return e.SaveTx(tx) 48 | } 49 | 50 | // GetExecByProcInst GetExecByProcInst 51 | // 根据流程实例查询执行流 52 | func GetExecByProcInst(procInst int) (*model.Execution, error) { 53 | return model.GetExecByProcInst(procInst) 54 | } 55 | 56 | // GenerateExec GenerateExec 57 | // 根据流程定义node生成执行流 58 | func GenerateExec(e *model.Execution, node *flow.Node, userID string, variable *map[string]string, tx *gorm.DB) (int, error) { 59 | list, err := flow.ParseProcessConfig(node, variable) 60 | if err != nil { 61 | return 0, err 62 | } 63 | list.PushBack(flow.NodeInfo{ 64 | NodeID: "结束", 65 | }) 66 | list.PushFront(flow.NodeInfo{ 67 | NodeID: "开始", 68 | Type: flow.NodeInfoTypes[flow.STARTER], 69 | Aprover: userID, 70 | }) 71 | arr := util.List2Array(list) 72 | str, err := util.ToJSONStr(arr) 73 | if err != nil { 74 | return 0, err 75 | } 76 | e.NodeInfos = str 77 | ID, err := SaveExecTx(e, tx) 78 | return ID, err 79 | } 80 | 81 | // GetExecNodeInfosByProcInstID GetExecNodeInfosByProcInstID 82 | // 获取执行流经过的节点信息 83 | func GetExecNodeInfosByProcInstID(procInstID int) ([]*flow.NodeInfo, error) { 84 | nodeinfoStr, err := model.GetExecNodeInfosByProcInstID(procInstID) 85 | if err != nil { 86 | return nil, err 87 | } 88 | var nodeInfos []*flow.NodeInfo 89 | err = util.Str2Struct(nodeinfoStr, &nodeInfos) 90 | return nodeInfos, err 91 | } 92 | -------------------------------------------------------------------------------- /workflow-engine/model/ACT_RE_PROCDEF.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/jinzhu/gorm" 4 | 5 | // Procdef 流程定义表 6 | type Procdef struct { 7 | Model 8 | Name string `json:"name,omitempty"` 9 | Version int `json:"version,omitempty"` 10 | // 流程定义json字符串 11 | Resource string `gorm:"size:10000" json:"resource,omitempty"` 12 | // 用户id 13 | Userid string `json:"userid,omitempty"` 14 | Username string `json:"username,omitempty"` 15 | // 用户所在公司 16 | Company string `json:"company,omitempty"` 17 | DeployTime string `json:"deployTime,omitempty"` 18 | } 19 | 20 | // Save save and return id 21 | // 保存并返回ID 22 | func (p *Procdef) Save() (ID int, err error) { 23 | err = db.Create(p).Error 24 | if err != nil { 25 | return 0, err 26 | } 27 | return p.ID, nil 28 | } 29 | 30 | // SaveTx SaveTx 31 | func (p *Procdef) SaveTx(tx *gorm.DB) error { 32 | err := tx.Create(p).Error 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | // GetProcdefLatestByNameAndCompany :get latest procdef by name and company 40 | // 根据名字和公司查询最新的流程定义 41 | func GetProcdefLatestByNameAndCompany(name, company string) (*Procdef, error) { 42 | var p []*Procdef 43 | err := db.Where("name=? and company=?", name, company).Order("version desc").Find(&p).Error 44 | if err != nil || len(p) == 0 { 45 | return nil, err 46 | } 47 | return p[0], err 48 | } 49 | 50 | // GetProcdefByID 根据流程定义 51 | func GetProcdefByID(id int) (*Procdef, error) { 52 | var p = &Procdef{} 53 | err := db.Where("id=?", id).Find(p).Error 54 | return p, err 55 | } 56 | 57 | // DelProcdefByID del by id 58 | // 根据id删除 59 | func DelProcdefByID(id int) error { 60 | err := db.Where("id = ?", id).Delete(&Procdef{}).Error 61 | return err 62 | } 63 | 64 | // DelProcdefByIDTx DelProcdefByIDTx 65 | func DelProcdefByIDTx(id int, tx *gorm.DB) error { 66 | return tx.Where("id = ?", id).Delete(&Procdef{}).Error 67 | } 68 | 69 | // FindProcdefsWithCountAndPaged return result with total count and error 70 | // 返回查询结果和总条数 71 | func FindProcdefsWithCountAndPaged(pageIndex, pageSize int, maps map[string]interface{}) (datas []*Procdef, count int, err error) { 72 | err = db.Select("id,name,version,userid,deploy_time").Where(maps).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&datas).Error 73 | if err != nil && err != gorm.ErrRecordNotFound { 74 | return nil, 0, err 75 | } 76 | err = db.Model(&Procdef{}).Where(maps).Count(&count).Error 77 | if err != nil { 78 | return nil, 0, err 79 | } 80 | return datas, count, nil 81 | } 82 | 83 | // MoveProcdefToHistoryByIDTx 将流程定义移至历史纪录表 84 | func MoveProcdefToHistoryByIDTx(ID int, tx *gorm.DB) error { 85 | err := tx.Exec("insert into procdef_history select * from procdef where id=?", ID).Error 86 | if err != nil { 87 | return err 88 | } 89 | return DelProcdefByIDTx(ID, tx) 90 | } 91 | -------------------------------------------------------------------------------- /workflow-engine/model/ACT_RU_IDENTITYLINK.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Identitylink 用户组同任务的关系 8 | type Identitylink struct { 9 | Model 10 | Group string `json:"group,omitempty"` 11 | Type string `json:"type,omitempty"` 12 | UserID string `json:"userid,omitempty"` 13 | UserName string `json:"username,omitempty"` 14 | TaskID int `json:"taskID,omitempty"` 15 | Step int `json:"step"` 16 | ProcInstID int `json:"procInstID,omitempty"` 17 | Company string `json:"company,omitempty"` 18 | Comment string `json:"comment,omitempty"` 19 | } 20 | 21 | // IdentityType 类型 22 | type IdentityType int 23 | 24 | const ( 25 | // CANDIDATE 候选 26 | CANDIDATE IdentityType = iota 27 | // PARTICIPANT 参与人 28 | PARTICIPANT 29 | // MANAGER 上级领导 30 | MANAGER 31 | // NOTIFIER 抄送人 32 | NOTIFIER 33 | ) 34 | 35 | // IdentityTypes IdentityTypes 36 | var IdentityTypes = [...]string{CANDIDATE: "candidate", PARTICIPANT: "participant", MANAGER: "主管", NOTIFIER: "notifier"} 37 | 38 | // SaveTx SaveTx 39 | func (i *Identitylink) SaveTx(tx *gorm.DB) error { 40 | // if len(i.Company) == 0 { 41 | // return errors.New("Identitylink表的company字段不能为空!!") 42 | // } 43 | err := tx.Create(i).Error 44 | return err 45 | } 46 | 47 | // DelCandidateByProcInstID DelCandidateByProcInstID 48 | // 删除历史候选人 49 | func DelCandidateByProcInstID(procInstID int, tx *gorm.DB) error { 50 | return tx.Where("proc_inst_id=? and type=?", procInstID, IdentityTypes[CANDIDATE]).Delete(&Identitylink{}).Error 51 | } 52 | 53 | // ExistsNotifierByProcInstIDAndGroup 抄送人是否已经存在 54 | func ExistsNotifierByProcInstIDAndGroup(procInstID int, group string) (bool, error) { 55 | var count int 56 | err := db.Model(&Identitylink{}).Where("identitylink.proc_inst_id=? and identitylink.group=? and identitylink.type=?", procInstID, group, IdentityTypes[NOTIFIER]).Count(&count).Error 57 | if err != nil { 58 | if err == gorm.ErrRecordNotFound { 59 | return false, nil 60 | } 61 | return false, err 62 | } 63 | if count > 0 { 64 | return true, nil 65 | } 66 | return false, nil 67 | } 68 | 69 | // IfParticipantByTaskID IfParticipantByTaskID 70 | // 针对指定任务判断用户是否已经审批过了 71 | func IfParticipantByTaskID(userID, company string, taskID int) (bool, error) { 72 | var count int 73 | err := db.Model(&Identitylink{}).Where("user_id=? and company=? and task_id=?", userID, company, taskID).Count(&count).Error 74 | if err != nil { 75 | if err == gorm.ErrRecordNotFound { 76 | return false, nil 77 | } 78 | return false, err 79 | } 80 | if count > 0 { 81 | return true, nil 82 | } 83 | return false, nil 84 | } 85 | 86 | // FindParticipantByProcInstID 查询参与审批的人 87 | func FindParticipantByProcInstID(procInstID int) ([]*Identitylink, error) { 88 | var datas []*Identitylink 89 | err := db.Select("id,user_id,user_name,step,comment").Where("proc_inst_id=? and type=?", procInstID, IdentityTypes[PARTICIPANT]).Order("id asc").Find(&datas).Error 90 | return datas, err 91 | } 92 | -------------------------------------------------------------------------------- /workflow-engine/model/database.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | 8 | config "github.com/go-workflow/go-workflow/workflow-config" 9 | 10 | "github.com/jinzhu/gorm" 11 | 12 | // mysql 13 | _ "github.com/go-sql-driver/mysql" 14 | ) 15 | 16 | var db *gorm.DB 17 | 18 | // Model 其它数据结构的公共部分 19 | type Model struct { 20 | ID int `gorm:"primary_key" json:"id,omitempty"` 21 | } 22 | 23 | // 配置 24 | var conf = *config.Config 25 | 26 | // Setup 初始化一个db连接 27 | func Setup() { 28 | var err error 29 | log.Println("数据库初始化!!") 30 | db, err = gorm.Open(conf.DbType, fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", conf.DbUser, conf.DbPassword, conf.DbHost, conf.DbPort, conf.DbName)) 31 | if err != nil { 32 | log.Fatalf("数据库连接失败 err: %v", err) 33 | } 34 | // 启用Logger,显示详细日志 35 | mode, _ := strconv.ParseBool(conf.DbLogMode) 36 | 37 | db.LogMode(mode) 38 | 39 | db.SingularTable(true) //全局设置表名不可以为复数形式 40 | // db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback) 41 | idle, err := strconv.Atoi(conf.DbMaxIdleConns) 42 | if err != nil { 43 | panic(err) 44 | } 45 | db.DB().SetMaxIdleConns(idle) 46 | open, err := strconv.Atoi(conf.DbMaxOpenConns) 47 | if err != nil { 48 | panic(err) 49 | } 50 | db.DB().SetMaxOpenConns(open) 51 | 52 | db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;").AutoMigrate(&Procdef{}). 53 | AutoMigrate(&Execution{}).AutoMigrate(&Task{}). 54 | AutoMigrate(&ProcInst{}).AutoMigrate(&Identitylink{}). 55 | AutoMigrate(&ExecutionHistory{}). 56 | AutoMigrate(&IdentitylinkHistory{}). 57 | AutoMigrate(&ProcInstHistory{}). 58 | AutoMigrate(&TaskHistory{}). 59 | AutoMigrate(&ProcdefHistory{}) 60 | db.Model(&Procdef{}).AddIndex("idx_id", "id") 61 | db.Model(&ProcInst{}).AddIndex("idx_id", "id") 62 | db.Model(&Execution{}).AddForeignKey("proc_inst_id", "proc_inst(id)", "CASCADE", "RESTRICT").AddIndex("idx_id", "id") 63 | db.Model(&Identitylink{}).AddForeignKey("proc_inst_id", "proc_inst(id)", "CASCADE", "RESTRICT").AddIndex("idx_id", "id") 64 | db.Model(&Task{}).AddForeignKey("proc_inst_id", "proc_inst(id)", "CASCADE", "RESTRICT").AddIndex("idx_id", "id") 65 | //---------------------历史纪录------------------------------ 66 | db.Model(&ProcInstHistory{}).AddIndex("idx_id", "id") 67 | db.Model(&ExecutionHistory{}).AddForeignKey("proc_inst_id", "proc_inst_history(id)", "CASCADE", "RESTRICT").AddIndex("idx_id", "id") 68 | db.Model(&IdentitylinkHistory{}).AddForeignKey("proc_inst_id", "proc_inst_history(id)", "CASCADE", "RESTRICT").AddIndex("idx_id", "id") 69 | db.Model(&TaskHistory{}). 70 | // AddForeignKey("proc_inst_id", "proc_inst_history(id)", "CASCADE", "RESTRICT"). 71 | AddIndex("idx_id", "id") 72 | // db.Model(&Comment{}).AddForeignKey("proc_inst_id", "proc_inst(id)", "CASCADE", "RESTRICT") 73 | } 74 | 75 | // CloseDB closes database connection (unnecessary) 76 | func CloseDB() { 77 | defer db.Close() 78 | } 79 | 80 | // GetDB getdb 81 | func GetDB() *gorm.DB { 82 | return db 83 | } 84 | 85 | // GetTx GetTx 86 | func GetTx() *gorm.DB { 87 | return db.Begin() 88 | } 89 | -------------------------------------------------------------------------------- /workflow-controller/procdefController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/service" 9 | 10 | "github.com/mumushuiding/util" 11 | ) 12 | 13 | // SaveProcdefByToken SaveProcdefByToken 14 | func SaveProcdefByToken(writer http.ResponseWriter, request *http.Request) { 15 | if request.Method != "POST" { 16 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 17 | return 18 | } 19 | token, err := GetToken(request) 20 | if err != nil { 21 | util.ResponseErr(writer, err) 22 | return 23 | } 24 | var procdef = service.Procdef{} 25 | err = util.Body2Struct(request, &procdef) 26 | if err != nil { 27 | util.ResponseErr(writer, err) 28 | return 29 | } 30 | if len(procdef.Name) == 0 { 31 | util.ResponseErr(writer, "流程名称 name 不能为空") 32 | return 33 | } 34 | if procdef.Resource == nil || len(procdef.Resource.Name) == 0 { 35 | util.ResponseErr(writer, "字段 resource 不能为空") 36 | return 37 | } 38 | id, err := procdef.SaveProcdefByToken(token) 39 | if err != nil { 40 | util.ResponseErr(writer, err) 41 | return 42 | } 43 | util.Response(writer, fmt.Sprintf("%d", id), true) 44 | } 45 | 46 | // SaveProcdef save new procdefnition 47 | // 保存流程定义 48 | func SaveProcdef(writer http.ResponseWriter, request *http.Request) { 49 | if request.Method != "POST" { 50 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 51 | return 52 | } 53 | var procdef = service.Procdef{} 54 | err := util.Body2Struct(request, &procdef) 55 | if err != nil { 56 | util.ResponseErr(writer, err) 57 | return 58 | } 59 | if len(procdef.Userid) == 0 { 60 | util.ResponseErr(writer, "字段 userid 不能为空") 61 | return 62 | } 63 | if len(procdef.Company) == 0 { 64 | util.ResponseErr(writer, "字段 company 不能为空") 65 | return 66 | } 67 | if len(procdef.Name) == 0 { 68 | util.ResponseErr(writer, "流程名称 name 不能为空") 69 | return 70 | } 71 | if procdef.Resource == nil || len(procdef.Resource.Name) == 0 { 72 | util.ResponseErr(writer, "字段 resource 不能为空") 73 | return 74 | } 75 | id, err := procdef.SaveProcdef() 76 | if err != nil { 77 | util.ResponseErr(writer, err) 78 | return 79 | } 80 | util.Response(writer, fmt.Sprintf("%d", id), true) 81 | } 82 | 83 | // FindAllProcdefPage find by page 84 | // 分页查询 85 | func FindAllProcdefPage(writer http.ResponseWriter, request *http.Request) { 86 | var procdef = service.Procdef{PageIndex: 1, PageSize: 10} 87 | err := util.Body2Struct(request, &procdef) 88 | if err != nil { 89 | util.ResponseErr(writer, err) 90 | return 91 | } 92 | datas, err := procdef.FindAllPageAsJSON() 93 | if err != nil { 94 | util.ResponseErr(writer, err) 95 | return 96 | } 97 | fmt.Fprintf(writer, "%s", datas) 98 | } 99 | 100 | // DelProcdefByID del by id 101 | // 根据 id 删除 102 | func DelProcdefByID(writer http.ResponseWriter, request *http.Request) { 103 | request.ParseForm() 104 | var ids []string = request.Form["id"] 105 | if len(ids) == 0 { 106 | util.ResponseErr(writer, "request param 【id】 is not valid , id 不存在 ") 107 | return 108 | } 109 | id, err := strconv.Atoi(ids[0]) 110 | if err != nil { 111 | util.ResponseErr(writer, err) 112 | return 113 | } 114 | err = service.DelProcdefByID(id) 115 | if err != nil { 116 | util.ResponseErr(writer, err) 117 | return 118 | } 119 | util.ResponseOk(writer) 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

7 | 8 | Apache 9 | 10 | 11 | example 12 | 13 |

14 | 15 |
16 | 17 |

Go-Workflow

18 | 19 |

go-workflow 是一个超轻量级的工作流引擎,基本架构同Activiti工作流有些相似,但是它更精简,更轻量,它是一个工作流微服务,具体案例详见:example.md

20 | 21 | 前端流程生成工具:https://github.com/go-workflow/go-workflow-UI 22 | 23 | # 一、特点: 24 | 25 | 1.它是一个工作流微服务 26 | 27 | 2.将所有的无关流程的数据,包括用户、用户组等信息从服务中解耦出去,go-workflow只纪录流程的流转 28 | 29 | 3.使用json数组替代bpmn来生成流程定义,简化流程定义的生成 30 | 31 | # 二、go-workflow框架 32 | # 1.go-workflow 数据库设计 33 | # 1.1 流程定义表 34 | 表 procdef 用于保存流程的配置, 35 | 主要字段有: 36 | 37 | name: 流程定义的名称,如:"请假流程" 38 | 39 | version: 流程定义的版本 40 | 41 | resource: 保存流程定义的具体配置,它是一个json格式的字符串 42 | 43 | company: 保存该流程创建人所在公司 44 | 45 | # 1.2 流程实例表 46 | 47 | 表 proc_inst 用于保存流程实例,当用户启动一个流程时,就会在这个表存入一个流程实例, 48 | 49 | 主要字段有: 50 | 51 | procDefID: 对应表procdef的id, 52 | 53 | title: 标题,如:"张三的请假流程" 54 | 55 | department: 用户所在部门 56 | 57 | nodeID: 当前所处于节点的名称 58 | 59 | candidate: 当前审批人或者审批用户组 60 | 61 | taskID: 当前任务id 62 | 63 | # 1.3 执行流表 64 | 表 execution 用于保存执行流,当用户启动一个流程时,就会生成一条执行流,之后的流程就会按照执行流的顺序流转, 65 | 66 | 比如:开始-主管审批-财务审批-人事审批-结束 , 67 | 68 | 主要的字段有: 69 | 70 | procInstID: 流程实例id,对应表proc_inst 71 | 72 | procDefID: 流程定义id,对应表procdef 73 | 74 | nodeInfos: 是一个json数组,纪录流程实例会经过的所有节点 75 | 76 | # 1.4 关系表 77 | 表 identitylink 用于保存任务task的候选用户组或者候选用户以及用户所参与的流程信息, 78 | 79 | 主要字段有 80 | 81 | type: 表示关系类型,有:"candidate"和"participant"两种 82 | 83 | group: 表示当前审批的用户组 84 | 85 | userID: 表示当前审批的用户 86 | 87 | taskID: 对应任务task表的id 88 | 89 | step: 表示任务对应的执行流位置,比如:有一个执行流:开始-主管审批-财务审批-人事审批-结束,那么 90 | step=0,则处于【开始】位置,step=1则处于【主管审批】位置 91 | 92 | company: 表示公司 93 | 94 | procInstID: 对应流程实例id 95 | 96 | # 1.5 任务表 97 | 表 task 用于保存任务, 98 | 99 | 主要字段有: 100 | 101 | nodeID: 表示节点,如:"主管审批"结点 102 | 103 | step: 表示任务对应的执行流位置 104 | 105 | assignee: 任务的处理人 106 | 107 | memberCount: 表示当前任务需要多少人审批之后才能结束,默认是 1 108 | 109 | unCompleteNum: 表示还有多少人没有审批,默认是1 110 | 111 | agreeNum: 表示通过的人数 112 | 113 | actType: 表示任务类型 "or"表示或签,即一个人通过或者驳回就结束,"and"表示会签,要所有人通过就流 114 | 转到下一步,如果有一个人驳回那么就跳转到上一步 115 | 116 | # 1.6 历史数据表 117 | 历史数据表包括 execution_history,identitylink_history,proc_inst_history,task_history这些表字段同正常的表相同,每隔20秒,将已经结束的流程数据会自动迁移过来 118 | ## 2 流程的存储 119 | # 2.1 添加流程资源 120 | 启动 go-workflow 微服务后,可以在浏览器中输入:http://localhost:8080/workflow/procdef/save 进行存储 121 | 122 | 具体见 example.md 说明文档 123 | 124 | # 3.流程的启动 125 | 通过调用 StartProcessInstanceByID 方法来启动流程实例, 126 | 127 | 主要涉及: 128 | 129 | 获取流程定义 130 | 131 | GetResourceByNameAndCompany()->启动流程实例CreateProcInstTx()->生成执行流GenerateExec() -> 生成新任务NewTaskTx() -> 流程流转 MoveStage() 132 | 133 | # 4.任务审批 134 | 调用方法 Complete()方法来执行任务的审批, 135 | 涉及方法: 136 | 137 | 更新任务 UpdateTaskWhenComplete()-> 流转MoveStageByProcInstID() 138 | 139 | 调用方法 WithDrawTask() 方法来执行任务的撤回 140 | 141 | -------------------------------------------------------------------------------- /workflow-controller/ProcHistoryController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-workflow/go-workflow/workflow-engine/model" 8 | 9 | "github.com/go-workflow/go-workflow/workflow-engine/service" 10 | "github.com/mumushuiding/util" 11 | ) 12 | 13 | // FindProcHistoryByToken 查看我审批的纪录 14 | func FindProcHistoryByToken(writer http.ResponseWriter, request *http.Request) { 15 | if request.Method != "POST" { 16 | util.ResponseErr(writer, "只支持POST方法") 17 | return 18 | } 19 | token := request.Header.Get("Authorization") 20 | if len(token) == 0 { 21 | request.ParseForm() 22 | if len(request.Form["token"]) == 0 { 23 | util.ResponseErr(writer, "header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 24 | return 25 | } 26 | token = request.Form["token"][0] 27 | } 28 | var receiver = service.GetDefaultProcessPageReceiver() 29 | err := util.Body2Struct(request, &receiver) 30 | if err != nil { 31 | util.ResponseErr(writer, err) 32 | return 33 | } 34 | result, err := service.FindProcHistoryByToken(token, receiver) 35 | if err != nil { 36 | util.ResponseErr(writer, err) 37 | return 38 | } 39 | fmt.Fprintf(writer, result) 40 | } 41 | 42 | // FindProcHistory 查询我的审批纪录 43 | func FindProcHistory(writer http.ResponseWriter, request *http.Request) { 44 | if model.RedisOpen { 45 | util.ResponseErr(writer, "已经连接 redis,请使用/workflow/procHistory/findTaskByToken") 46 | return 47 | } 48 | if request.Method != "POST" { 49 | util.ResponseErr(writer, "只支持POST方法") 50 | return 51 | } 52 | var receiver = service.GetDefaultProcessPageReceiver() 53 | err := util.Body2Struct(request, &receiver) 54 | if err != nil { 55 | util.ResponseErr(writer, err) 56 | return 57 | } 58 | if len(receiver.UserID) == 0 { 59 | util.Response(writer, "用户userID不能为空", false) 60 | return 61 | } 62 | if len(receiver.Company) == 0 { 63 | util.Response(writer, "字段 company 不能为空", false) 64 | return 65 | } 66 | result, err := service.FindProcHistory(receiver) 67 | if err != nil { 68 | util.ResponseErr(writer, err) 69 | return 70 | } 71 | fmt.Fprintf(writer, result) 72 | } 73 | 74 | // StartHistoryByMyself 查询我发起的流程 75 | func StartHistoryByMyself(writer http.ResponseWriter, request *http.Request) { 76 | if request.Method != "POST" { 77 | util.ResponseErr(writer, "只支持Post方法!!Only suppoert Post ") 78 | return 79 | } 80 | var receiver = service.GetDefaultProcessPageReceiver() 81 | err := util.Body2Struct(request, &receiver) 82 | if err != nil { 83 | util.ResponseErr(writer, err) 84 | return 85 | } 86 | if len(receiver.UserID) == 0 { 87 | util.Response(writer, "用户userID不能为空", false) 88 | return 89 | } 90 | if len(receiver.Company) == 0 { 91 | util.Response(writer, "字段 company 不能为空", false) 92 | return 93 | } 94 | result, err := service.StartHistoryByMyself(receiver) 95 | if err != nil { 96 | util.ResponseErr(writer, err) 97 | return 98 | } 99 | fmt.Fprintf(writer, result) 100 | } 101 | 102 | // FindProcHistoryNotify 查询抄送我的流程 103 | func FindProcHistoryNotify(writer http.ResponseWriter, request *http.Request) { 104 | if request.Method != "POST" { 105 | util.ResponseErr(writer, "只支持POST方法") 106 | } 107 | var receiver = service.GetDefaultProcessPageReceiver() 108 | err := util.Body2Struct(request, &receiver) 109 | if err != nil { 110 | util.ResponseErr(writer, err) 111 | return 112 | } 113 | if len(receiver.UserID) == 0 { 114 | util.Response(writer, "用户userID不能为空", false) 115 | return 116 | } 117 | if len(receiver.Company) == 0 { 118 | util.Response(writer, "字段 company 不能为空", false) 119 | return 120 | } 121 | result, err := service.FindProcHistoryNotify(receiver) 122 | if err != nil { 123 | util.ResponseErr(writer, err) 124 | return 125 | } 126 | fmt.Fprintf(writer, result) 127 | } 128 | -------------------------------------------------------------------------------- /processConfig2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "发起人", 3 | "type": "start", 4 | "nodeId": "sid-startevent", 5 | "childNode": { 6 | "type": "route", 7 | "nodeId": "8b5c_debb", 8 | "prevId": "sid-startevent", 9 | "childNode": { 10 | "name": "UNKNOWN", 11 | "type": "approver", 12 | "nodeId": "59ba_8815", 13 | "prevId": "8b5c_debb", 14 | "properties": { 15 | "activateType": "ALL", 16 | "agreeAll": true, 17 | "actionerRules": [ 18 | { 19 | "type": "target_label", 20 | "labelNames": "人事", 21 | "labels": 427529104, 22 | "memberCount": 1, 23 | "actType": "and" 24 | } 25 | ], 26 | "noneActionerAction": "admin" 27 | } 28 | }, 29 | "conditionNodes": [ 30 | { 31 | "name": "条件1", 32 | "type": "condition", 33 | "nodeId": "da89_be76", 34 | "prevId": "8b5c_debb", 35 | "childNode": { 36 | "name": "UNKNOWN", 37 | "type": "approver", 38 | "nodeId": "735c_0854", 39 | "prevId": "da89_be76", 40 | "properties": { 41 | "activateType": "ONE_BY_ONE", 42 | "actionerRules": [ 43 | { 44 | "type": "target_management", 45 | "level": 1, 46 | "autoUp": true 47 | } 48 | ], 49 | "noneActionerAction": "admin" 50 | } 51 | }, 52 | "properties": { 53 | "conditions": [ 54 | [ 55 | { 56 | "type": "dingtalk_actioner_value_condition", 57 | "paramKey": "DDHolidayField-J2BWEN12__options", 58 | "paramLabel": "请假类型", 59 | "paramValues": [ 60 | "年假" 61 | ], 62 | "oriValue": [ 63 | "年假", 64 | "事假", 65 | "病假", 66 | "调休", 67 | "产假", 68 | "婚假", 69 | "例假", 70 | "丧假" 71 | ] 72 | } 73 | ] 74 | ] 75 | } 76 | }, 77 | { 78 | "name": "条件2", 79 | "type": "condition", 80 | "nodeId": "a97f_9517", 81 | "prevId": "8b5c_debb", 82 | "childNode": { 83 | "name": "UNKNOWN", 84 | "type": "approver", 85 | "nodeId": "5891_395b", 86 | "prevId": "a97f_9517", 87 | "properties": { 88 | "activateType": "ALL", 89 | "agreeAll": true, 90 | "actionerRules": [ 91 | { 92 | "type": "target_label", 93 | "labelNames": "财务", 94 | "labels": 427529103, 95 | "memberCount": 2, 96 | "actType": "and" 97 | } 98 | ], 99 | "noneActionerAction": "auto" 100 | } 101 | }, 102 | "properties": { 103 | "conditions": [ 104 | [ 105 | { 106 | "type": "dingtalk_actioner_value_condition", 107 | "paramKey": "DDHolidayField-J2BWEN12__options", 108 | "paramLabel": "请假类型", 109 | "paramValues": [ 110 | "调休" 111 | ], 112 | "oriValue": [ 113 | "年假", 114 | "事假", 115 | "病假", 116 | "调休", 117 | "产假", 118 | "婚假", 119 | "例假", 120 | "丧假" 121 | ] 122 | } 123 | ] 124 | ] 125 | } 126 | } 127 | ], 128 | "properties": {} 129 | } 130 | } -------------------------------------------------------------------------------- /workflow-engine/service/identitylinkService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-workflow/go-workflow/workflow-engine/model" 5 | "github.com/jinzhu/gorm" 6 | "github.com/mumushuiding/util" 7 | ) 8 | 9 | // SaveIdentitylinkTx SaveIdentitylinkTx 10 | func SaveIdentitylinkTx(i *model.Identitylink, tx *gorm.DB) error { 11 | return i.SaveTx(tx) 12 | } 13 | 14 | // AddNotifierTx 添加抄送人候选用户组 15 | func AddNotifierTx(group, company string, step, procInstID int, tx *gorm.DB) error { 16 | yes, err := ExistsNotifierByProcInstIDAndGroup(procInstID, group) 17 | if err != nil { 18 | return err 19 | } 20 | if yes { 21 | return nil 22 | } 23 | i := &model.Identitylink{ 24 | Group: group, 25 | Type: model.IdentityTypes[model.NOTIFIER], 26 | Step: step, 27 | ProcInstID: procInstID, 28 | Company: company, 29 | } 30 | return SaveIdentitylinkTx(i, tx) 31 | } 32 | 33 | // AddCandidateGroupTx AddCandidateGroupTx 34 | // 添加候选用户组 35 | func AddCandidateGroupTx(group, company string, step, taskID, procInstID int, tx *gorm.DB) error { 36 | err := DelCandidateByProcInstID(procInstID, tx) 37 | if err != nil { 38 | return err 39 | } 40 | i := &model.Identitylink{ 41 | Group: group, 42 | Type: model.IdentityTypes[model.CANDIDATE], 43 | TaskID: taskID, 44 | Step: step, 45 | ProcInstID: procInstID, 46 | Company: company, 47 | } 48 | return SaveIdentitylinkTx(i, tx) 49 | } 50 | 51 | // AddCandidateUserTx AddCandidateUserTx 52 | // 添加候选用户 53 | func AddCandidateUserTx(userID, company string, step, taskID, procInstID int, tx *gorm.DB) error { 54 | err := DelCandidateByProcInstID(procInstID, tx) 55 | if err != nil { 56 | return err 57 | } 58 | i := &model.Identitylink{ 59 | UserID: userID, 60 | Type: model.IdentityTypes[model.CANDIDATE], 61 | TaskID: taskID, 62 | Step: step, 63 | ProcInstID: procInstID, 64 | Company: company, 65 | } 66 | return SaveIdentitylinkTx(i, tx) 67 | // var wg sync.WaitGroup 68 | // var err1, err2 error 69 | // numberOfRoutine := 2 70 | // wg.Add(numberOfRoutine) 71 | // go func() { 72 | // defer wg.Done() 73 | // err1 = DelCandidateByProcInstID(procInstID, tx) 74 | // }() 75 | // go func() { 76 | // defer wg.Done() 77 | // i := &model.Identitylink{ 78 | // UserID: userID, 79 | // Type: model.IdentityTypes[model.CANDIDATE], 80 | // TaskID: taskID, 81 | // Step: step, 82 | // ProcInstID: procInstID, 83 | // Company: company, 84 | // } 85 | // err2 = SaveIdentitylinkTx(i, tx) 86 | // }() 87 | // wg.Wait() 88 | // fmt.Println("保存identyilink结束") 89 | // if err1 != nil { 90 | // return err1 91 | // } 92 | // return err2 93 | } 94 | 95 | //AddParticipantTx AddParticipantTx 96 | // 添加任务参与人 97 | func AddParticipantTx(userID, username, company, comment string, taskID, procInstID, step int, tx *gorm.DB) error { 98 | i := &model.Identitylink{ 99 | Type: model.IdentityTypes[model.PARTICIPANT], 100 | UserID: userID, 101 | UserName: username, 102 | ProcInstID: procInstID, 103 | Step: step, 104 | Company: company, 105 | TaskID: taskID, 106 | Comment: comment, 107 | } 108 | return SaveIdentitylinkTx(i, tx) 109 | } 110 | 111 | // IfParticipantByTaskID IfParticipantByTaskID 112 | // 针对指定任务判断用户是否已经审批过了 113 | func IfParticipantByTaskID(userID, company string, taskID int) (bool, error) { 114 | return model.IfParticipantByTaskID(userID, company, taskID) 115 | } 116 | 117 | // DelCandidateByProcInstID DelCandidateByProcInstID 118 | // 删除历史候选人 119 | func DelCandidateByProcInstID(procInstID int, tx *gorm.DB) error { 120 | return model.DelCandidateByProcInstID(procInstID, tx) 121 | } 122 | 123 | // ExistsNotifierByProcInstIDAndGroup 抄送人是否已经存在 124 | func ExistsNotifierByProcInstIDAndGroup(procInstID int, group string) (bool, error) { 125 | return model.ExistsNotifierByProcInstIDAndGroup(procInstID, group) 126 | } 127 | 128 | // FindParticipantByProcInstID 查询参与审批的人 129 | func FindParticipantByProcInstID(procInstID int) (string, error) { 130 | datas, err := model.FindParticipantByProcInstID(procInstID) 131 | if err != nil { 132 | return "", err 133 | } 134 | str, err := util.ToJSONStr(datas) 135 | if err != nil { 136 | return "", err 137 | } 138 | return str, nil 139 | } 140 | -------------------------------------------------------------------------------- /workflow-engine/model/ProcInstHistory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | // ProcInstHistory ProcInstHistory 11 | type ProcInstHistory struct { 12 | ProcInst 13 | } 14 | 15 | // StartHistoryByMyself 查询我发起的流程 16 | func StartHistoryByMyself(userID, company string, pageIndex, pageSize int) ([]*ProcInstHistory, int, error) { 17 | maps := map[string]interface{}{ 18 | "start_user_id": userID, 19 | "company": company, 20 | } 21 | return findProcInstsHistory(maps, pageIndex, pageSize) 22 | } 23 | func findProcInstsHistory(maps map[string]interface{}, pageIndex, pageSize int) ([]*ProcInstHistory, int, error) { 24 | var datas []*ProcInstHistory 25 | var count int 26 | selectDatas := func(in chan<- error, wg *sync.WaitGroup) { 27 | go func() { 28 | err := db.Where(maps).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Order("start_time desc").Find(&datas).Error 29 | in <- err 30 | wg.Done() 31 | }() 32 | } 33 | selectCount := func(in chan<- error, wg *sync.WaitGroup) { 34 | err := db.Model(&ProcInstHistory{}).Where(maps).Count(&count).Error 35 | in <- err 36 | wg.Done() 37 | } 38 | var err1 error 39 | var wg sync.WaitGroup 40 | numberOfRoutine := 2 41 | wg.Add(numberOfRoutine) 42 | errStream := make(chan error, numberOfRoutine) 43 | // defer fmt.Println("close channel") 44 | selectDatas(errStream, &wg) 45 | selectCount(errStream, &wg) 46 | wg.Wait() 47 | defer close(errStream) // 关闭通道 48 | for i := 0; i < numberOfRoutine; i++ { 49 | // log.Printf("send: %v", <-errStream) 50 | if err := <-errStream; err != nil { 51 | err1 = err 52 | } 53 | } 54 | // fmt.Println("结束") 55 | return datas, count, err1 56 | } 57 | 58 | // FindProcHistory 查询历史纪录 59 | func FindProcHistory(userID, company string, pageIndex, pageSize int) ([]*ProcInstHistory, int, error) { 60 | var datas []*ProcInstHistory 61 | var count int 62 | var err1 error 63 | var wg sync.WaitGroup 64 | numberOfRoutine := 2 65 | errStream := make(chan error, numberOfRoutine) 66 | selectDatas := func(wg *sync.WaitGroup) { 67 | go func() { 68 | err := db.Where("id in (select distinct proc_inst_id from identitylink_history where company=? and user_id=?)", company, userID). 69 | Offset((pageIndex - 1) * pageSize).Limit(pageSize). 70 | Order("start_time desc").Find(&datas).Error 71 | errStream <- err 72 | wg.Done() 73 | }() 74 | } 75 | selectCount := func(wg *sync.WaitGroup) { 76 | go func() { 77 | err := db.Model(&ProcInstHistory{}). 78 | Where("id in (select distinct proc_inst_id from identitylink_history where company=? and user_id=?)", company, userID). 79 | Count(&count).Error 80 | errStream <- err 81 | wg.Done() 82 | }() 83 | } 84 | wg.Add(numberOfRoutine) 85 | selectDatas(&wg) 86 | selectCount(&wg) 87 | wg.Wait() 88 | close(errStream) 89 | 90 | for i := 0; i < numberOfRoutine; i++ { 91 | if err := <-errStream; err != nil { 92 | err1 = err 93 | } 94 | } 95 | return datas, count, err1 96 | } 97 | 98 | // SaveProcInstHistory SaveProcInstHistory 99 | func SaveProcInstHistory(p *ProcInst) error { 100 | return db.Table("proc_inst_history").Create(p).Error 101 | } 102 | 103 | // DelProcInstHistoryByID DelProcInstHistoryByID 104 | func DelProcInstHistoryByID(id int) error { 105 | return db.Where("id=?", id).Delete(&ProcInstHistory{}).Error 106 | } 107 | 108 | // SaveProcInstHistoryTx SaveProcInstHistoryTx 109 | func SaveProcInstHistoryTx(p *ProcInst, tx *gorm.DB) error { 110 | return tx.Table("proc_inst_history").Create(p).Error 111 | } 112 | 113 | // FindProcHistoryNotify 查询抄送我的历史纪录 114 | func FindProcHistoryNotify(userID, company string, groups []string, pageIndex, pageSize int) ([]*ProcInstHistory, int, error) { 115 | var datas []*ProcInstHistory 116 | var count int 117 | var sql string 118 | if len(groups) != 0 { 119 | var s []string 120 | for _, val := range groups { 121 | s = append(s, "\""+val+"\"") 122 | } 123 | sql = "select proc_inst_id from identitylink_history i where i.type='notifier' and i.company='" + company + "' and (i.user_id='" + userID + "' or i.group in (" + strings.Join(s, ",") + "))" 124 | } else { 125 | sql = "select proc_inst_id from identitylink_history i where i.type='notifier' and i.company='" + company + "' and i.user_id='" + userID + "'" 126 | } 127 | err := db.Where("id in (" + sql + ")").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Order("start_time desc").Find(&datas).Error 128 | if err != nil { 129 | return datas, count, err 130 | } 131 | err = db.Model(&ProcInstHistory{}).Where("id in (" + sql + ")").Count(&count).Error 132 | if err != nil { 133 | return nil, count, err 134 | } 135 | return datas, count, err 136 | } 137 | -------------------------------------------------------------------------------- /workflow-controller/taskController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/model" 9 | 10 | "github.com/go-workflow/go-workflow/workflow-engine/service" 11 | 12 | "github.com/mumushuiding/util" 13 | ) 14 | 15 | // WithDrawTask 撤回 16 | func WithDrawTask(writer http.ResponseWriter, request *http.Request) { 17 | if request.Method != "POST" { 18 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 19 | return 20 | } 21 | if model.RedisOpen { 22 | util.ResponseErr(writer, "已经连接redis缓存,请使用方法 /workflow/task/withdrawByToken ") 23 | return 24 | } 25 | var taskRe = service.TaskReceiver{} 26 | err := util.Body2Struct(request, &taskRe) 27 | str, _ := util.ToJSONStr(taskRe) 28 | log.Println(str) 29 | if taskRe.TaskID == 0 { 30 | util.ResponseErr(writer, "字段taskID不能为空,必须为数字!") 31 | return 32 | } 33 | if len(taskRe.UserID) == 0 { 34 | util.ResponseErr(writer, "字段userID不能为空!") 35 | return 36 | } 37 | if taskRe.ProcInstID == 0 { 38 | util.ResponseErr(writer, "字段 procInstID 不能为空,必须为数字!") 39 | return 40 | } 41 | if len(taskRe.Company) == 0 { 42 | util.ResponseErr(writer, "字段company不能为空!") 43 | return 44 | } 45 | err = service.WithDrawTask(taskRe.TaskID, taskRe.ProcInstID, taskRe.UserID, taskRe.UserName, taskRe.Company, taskRe.Comment) 46 | if err != nil { 47 | util.ResponseErr(writer, err) 48 | return 49 | } 50 | util.ResponseOk(writer) 51 | } 52 | 53 | // WithDrawTaskByToken 撤回 54 | func WithDrawTaskByToken(writer http.ResponseWriter, request *http.Request) { 55 | if request.Method != "POST" { 56 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 57 | return 58 | } 59 | token := request.Header.Get("Authorization") 60 | if len(token) == 0 { 61 | request.ParseForm() 62 | if len(request.Form["token"]) == 0 { 63 | util.ResponseErr(writer, "header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 64 | return 65 | } 66 | token = request.Form["token"][0] 67 | } 68 | var taskRe = service.TaskReceiver{} 69 | err := util.Body2Struct(request, &taskRe) 70 | if taskRe.TaskID == 0 { 71 | util.ResponseErr(writer, "字段taskID不能为空,必须为数字!") 72 | return 73 | } 74 | if taskRe.ProcInstID == 0 { 75 | util.ResponseErr(writer, "字段 procInstID 不能为空,必须为数字!") 76 | return 77 | } 78 | err = service.WithDrawTaskByToken(token, &taskRe) 79 | if err != nil { 80 | util.ResponseErr(writer, err) 81 | return 82 | } 83 | util.ResponseOk(writer) 84 | } 85 | 86 | // CompleteTaskByToken 使用redis缓存时使用当前方法,更安全 87 | func CompleteTaskByToken(writer http.ResponseWriter, request *http.Request) { 88 | if request.Method != "POST" { 89 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 90 | return 91 | } 92 | token := request.Header.Get("Authorization") 93 | if len(token) == 0 { 94 | request.ParseForm() 95 | if len(request.Form["token"]) == 0 { 96 | util.ResponseErr(writer, "header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 97 | return 98 | } 99 | token = request.Form["token"][0] 100 | } 101 | var taskRe = service.TaskReceiver{} 102 | err := util.Body2Struct(request, &taskRe) 103 | // str, _ := util.ToJSONStr(taskRe) 104 | // log.Println(str) 105 | if err != nil { 106 | util.ResponseErr(writer, err) 107 | return 108 | } 109 | if len(taskRe.Comment) > 255 { 110 | util.ResponseErr(writer, "字段comment 长度不能超过255") 111 | return 112 | } 113 | if len(taskRe.Pass) == 0 { 114 | util.ResponseErr(writer, "字段pass不能为空!") 115 | return 116 | } 117 | if taskRe.TaskID == 0 { 118 | util.ResponseErr(writer, "字段taskID不能为空!") 119 | return 120 | } 121 | err = service.CompleteByToken(token, &taskRe) 122 | if err != nil { 123 | util.ResponseErr(writer, err) 124 | return 125 | } 126 | util.ResponseOk(writer) 127 | } 128 | 129 | // CompleteTask CompleteTask 130 | // 审批 131 | func CompleteTask(writer http.ResponseWriter, request *http.Request) { 132 | if request.Method != "POST" { 133 | util.ResponseErr(writer, "只支持Post方法!!Only support Post ") 134 | return 135 | } 136 | if model.RedisOpen { 137 | util.ResponseErr(writer, "已经连接redis缓存,请使用方法 /workflow/task/completeByToken") 138 | return 139 | } 140 | var taskRe = service.TaskReceiver{} 141 | err := util.Body2Struct(request, &taskRe) 142 | // str, _ := util.ToJSONStr(taskRe) 143 | // log.Println(str) 144 | if err != nil { 145 | util.ResponseErr(writer, err) 146 | return 147 | } 148 | if len(taskRe.Pass) == 0 { 149 | util.ResponseErr(writer, "字段pass不能为空!") 150 | return 151 | } 152 | pass, err := strconv.ParseBool(taskRe.Pass) 153 | if err != nil { 154 | util.ResponseErr(writer, err) 155 | return 156 | } 157 | if taskRe.TaskID == 0 { 158 | util.ResponseErr(writer, "字段taskID不能为空!") 159 | return 160 | } 161 | if len(taskRe.UserID) == 0 { 162 | util.ResponseErr(writer, "字段userID不能为空!") 163 | return 164 | } 165 | if len(taskRe.UserName) == 0 { 166 | util.ResponseErr(writer, "字段username不能为空!") 167 | return 168 | } 169 | if len(taskRe.Company) == 0 { 170 | util.ResponseErr(writer, "字段company不能为空!") 171 | return 172 | } 173 | err = service.Complete(taskRe.TaskID, taskRe.UserID, taskRe.UserName, taskRe.Company, taskRe.Comment, taskRe.Candidate, pass) 174 | if err != nil { 175 | util.ResponseErr(writer, err) 176 | return 177 | } 178 | util.ResponseOk(writer) 179 | } 180 | -------------------------------------------------------------------------------- /ProcessConfig流程定义配置详解.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 整个配置信息参考的是钉钉,钉钉生成的配置信息基本上能用,但是有所精简,只支持 主管审批和角色审批,可以打开钉钉控制平台来生成配置数据 3 | 4 | https://github.com/mumushuiding/go-workflow/blob/master/images/processConfig.png 5 | 6 | 首先配置信息是一个Node对象的嵌套对象 7 | 8 | type Node struct { 9 | 10 | Name string `json:"name,omitempty"` 11 | 12 | Type string `json:"type,omitempty"` 13 | 14 | NodeID string `json:"nodeId,omitempty"` 15 | 16 | PrevID string `json:"prevId,omitempty"` 17 | 18 | ChildNode *Node `json:"childNode,omitempty"` 19 | 20 | ConditionNodes []*Node `json:"conditionNodes,omitempty"` 21 | 22 | Properties *NodeProperties `json:"properties,omitempty"` 23 | 24 | } 25 | 26 | 在解析json对象时,先迭代遍历ConditionNodes里面的所有节点,然后再迭代遍历ChildNode 27 | 28 | 以下是配置信息json对象: 29 | 30 | { 31 | 32 | "name": "发起人", 33 | 34 | "type": "start", // Node 类型:开始节点 35 | 36 | "nodeId": "开始", // 当前节点名称 37 | 38 | "childNode": { 39 | 40 | "type": "route", // Node 类型:条件节点 41 | 42 | "prevId": "sid-startevent", 43 | 44 | "nodeId": "8b5c_debb", 45 | 46 | "conditionNodes": [ 47 | 48 | { 49 | 50 | "name": "条件1", 51 | 52 | "type": "condition", 53 | 54 | "prevId": "8b5c_debb", 55 | 56 | "nodeId": "da89_be76", 57 | 58 | "properties": { 59 | 60 | "conditions": [ 61 | [ 62 | { 63 | 64 | "type": "dingtalk_actioner_value_condition", // 条件类型:代表的是有范围的值 65 | 66 | "paramKey": "DDHolidayField-J2BWEN12__options", // 值的key, EXAMPLE.md文件里面有一个启动流程的案例,你往后台传递的变量key要与之匹配 67 | 68 | "paramLabel": "请假类型", 69 | 70 | "paramValues": [ // 当前条件值,可以多个,只要包含其中一个,这个条件就满足 71 | "年假" 72 | ], 73 | } 74 | 87 | ] 88 | ] 89 | }, 90 | 91 | "childNode": { 92 | 93 | "name": "UNKNOWN", 94 | 95 | "type": "approver", // Node节点类型 approver 审批人,生成执行流时,只会纪录approver类型的节点 96 | 97 | "prevId": "da89_be76", 98 | 99 | "nodeId": "735c_0854", 100 | 101 | "properties": { 102 | 103 | "actionerRules": [ 104 | { 105 | 106 | "type": "target_management", //审批人类型 target_management 表示下一级审批人为主管 107 | 108 | } 109 | ], 110 | } 111 | } 112 | }, 113 | { 114 | 115 | "name": "条件2", 116 | 117 | "type": "condition", 118 | 119 | "prevId": "8b5c_debb", 120 | 121 | "nodeId": "a97f_9517", 122 | 123 | "properties": { 124 | 125 | "conditions": [ 126 | [ 127 | { 128 | 129 | "type": "dingtalk_actioner_value_condition", 130 | 131 | "paramKey": "DDHolidayField-J2BWEN12__options", 132 | 133 | "paramLabel": "请假类型", 134 | 135 | "paramValue": "", 136 | 137 | "paramValues": [ 138 | "调休" 139 | ], 140 | } 141 | ] 142 | ] 143 | }, 144 | 145 | "childNode": { 146 | 147 | "name": "UNKNOWN", 148 | 149 | "type": "approver", 150 | 151 | "prevId": "a97f_9517", 152 | 153 | "nodeId": "5891_395b", 154 | 155 | "properties": { 156 | 157 | "actionerRules": [ 158 | { 159 | 160 | "type": "target_label", // 审批人类型 target_label代表的是角色审批,比如:财务,人事 161 | 162 | "labelNames": "财务", 163 | 164 | "memberCount": 2, // 表示需要通过的人数,必须有2人审批通过,都会流转到下一环节,只要有一人驳回就流转到上一环节 165 | 166 | "actType": "and" // action类型 会签 or表示或签,默认为或签 167 | } 168 | ], 169 | "noneActionerAction": "auto" 170 | } 171 | } 172 | } 173 | ], 174 | "properties": {}, 175 | 176 | "childNode": { 177 | 178 | "name": "UNKNOWN", 179 | 180 | "type": "approver", 181 | 182 | "prevId": "8b5c_debb", 183 | 184 | "nodeId": "59ba_8815", 185 | 186 | "properties": { 187 | 188 | "actionerRules": [ 189 | { 190 | 191 | "type": "target_label", 192 | 193 | "labelNames": "人事", 194 | 195 | "labels": 427529104, 196 | 197 | "isEmpty": false, 198 | 199 | "memberCount": 1, 200 | 201 | "actType": "and" 202 | } 203 | ], 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /workflow-engine/service/procdefService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/flow" 9 | 10 | "github.com/mumushuiding/util" 11 | 12 | "github.com/go-workflow/go-workflow/workflow-engine/model" 13 | ) 14 | 15 | var saveLock sync.Mutex 16 | 17 | // Procdef 流程定义表 18 | type Procdef struct { 19 | Name string `json:"name"` 20 | // 流程定义json字符串 21 | Resource *flow.Node `json:"resource"` 22 | // 用户id 23 | Userid string `json:"userid"` 24 | Username string `json:"username"` 25 | // 用户所在公司 26 | Company string `json:"company"` 27 | PageSize int `json:"pageSize"` 28 | PageIndex int `json:"pageIndex"` 29 | } 30 | 31 | // GetProcdefByID 根据流程定义id获取流程定义 32 | func GetProcdefByID(id int) (*model.Procdef, error) { 33 | return model.GetProcdefByID(id) 34 | } 35 | 36 | // GetProcdefLatestByNameAndCompany GetProcdefLatestByNameAndCompany 37 | // 根据流程定义名字和公司查询流程定义 38 | func GetProcdefLatestByNameAndCompany(name, company string) (*model.Procdef, error) { 39 | return model.GetProcdefLatestByNameAndCompany(name, company) 40 | } 41 | 42 | // GetResourceByNameAndCompany GetResourceByNameAndCompany 43 | // 获取流程定义配置信息 44 | func GetResourceByNameAndCompany(name, company string) (*flow.Node, int, string, error) { 45 | prodef, err := GetProcdefLatestByNameAndCompany(name, company) 46 | if err != nil { 47 | return nil, 0, "", err 48 | } 49 | if prodef == nil { 50 | return nil, 0, "", errors.New("流程【" + name + "】不存在") 51 | } 52 | node := &flow.Node{} 53 | err = util.Str2Struct(prodef.Resource, node) 54 | return node, prodef.ID, prodef.Name, err 55 | } 56 | 57 | // GetResourceByID GetResourceByID 58 | // 根据id查询流程定义 59 | func GetResourceByID(id int) (*flow.Node, int, error) { 60 | prodef, err := GetProcdefByID(id) 61 | if err != nil { 62 | return nil, 0, err 63 | } 64 | node := &flow.Node{} 65 | err = util.Str2Struct(prodef.Resource, node) 66 | return node, prodef.ID, err 67 | } 68 | 69 | // SaveProcdefByToken SaveProcdefByToken 70 | func (p *Procdef) SaveProcdefByToken(token string) (int, error) { 71 | // 根据 token 获取用户信息 72 | userinfo, err := GetUserinfoFromRedis(token) 73 | if err != nil { 74 | return 0, err 75 | } 76 | if len(userinfo.Company) == 0 { 77 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 company 不能为空") 78 | } 79 | if len(userinfo.Username) == 0 { 80 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 username 不能为空") 81 | } 82 | if len(userinfo.ID) == 0 { 83 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 ID 不能为空") 84 | } 85 | p.Company = userinfo.Company 86 | p.Userid = userinfo.ID 87 | p.Username = userinfo.Username 88 | return p.SaveProcdef() 89 | } 90 | 91 | // SaveProcdef 保存 92 | func (p *Procdef) SaveProcdef() (id int, err error) { 93 | // 流程定义有效性检验 94 | err = IsProdefValid(p.Resource) 95 | if err != nil { 96 | return 0, err 97 | } 98 | resource, err := util.ToJSONStr(p.Resource) 99 | if err != nil { 100 | return 0, err 101 | } 102 | // fmt.Println(resource) 103 | var procdef = model.Procdef{ 104 | Name: p.Name, 105 | Userid: p.Userid, 106 | Username: p.Username, 107 | Company: p.Company, 108 | Resource: resource, 109 | } 110 | return SaveProcdef(&procdef) 111 | } 112 | 113 | // SaveProcdef 保存 114 | func SaveProcdef(p *model.Procdef) (id int, err error) { 115 | // 参数是否为空判定 116 | saveLock.Lock() 117 | defer saveLock.Unlock() 118 | old, err := GetProcdefLatestByNameAndCompany(p.Name, p.Company) 119 | if err != nil { 120 | return 0, err 121 | } 122 | p.DeployTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 123 | if old == nil { 124 | p.Version = 1 125 | return p.Save() 126 | } 127 | tx := model.GetTx() 128 | // 保存新版本 129 | p.Version = old.Version + 1 130 | err = p.SaveTx(tx) 131 | if err != nil { 132 | tx.Rollback() 133 | return 0, err 134 | } 135 | // 转移旧版本 136 | err = model.MoveProcdefToHistoryByIDTx(old.ID, tx) 137 | if err != nil { 138 | tx.Rollback() 139 | return 0, err 140 | } 141 | tx.Commit() 142 | return p.ID, nil 143 | } 144 | 145 | // ExistsProcdefByNameAndCompany if exists 146 | // 查询流程定义是否存在 147 | func ExistsProcdefByNameAndCompany(name, company string) (yes bool, version int, err error) { 148 | p, err := GetProcdefLatestByNameAndCompany(name, company) 149 | if p == nil { 150 | return false, 1, err 151 | } 152 | version = p.Version + 1 153 | return true, version, err 154 | } 155 | 156 | // FindAllPageAsJSON find by page and transform result to string 157 | // 分页查询并将结果转换成 json 字符串 158 | func (p *Procdef) FindAllPageAsJSON() (string, error) { 159 | datas, count, err := p.FindAll() 160 | if err != nil { 161 | return "", err 162 | } 163 | return util.ToPageJSON(datas, count, p.PageIndex, p.PageSize) 164 | } 165 | 166 | // FindAll FindAll 167 | func (p *Procdef) FindAll() ([]*model.Procdef, int, error) { 168 | var page = util.Page{} 169 | page.PageRequest(p.PageIndex, p.PageSize) 170 | maps := p.getMaps() 171 | return model.FindProcdefsWithCountAndPaged(page.PageIndex, page.PageSize, maps) 172 | } 173 | func (p *Procdef) getMaps() map[string]interface{} { 174 | maps := make(map[string]interface{}) 175 | if len(p.Name) > 0 { 176 | maps["name"] = p.Name 177 | } 178 | if len(p.Company) > 0 { 179 | maps["company"] = p.Company 180 | } 181 | return maps 182 | } 183 | 184 | // DelProcdefByID del by id 185 | func DelProcdefByID(id int) error { 186 | return model.DelProcdefByID(id) 187 | } 188 | 189 | // IsProdefValid 流程定义格式是否有效 190 | func IsProdefValid(node *flow.Node) error { 191 | 192 | return flow.IfProcessConifgIsValid(node) 193 | } 194 | -------------------------------------------------------------------------------- /EXAMPLE.md: -------------------------------------------------------------------------------- 1 | # 1.安装 2 | # 1.1 安装mysql 3 | 4 | 目前只支持mysql数据库,测试之前先安装好数据库 5 | 6 | # 1.2 docker 安装最新版 go-workflow 微服务 7 | 8 | docker run -e DbType=mysql -e DbLogMode=false -e DbName=test -e DbHost=localhost -e DbUser=root -e DbPassword=123 -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/mumushuiding/go-workflow:latest 9 | 10 | # 1.3 通过 go get 获取 11 | 12 | 1.go get https://github.com/go-workflow/go-workflow 13 | 14 | 2.进入根目录,打开config.json文件,修改数据库连接配置 15 | 16 | 3. $ go build 17 | 18 | 4. $ go-workflow.exe 19 | 20 | # 1.4 部署到 K8s 21 | 22 | 请查阅根目录的 k8s.yaml 文件 , 配置使用了Istio, 未使用Istio的请稍作修改 23 | 24 | # 2.流程存储 25 | 26 | 27 | # 2.1 存储流程定义。 28 | 通过 Post 访问: http://localhost:8080/api/v1/workflow/procdef/save 29 | 30 | (参数详解见 ProcessConfig流程定义配置.md) 31 | 32 | Post参数: 33 | 34 | {"userid":"11025","name":"请假","company":"A公司","resource":{"name":"发起人","type":"start","nodeId":"sid-startevent","childNode":{"type":"route","prevId":"sid-startevent","nodeId":"8b5c_debb","conditionNodes":[{"name":"条件1","type":"condition","prevId":"8b5c_debb","nodeId":"da89_be76","properties":{"conditions":[[{"type":"dingtalk_actioner_value_condition","paramKey":"DDHolidayField-J2BWEN12__options","paramLabel":"请假类型","paramValue":"","paramValues":["年假"],"oriValue":["年假","事假","病假","调休","产假","婚假","例假","丧假"],"isEmpty":false}]]},"childNode":{"name":"UNKNOWN","type":"approver","prevId":"da89_be76","nodeId":"735c_0854","properties":{"activateType":"ONE_BY_ONE","agreeAll":false,"actionerRules":[{"type":"target_management","level":1,"isEmpty":false,"autoUp":true}]}}},{"name":"条件2","type":"condition","prevId":"8b5c_debb","nodeId":"a97f_9517","properties":{"conditions":[[{"type":"dingtalk_actioner_value_condition","paramKey":"DDHolidayField-J2BWEN12__options","paramLabel":"请假类型","paramValue":"","paramValues":["调休"],"oriValue":["年假","事假","病假","调休","产假","婚假","例假","丧假"],"isEmpty":false}]]},"childNode":{"name":"UNKNOWN","type":"approver","prevId":"a97f_9517","nodeId":"5891_395b","properties":{"activateType":"ALL","agreeAll":true,"actionerRules":[{"type":"target_label","labelNames":"财务","labels":427529103,"isEmpty":false,"memberCount":2,"actType":"and"}],"noneActionerAction":"auto"}}}],"properties":{},"childNode":{"name":"UNKNOWN","type":"approver","prevId":"8b5c_debb","nodeId":"59ba_8815","properties":{"activateType":"ALL","agreeAll":true,"actionerRules":[{"type":"target_label","labelNames":"人事","labels":427529104,"isEmpty":false,"memberCount":1,"actType":"and"}]}}}}} 35 | 36 | 如果返回:{"data":"1","ok":true} ,1表示流程实例的id,true表示成功了 37 | 38 | 39 | --------------------------------------------------------------- 40 | 或 通过 POST 访问: http://localhost:8080/api/v1/workflow/procdef/saveByToken (后台通过 token 从redis查询用户信息 userinfo,token可以保存在 Authorization 里 41 | 面 或者 reques参数里) 42 | 43 | // UserInfo 用户信息 44 | 45 | type UserInfo struct { 46 | 47 | Company string `json:"company"` 48 | 49 | // 用户所属部门 50 | 51 | Department string `json:"department"` 52 | 53 | // 用户ID 54 | 55 | ID string `json:"ID"` 56 | 57 | Username string `json:"username"` 58 | 59 | // 用户的角色 60 | 61 | Roles []string `json:"roles"` 62 | 63 | // 用户负责的部门,用户是哪些部门的主管 64 | 65 | Departments []string `json:"departments"` 66 | 67 | } 68 | 69 | 在config.json 里 配置 redis 连接: 70 | 71 | "RedisCluster": "false", // false表示redis是单点,true表示redis是集群 72 | 73 | "RedisHost": "localhost", 74 | 75 | "RedisPort": "6379", 76 | 77 | "RedisPassword": "", 78 | 79 | -------------------------------------------------------------- 80 | 81 | # 2.2 查询流程定义 82 | 83 | 通过 POST 访问: http://localhost:8080/api/v1/workflow/procdef/findAll 84 | 85 | POST参数: {"name": "请假", "company","pageSize": 1, "pageIndex": 1} , 四个参数皆可为空 86 | 87 | # 3.启动流程 88 | 通过 POST 访问: http://localhost:8080/api/v1/workflow/process/start 89 | 90 | POST参数: 91 | 92 | {"procName":"请假","title":"请假-张三","userId":"11025","department":"技术中心","company":"A公司","var":{"DDHolidayField-J2BWEN12__duration":"8","DDHolidayField-J2BWEN12__options":"年假"}} 93 | 94 | 返回结果:{"data":"1","ok":true} 95 | 96 | # 4.审批 97 | 98 | # 4.1 审批 99 | 通过POST访问:http://localhost:8080/workflow/task/complete 100 | 101 | POST参数:{"taskID":2,"pass":"true","userID":"11029","company":"A公司","comment": "评论备注","candidate": "王五"} 102 | 103 | 参数详解: taskID代表当前任务id,true表示通过,false表示驳回,candidate指定下一步执行人或者审批组如:candidate: "人事组"(一般不指定) 104 | 105 | (注意:整个流程框架,所有关于 userID的值最好是用户名,用户名不可重复) 106 | # 4.2 撤回 107 | 108 | 通过POST访问:http://localhost:8080/workflow/task/withdraw 109 | 110 | POST参数:{"taskID":2,"userID":"11029","procInstID":1,"company":"A公司"} 111 | 112 | 参数详解: taskID为当前任务id 113 | 114 | # 4.3 任务查询 115 | 116 | 通过POST访问 :http://localhost:8080/workflow/process/findTask 117 | 118 | POST参数:{"userID":"11025","groups":["人事"],"departments":["技术中心"],"company":"A公司","procName": "请假","pageIndex":1,"pageSize":10} 119 | 120 | 参数详解: groups 表示用户的所有角色,departments表示用户负责的部门, procName:流程类型,pageIndex表示当前页可不填(默认1),pageSize表示每页显示条数可不填(默认10) 121 | 122 | 123 | (注意:整个流程框架,所有关于 userID的值最好是用户名,用户名不可重复) 124 | 125 | # 4.4 查询流程审批人与评论 126 | 127 | 通过GET访问 :http://localhost:8080/workflow/identitylink/findParticipant?procInstID=12562 128 | 129 | 参数详解: procInstID 为流程实例id 130 | 131 | # 5 查询历史流程 132 | 133 | # 5.1 查询我审批的流程 134 | 135 | 通过POST访问: http://localhost:8080/api/v1/workflow/procHistory/findTask 136 | 137 | POST参数:{"userID":"admin","company":"A公司","pageIndex":1,"pageSize":2} 138 | 139 | (注意:整个流程框架,所有关于 userID 的值最好是用户名,用户名不可重复) 140 | 141 | # 5.2 查询我发起的流程 142 | 143 | -----------查询已经结束的流程-------- 144 | 145 | 通过POST访问: http://localhost:8080/api/v1/workflow/procHistory/startByMyself 146 | 147 | POST参数: {"userID":"admin","company":"A公司","pageIndex":1,"pageSize":2} 148 | 149 | -----------查询正在审批的流程-------- 150 | 151 | 通过POST访问: http://localhost:8080/api/v1/workflow/process/startByMyself 152 | 153 | POST参数: {"userID":"admin","company":"A公司","pageIndex":1,"pageSize":2} 154 | 155 | # 5.3 查询抄送我的流程 156 | 157 | ------------------ 审批中 ------------------------ 158 | 159 | 通过POST访问: http://localhost:8080/api/v1/workflow/process/FindProcNotify 160 | 161 | POST参数:{"userID":"admin","company":"A公司","groups":["人事","产品经理"]} 162 | 163 | ----------------- 已结束------------------------- 164 | 165 | 通过POST访问:http://localhost:8080/workflow/procHistory/FindProcNotify 166 | 167 | POST参数:{"userID":"admin","company":"A公司","groups":["人事","产品经理"]} 168 | 169 | 170 | -------------------------------------------------------------------------------- /workflow-router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | config "github.com/go-workflow/go-workflow/workflow-config" 7 | controller "github.com/go-workflow/go-workflow/workflow-controller" 8 | ) 9 | 10 | // Mux 路由 11 | var Mux = http.NewServeMux() 12 | var conf = *config.Config 13 | 14 | func init() { 15 | setMux() 16 | } 17 | func intercept(h http.HandlerFunc) http.HandlerFunc { 18 | return crossOrigin(h) 19 | } 20 | func crossOrigin(h http.HandlerFunc) http.HandlerFunc { 21 | return func(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Access-Control-Allow-Origin", conf.AccessControlAllowOrigin) 23 | w.Header().Set("Access-Control-Allow-Methods", conf.AccessControlAllowMethods) 24 | w.Header().Set("Access-Control-Allow-Headers", conf.AccessControlAllowHeaders) 25 | h(w, r) 26 | } 27 | } 28 | func setMux() { 29 | Mux.HandleFunc("/api/v1/workflow/", controller.Index) 30 | //-------------------------流程定义---------------------- 31 | Mux.HandleFunc("/api/v1/workflow/procdef/save", intercept(controller.SaveProcdef)) 32 | Mux.HandleFunc("/api/v1/workflow/procdef/saveByToken", intercept(controller.SaveProcdefByToken)) 33 | Mux.HandleFunc("/api/v1/workflow/procdef/findAll", intercept(controller.FindAllProcdefPage)) 34 | Mux.HandleFunc("/api/v1/workflow/procdef/delById", intercept(controller.DelProcdefByID)) 35 | // -----------------------流程实例----------------------- 36 | Mux.HandleFunc("/api/v1/workflow/process/start", intercept(controller.StartProcessInstance)) // 启动流程 37 | Mux.HandleFunc("/api/v1/workflow/process/startByToken", intercept(controller.StartProcessInstanceByToken)) // 启动流程 38 | Mux.HandleFunc("/api/v1/workflow/process/findTask", intercept(controller.FindMyProcInstPageAsJSON)) // 查询需要我审批的流程 39 | Mux.HandleFunc("/api/v1/workflow/process/findById", intercept(controller.FindProcInstByID)) // 根据id查询流程实例 40 | Mux.HandleFunc("/api/v1/workflow/process/findTaskByToken", intercept(controller.FindMyProcInstByToken)) 41 | Mux.HandleFunc("/api/v1/workflow/process/startByMyself", intercept(controller.StartByMyself)) // 查询我启动的流程 42 | Mux.HandleFunc("/api/v1/workflow/process/FindProcNotify", intercept(controller.FindProcNotify)) // 查询抄送我的流程 43 | // Mux.HandleFunc("/workflow/process/moveToHistory", controller.MoveFinishedProcInstToHistory) 44 | // -----------------------任务-------------------------- 45 | Mux.HandleFunc("/api/v1/workflow/task/complete", intercept(controller.CompleteTask)) 46 | Mux.HandleFunc("/api/v1/workflow/task/completeByToken", intercept(controller.CompleteTaskByToken)) 47 | Mux.HandleFunc("/api/v1/workflow/task/withdraw", intercept(controller.WithDrawTask)) 48 | Mux.HandleFunc("/api/v1/workflow/task/withdrawByToken", intercept(controller.WithDrawTaskByToken)) 49 | // ----------------------- 关系表 ------------------------- 50 | Mux.HandleFunc("/api/v1/workflow/identitylink/findParticipant", intercept(controller.FindParticipantByProcInstID)) 51 | 52 | // ******************************** 历史纪录 *********************************** 53 | // -------------------------- 流程实例 ------------------------------- 54 | Mux.HandleFunc("/api/v1/workflow/procHistory/findTask", intercept(controller.FindProcHistory)) 55 | Mux.HandleFunc("/api/v1/workflow/procHistory/findTaskByToken", intercept(controller.FindProcHistoryByToken)) 56 | Mux.HandleFunc("/api/v1/workflow/procHistory/startByMyself", intercept(controller.StartHistoryByMyself)) // 查询我启动的流程 57 | Mux.HandleFunc("/api/v1/workflow/procHistory/FindProcNotify", intercept(controller.FindProcHistoryNotify)) // 查询抄送我的流程 58 | // ----------------------- 关系表 ------------------------- 59 | Mux.HandleFunc("/api/v1/workflow/identitylinkHistory/findParticipant", intercept(controller.FindParticipantHistoryByProcInstID)) 60 | 61 | // ************************** 以下为废弃接口 ***************************************************** 62 | //********************************************************************************************** 63 | Mux.HandleFunc("/workflow/", controller.Index) 64 | //-------------------------流程定义---------------------- 65 | Mux.HandleFunc("/workflow/procdef/save", intercept(controller.SaveProcdef)) 66 | Mux.HandleFunc("/workflow/procdef/saveByToken", intercept(controller.SaveProcdefByToken)) 67 | Mux.HandleFunc("/workflow/procdef/findAll", intercept(controller.FindAllProcdefPage)) 68 | Mux.HandleFunc("/workflow/procdef/delById", intercept(controller.DelProcdefByID)) 69 | // -----------------------流程实例----------------------- 70 | Mux.HandleFunc("/workflow/process/start", intercept(controller.StartProcessInstance)) // 启动流程 71 | Mux.HandleFunc("/workflow/process/startByToken", intercept(controller.StartProcessInstanceByToken)) // 启动流程 72 | Mux.HandleFunc("/workflow/process/findTask", intercept(controller.FindMyProcInstPageAsJSON)) // 查询需要我审批的流程 73 | Mux.HandleFunc("/workflow/process/findTaskByToken", intercept(controller.FindMyProcInstByToken)) 74 | Mux.HandleFunc("/workflow/process/startByMyself", intercept(controller.StartByMyself)) // 查询我启动的流程 75 | Mux.HandleFunc("/workflow/process/FindProcNotify", intercept(controller.FindProcNotify)) // 查询抄送我的流程 76 | // Mux.HandleFunc("/workflow/process/moveToHistory", controller.MoveFinishedProcInstToHistory) 77 | // -----------------------任务-------------------------- 78 | Mux.HandleFunc("/workflow/task/complete", intercept(controller.CompleteTask)) 79 | Mux.HandleFunc("/workflow/task/completeByToken", intercept(controller.CompleteTaskByToken)) 80 | Mux.HandleFunc("/workflow/task/withdraw", intercept(controller.WithDrawTask)) 81 | Mux.HandleFunc("/workflow/task/withdrawByToken", intercept(controller.WithDrawTaskByToken)) 82 | // ----------------------- 关系表 ------------------------- 83 | Mux.HandleFunc("/workflow/identitylink/findParticipant", intercept(controller.FindParticipantByProcInstID)) 84 | 85 | // ******************************** 历史纪录 *********************************** 86 | // -------------------------- 流程实例 ------------------------------- 87 | Mux.HandleFunc("/workflow/procHistory/findTask", intercept(controller.FindProcHistory)) 88 | Mux.HandleFunc("/workflow/procHistory/findTaskByToken", intercept(controller.FindProcHistoryByToken)) 89 | Mux.HandleFunc("/workflow/procHistory/startByMyself", intercept(controller.StartHistoryByMyself)) // 查询我启动的流程 90 | Mux.HandleFunc("/workflow/procHistory/FindProcNotify", intercept(controller.FindProcHistoryNotify)) // 查询抄送我的流程 91 | // ----------------------- 关系表 ------------------------- 92 | Mux.HandleFunc("/workflow/identitylinkHistory/findParticipant", intercept(controller.FindParticipantHistoryByProcInstID)) 93 | } 94 | -------------------------------------------------------------------------------- /workflow-engine/model/ACT_HI_PROCINST.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | // ProcInst 流程实例 11 | type ProcInst struct { 12 | Model 13 | // 流程定义ID 14 | ProcDefID int `json:"procDefId"` 15 | // 流程定义名 16 | ProcDefName string `json:"procDefName"` 17 | // title 标题 18 | Title string `json:"title"` 19 | // 用户部门 20 | Department string `json:"department"` 21 | Company string `json:"company"` 22 | // 当前节点 23 | NodeID string `json:"nodeID"` 24 | // 审批人 25 | Candidate string `json:"candidate"` 26 | // 当前任务 27 | TaskID int `json:"taskID"` 28 | StartTime string `json:"startTime"` 29 | EndTime string `json:"endTime"` 30 | Duration int64 `json:"duration"` 31 | StartUserID string `json:"startUserId"` 32 | StartUserName string `json:"startUserName"` 33 | IsFinished bool `gorm:"default:false" json:"isFinished"` 34 | } 35 | 36 | // GroupsNotNull 候选组 37 | func GroupsNotNull(groups []string, sql string) func(db *gorm.DB) *gorm.DB { 38 | if len(groups) > 0 { 39 | return func(db *gorm.DB) *gorm.DB { 40 | return db.Or("candidate in (?) and "+sql, groups) 41 | } 42 | } 43 | return func(db *gorm.DB) *gorm.DB { 44 | return db 45 | } 46 | } 47 | 48 | // DepartmentsNotNull 分管部门 49 | func DepartmentsNotNull(departments []string, sql string) func(db *gorm.DB) *gorm.DB { 50 | if len(departments) > 0 { 51 | return func(db *gorm.DB) *gorm.DB { 52 | return db.Or("department in (?) and candidate=? and "+sql, departments, IdentityTypes[MANAGER]) 53 | } 54 | } 55 | return func(db *gorm.DB) *gorm.DB { 56 | return db 57 | } 58 | } 59 | 60 | // StartByMyself 我发起的流程 61 | func StartByMyself(userID, company string, pageIndex, pageSize int) ([]*ProcInst, int, error) { 62 | maps := map[string]interface{}{ 63 | "start_user_id": userID, 64 | "company": company, 65 | } 66 | return findProcInsts(maps, pageIndex, pageSize) 67 | } 68 | 69 | // FindProcInstByID FindProcInstByID 70 | func FindProcInstByID(id int) (*ProcInst, error) { 71 | var data = ProcInst{} 72 | err := db.Where("id=?", id).Find(&data).Error 73 | if err != nil { 74 | return nil, err 75 | } 76 | return &data, nil 77 | } 78 | 79 | // FindProcNotify 查询抄送我的流程 80 | func FindProcNotify(userID, company string, groups []string, pageIndex, pageSize int) ([]*ProcInst, int, error) { 81 | var datas []*ProcInst 82 | var count int 83 | var sql string 84 | if len(groups) != 0 { 85 | var s []string 86 | for _, val := range groups { 87 | s = append(s, "\""+val+"\"") 88 | } 89 | sql = "select proc_inst_id from identitylink i where i.type='notifier' and i.company='" + company + "' and (i.user_id='" + userID + "' or i.group in (" + strings.Join(s, ",") + "))" 90 | } else { 91 | sql = "select proc_inst_id from identitylink i where i.type='notifier' and i.company='" + company + "' and i.user_id='" + userID + "'" 92 | } 93 | err := db.Where("id in (" + sql + ")").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Order("start_time desc").Find(&datas).Error 94 | if err != nil { 95 | return datas, count, err 96 | } 97 | err = db.Model(&ProcInst{}).Where("id in (" + sql + ")").Count(&count).Error 98 | if err != nil { 99 | return nil, count, err 100 | } 101 | return datas, count, err 102 | } 103 | func findProcInsts(maps map[string]interface{}, pageIndex, pageSize int) ([]*ProcInst, int, error) { 104 | var datas []*ProcInst 105 | var count int 106 | selectDatas := func(in chan<- error, wg *sync.WaitGroup) { 107 | go func() { 108 | err := db.Where(maps).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Order("start_time desc").Find(&datas).Error 109 | in <- err 110 | wg.Done() 111 | }() 112 | } 113 | selectCount := func(in chan<- error, wg *sync.WaitGroup) { 114 | err := db.Model(&ProcInst{}).Where(maps).Count(&count).Error 115 | in <- err 116 | wg.Done() 117 | } 118 | var err1 error 119 | var wg sync.WaitGroup 120 | numberOfRoutine := 2 121 | wg.Add(numberOfRoutine) 122 | errStream := make(chan error, numberOfRoutine) 123 | // defer fmt.Println("close channel") 124 | selectDatas(errStream, &wg) 125 | selectCount(errStream, &wg) 126 | wg.Wait() 127 | defer close(errStream) // 关闭通道 128 | for i := 0; i < numberOfRoutine; i++ { 129 | // log.Printf("send: %v", <-errStream) 130 | if err := <-errStream; err != nil { 131 | err1 = err 132 | } 133 | } 134 | // fmt.Println("结束") 135 | return datas, count, err1 136 | } 137 | 138 | // FindProcInsts FindProcInsts 139 | // 分页查询 140 | func FindProcInsts(userID, procName, company string, groups, departments []string, pageIndex, pageSize int) ([]*ProcInst, int, error) { 141 | var datas []*ProcInst 142 | var count int 143 | var sql = " company='" + company + "' and is_finished=0 " 144 | if len(procName) > 0 { 145 | sql += "and proc_def_name='" + procName + "'" 146 | } 147 | // fmt.Println(sql) 148 | selectDatas := func(in chan<- error, wg *sync.WaitGroup) { 149 | go func() { 150 | err := db.Scopes(GroupsNotNull(groups, sql), DepartmentsNotNull(departments, sql)). 151 | Or("candidate=? and "+sql, userID). 152 | Offset((pageIndex - 1) * pageSize).Limit(pageSize). 153 | Order("start_time desc"). 154 | Find(&datas).Error 155 | in <- err 156 | wg.Done() 157 | }() 158 | } 159 | selectCount := func(in chan<- error, wg *sync.WaitGroup) { 160 | go func() { 161 | err := db.Scopes(GroupsNotNull(groups, sql), DepartmentsNotNull(departments, sql)).Model(&ProcInst{}).Or("candidate=? and "+sql, userID).Count(&count).Error 162 | in <- err 163 | wg.Done() 164 | }() 165 | } 166 | var err1 error 167 | var wg sync.WaitGroup 168 | numberOfRoutine := 2 169 | wg.Add(numberOfRoutine) 170 | errStream := make(chan error, numberOfRoutine) 171 | // defer fmt.Println("close channel") 172 | selectDatas(errStream, &wg) 173 | selectCount(errStream, &wg) 174 | wg.Wait() 175 | defer close(errStream) // 关闭通道 176 | 177 | for i := 0; i < numberOfRoutine; i++ { 178 | // log.Printf("send: %v", <-errStream) 179 | if err := <-errStream; err != nil { 180 | err1 = err 181 | } 182 | } 183 | // fmt.Println("结束") 184 | return datas, count, err1 185 | } 186 | 187 | // Save save 188 | func (p *ProcInst) Save() (int, error) { 189 | err := db.Create(p).Error 190 | if err != nil { 191 | return 0, err 192 | } 193 | return p.ID, nil 194 | } 195 | 196 | //SaveTx SaveTx 197 | func (p *ProcInst) SaveTx(tx *gorm.DB) (int, error) { 198 | if err := tx.Create(p).Error; err != nil { 199 | tx.Rollback() 200 | return 0, err 201 | } 202 | return p.ID, nil 203 | } 204 | 205 | // DelProcInstByID DelProcInstByID 206 | func DelProcInstByID(id int) error { 207 | return db.Where("id=?", id).Delete(&ProcInst{}).Error 208 | } 209 | 210 | // DelProcInstByIDTx DelProcInstByIDTx 211 | // 事务 212 | func DelProcInstByIDTx(id int, tx *gorm.DB) error { 213 | return tx.Where("id=?", id).Delete(&ProcInst{}).Error 214 | } 215 | 216 | // UpdateTx UpdateTx 217 | func (p *ProcInst) UpdateTx(tx *gorm.DB) error { 218 | return tx.Model(&ProcInst{}).Updates(p).Error 219 | } 220 | 221 | // FindFinishedProc FindFinishedProc 222 | func FindFinishedProc() ([]*ProcInst, error) { 223 | var datas []*ProcInst 224 | err := db.Where("is_finished=1").Find(&datas).Error 225 | return datas, err 226 | } 227 | -------------------------------------------------------------------------------- /workflow-controller/processController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-workflow/go-workflow/workflow-engine/model" 9 | 10 | "github.com/mumushuiding/util" 11 | 12 | "github.com/go-workflow/go-workflow/workflow-engine/service" 13 | ) 14 | 15 | // StartProcessInstanceByToken 启动流程 16 | func StartProcessInstanceByToken(writer http.ResponseWriter, request *http.Request) { 17 | if request.Method != "POST" { 18 | util.ResponseErr(writer, "只支持Post方法!!Only suppoert Post ") 19 | return 20 | } 21 | token := request.Header.Get("Authorization") 22 | if len(token) == 0 { 23 | request.ParseForm() 24 | if len(request.Form["token"]) == 0 { 25 | util.ResponseErr(writer, "header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 26 | return 27 | } 28 | token = request.Form["token"][0] 29 | } 30 | var proc = service.ProcessReceiver{} 31 | err := util.Body2Struct(request, &proc) 32 | if err != nil { 33 | util.ResponseErr(writer, err) 34 | return 35 | } 36 | if len(proc.ProcName) == 0 { 37 | util.Response(writer, "流程定义名procName不能为空", false) 38 | return 39 | } 40 | id, err := service.StartProcessInstanceByToken(token, &proc) 41 | if err != nil { 42 | util.ResponseErr(writer, err) 43 | return 44 | } 45 | util.Response(writer, fmt.Sprintf("%d", id), true) 46 | } 47 | 48 | // StartProcessInstance 启动流程 49 | func StartProcessInstance(writer http.ResponseWriter, request *http.Request) { 50 | if request.Method != "POST" { 51 | util.ResponseErr(writer, "只支持Post方法!!Only suppoert Post ") 52 | return 53 | } 54 | if model.RedisOpen { 55 | util.ResponseErr(writer, "已经连接 redis,请使用/workflow/process/startByToken 路径访问") 56 | return 57 | } 58 | var proc = service.ProcessReceiver{} 59 | err := util.Body2Struct(request, &proc) 60 | if err != nil { 61 | util.ResponseErr(writer, err) 62 | return 63 | } 64 | if len(proc.ProcName) == 0 { 65 | util.Response(writer, "流程定义名procName不能为空", false) 66 | return 67 | } 68 | if len(proc.Company) == 0 { 69 | util.Response(writer, "用户所在的公司company不能为空", false) 70 | return 71 | } 72 | if len(proc.UserID) == 0 { 73 | util.Response(writer, "启动流程的用户userId不能为空", false) 74 | return 75 | } 76 | if len(proc.Username) == 0 { 77 | util.Response(writer, "启动流程的用户username不能为空", false) 78 | return 79 | } 80 | if len(proc.Department) == 0 { 81 | util.Response(writer, "用户所在部门department不能为空", false) 82 | return 83 | } 84 | id, err := proc.StartProcessInstanceByID(proc.Var) 85 | if err != nil { 86 | util.ResponseErr(writer, err) 87 | return 88 | } 89 | util.Response(writer, fmt.Sprintf("%d", id), true) 90 | } 91 | 92 | // FindMyProcInstPageAsJSON FindMyProcInstPageAsJSON 93 | // 查询到我审批的流程实例 94 | func FindMyProcInstPageAsJSON(writer http.ResponseWriter, request *http.Request) { 95 | if model.RedisOpen { 96 | util.ResponseErr(writer, "已经连接 redis,请使用/workflow/process/findTaskByToken 路径访问") 97 | return 98 | } 99 | if request.Method != "POST" { 100 | util.ResponseErr(writer, "只支持Post方法!!Only suppoert Post ") 101 | return 102 | } 103 | var receiver = service.GetDefaultProcessPageReceiver() 104 | err := util.Body2Struct(request, &receiver) 105 | if err != nil { 106 | util.ResponseErr(writer, err) 107 | return 108 | } 109 | if len(receiver.UserID) == 0 { 110 | util.Response(writer, "用户userID不能为空", false) 111 | return 112 | } 113 | if len(receiver.Company) == 0 { 114 | util.Response(writer, "字段 company 不能为空", false) 115 | return 116 | } 117 | result, err := service.FindAllPageAsJSON(receiver) 118 | if err != nil { 119 | util.ResponseErr(writer, err) 120 | return 121 | } 122 | fmt.Fprintf(writer, result) 123 | } 124 | 125 | // FindMyProcInstByToken FindMyProcInstByToken 126 | // 查询待办的流程 127 | func FindMyProcInstByToken(writer http.ResponseWriter, request *http.Request) { 128 | if request.Method != "POST" { 129 | util.ResponseErr(writer, "只支持Post方法!!") 130 | return 131 | } 132 | token := request.Header.Get("Authorization") 133 | if len(token) == 0 { 134 | request.ParseForm() 135 | if len(request.Form["token"]) == 0 { 136 | util.ResponseErr(writer, "header Authorization 没有保存 token, url参数也不存在 token, 访问失败 !") 137 | return 138 | } 139 | token = request.Form["token"][0] 140 | } 141 | // fmt.Printf("token:%s\n", token) 142 | var receiver = service.GetDefaultProcessPageReceiver() 143 | err := util.Body2Struct(request, &receiver) 144 | if err != nil { 145 | util.ResponseErr(writer, err) 146 | return 147 | } 148 | result, err := service.FindMyProcInstByToken(token, receiver) 149 | if err != nil { 150 | util.ResponseErr(writer, err) 151 | return 152 | } 153 | fmt.Fprintf(writer, result) 154 | } 155 | 156 | // StartByMyself 我启动的流程 157 | func StartByMyself(writer http.ResponseWriter, request *http.Request) { 158 | if request.Method != "POST" { 159 | util.ResponseErr(writer, "只支持Post方法!!Only suppoert Post ") 160 | return 161 | } 162 | var receiver = service.GetDefaultProcessPageReceiver() 163 | err := util.Body2Struct(request, &receiver) 164 | if err != nil { 165 | util.ResponseErr(writer, err) 166 | return 167 | } 168 | if len(receiver.UserID) == 0 { 169 | util.Response(writer, "用户userID不能为空", false) 170 | return 171 | } 172 | if len(receiver.Company) == 0 { 173 | util.Response(writer, "字段 company 不能为空", false) 174 | return 175 | } 176 | result, err := service.StartByMyself(receiver) 177 | if err != nil { 178 | util.ResponseErr(writer, err) 179 | return 180 | } 181 | fmt.Fprintf(writer, result) 182 | } 183 | 184 | // FindProcNotify 查询抄送我的流程 185 | func FindProcNotify(writer http.ResponseWriter, request *http.Request) { 186 | if request.Method != "POST" { 187 | util.ResponseErr(writer, "只支持POST方法") 188 | } 189 | var receiver = service.GetDefaultProcessPageReceiver() 190 | err := util.Body2Struct(request, &receiver) 191 | if err != nil { 192 | util.ResponseErr(writer, err) 193 | return 194 | } 195 | if len(receiver.UserID) == 0 { 196 | util.Response(writer, "用户userID不能为空", false) 197 | return 198 | } 199 | if len(receiver.Company) == 0 { 200 | util.Response(writer, "字段 company 不能为空", false) 201 | return 202 | } 203 | result, err := service.FindProcNotify(receiver) 204 | if err != nil { 205 | util.ResponseErr(writer, err) 206 | return 207 | } 208 | fmt.Fprintf(writer, result) 209 | } 210 | 211 | // MoveFinishedProcInstToHistory MoveFinishedProcInstToHistory 212 | // 将已经结束的流程实例移动到历史数据库 213 | func MoveFinishedProcInstToHistory(writer http.ResponseWriter, request *http.Request) { 214 | err := service.MoveFinishedProcInstToHistory() 215 | if err != nil { 216 | util.ResponseErr(writer, err) 217 | return 218 | } 219 | util.ResponseOk(writer) 220 | } 221 | 222 | // FindProcInstByID 根据流程ID查询流程 223 | func FindProcInstByID(writer http.ResponseWriter, request *http.Request) { 224 | request.ParseForm() 225 | if len(request.Form["id"]) == 0 { 226 | util.ResponseErr(writer, "字段 id 不能为空") 227 | return 228 | } 229 | id, err := strconv.Atoi(request.Form["id"][0]) 230 | if err != nil { 231 | util.ResponseErr(writer, err) 232 | return 233 | } 234 | data, err := service.FindProcInstByID(id) 235 | if err != nil { 236 | util.ResponseErr(writer, err) 237 | return 238 | } 239 | util.Response(writer, data, true) 240 | } 241 | -------------------------------------------------------------------------------- /processConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "发起人", 3 | "type": "start", 4 | "nodeId": "开始", 5 | "childNode": { 6 | "type": "route", 7 | "prevId": "sid-startevent", 8 | "nodeId": "7e68_00d7", 9 | "conditionNodes": [ 10 | { 11 | "name": "条件1", 12 | "type": "condition", 13 | "prevId": "7e68_00d7", 14 | "nodeId": "a8be_c6f5", 15 | "properties": { 16 | "conditions": [ 17 | [ 18 | { 19 | "type": "dingtalk_actioner_range_condition", 20 | "paramKey": "DDHolidayField-J2BWEN12__duration", 21 | "paramLabel": "时长(天)", 22 | "lowerBound": "10", 23 | "upperBound": "", 24 | "unit": "天", 25 | "isEmpty": false 26 | } 27 | ] 28 | ] 29 | }, 30 | "childNode": { 31 | "name": "审批人", 32 | "type": "approver", 33 | "prevId": "a8be_c6f5", 34 | "nodeId": "sid-1234_5678", 35 | "properties": { 36 | "activateType": "ONE_BY_ONE", 37 | "agreeAll": false, 38 | "allowTaskAppend": false, 39 | "allowTaskRedirect": true, 40 | "actionerRules": [ 41 | { 42 | "type": "target_manager", 43 | "labelNames": "主管", 44 | "labels": 427529103, 45 | "isEmpty": false, 46 | "memberCount": 1, 47 | "actType": "or" 48 | } 49 | ] 50 | } 51 | } 52 | }, 53 | { 54 | "name": "条件2", 55 | "type": "condition", 56 | "prevId": "7e68_00d7", 57 | "nodeId": "49db_31b3", 58 | "properties": { 59 | "conditions": [ 60 | [ 61 | { 62 | "type": "dingtalk_actioner_range_condition", 63 | "paramKey": "DDHolidayField-J2BWEN12__duration", 64 | "paramLabel": "时长(天)", 65 | "lowerBound": "", 66 | "upperBound": "10", 67 | "unit": "天", 68 | "isEmpty": false, 69 | "lowerBoundNotEqual": "", 70 | "upperBoundEqual": "", 71 | "boundEqual": "", 72 | "key": "l" 73 | }, 74 | { 75 | "type": "dingtalk_actioner_value_condition", 76 | "paramKey": "DDHolidayField-J2BWEN12__options", 77 | "paramLabel": "请假类型", 78 | "paramValue": "", 79 | "paramValues": [ 80 | "年假" 81 | ], 82 | "oriValue": [ 83 | "年假", 84 | "事假", 85 | "病假", 86 | "调休", 87 | "产假", 88 | "陪产假", 89 | "婚假", 90 | "例假", 91 | "丧假", 92 | "哺乳假" 93 | ], 94 | "isEmpty": false 95 | } 96 | ] 97 | ] 98 | }, 99 | "childNode": { 100 | "name": "UNKNOWN", 101 | "type": "approver", 102 | "prevId": "49db_31b3", 103 | "nodeId": "59df_acce", 104 | "properties": { 105 | "activateType": "ALL", 106 | "agreeAll": false, 107 | "actionerRules": [ 108 | { 109 | "type": "target_label", 110 | "labelNames": "财务", 111 | "labels": 427529103, 112 | "isEmpty": false, 113 | "memberCount": 1, 114 | "actType": "or" 115 | } 116 | ], 117 | "noneActionerAction": "admin" 118 | }, 119 | "childNode": { 120 | "type": "route", 121 | "prevId": "59df_acce", 122 | "nodeId": "5a90_65f8", 123 | "conditionNodes": [ 124 | { 125 | "name": "条件1", 126 | "type": "condition", 127 | "prevId": "5a90_65f8", 128 | "nodeId": "d757_1c62", 129 | "properties": { 130 | "conditions": [ 131 | [ 132 | { 133 | "type": "dingtalk_actioner_value_condition", 134 | "paramKey": "DDHolidayField-J2BWEN12__options", 135 | "paramLabel": "请假类型", 136 | "paramValue": "", 137 | "paramValues": [ 138 | "哺乳假" 139 | ], 140 | "oriValue": [ 141 | "年假", 142 | "事假", 143 | "病假", 144 | "调休", 145 | "产假", 146 | "陪产假", 147 | "婚假", 148 | "例假", 149 | "丧假", 150 | "哺乳假" 151 | ], 152 | "isEmpty": false 153 | } 154 | ] 155 | ] 156 | }, 157 | "childNode": { 158 | "name": "UNKNOWN", 159 | "type": "approver", 160 | "prevId": "d757_1c62", 161 | "nodeId": "146e_7dc5", 162 | "properties": { 163 | "activateType": "ALL", 164 | "agreeAll": false, 165 | "actionerRules": [ 166 | { 167 | "type": "target_label", 168 | "labelNames": "人事", 169 | "labels": 427529104, 170 | "isEmpty": false, 171 | "memberCount": 1, 172 | "actType": "or" 173 | } 174 | ], 175 | "noneActionerAction": "admin" 176 | } 177 | } 178 | }, 179 | { 180 | "name": "条件2", 181 | "type": "condition", 182 | "prevId": "5a90_65f8", 183 | "nodeId": "0446_47db", 184 | "properties": { 185 | "conditions": [ 186 | [ 187 | { 188 | "type": "dingtalk_actioner_value_condition", 189 | "paramKey": "DDHolidayField-J2BWEN12__options", 190 | "paramLabel": "请假类型", 191 | "paramValue": "", 192 | "paramValues": [ 193 | "年假" 194 | ], 195 | "oriValue": [ 196 | "年假", 197 | "事假", 198 | "病假", 199 | "调休", 200 | "产假", 201 | "陪产假", 202 | "婚假", 203 | "例假", 204 | "丧假", 205 | "哺乳假" 206 | ], 207 | "isEmpty": false 208 | } 209 | ] 210 | ] 211 | }, 212 | "childNode": { 213 | "name": "UNKNOWN", 214 | "type": "approver", 215 | "prevId": "0446_47db", 216 | "nodeId": "e08e_4aff", 217 | "properties": { 218 | "activateType": "ALL", 219 | "agreeAll": false, 220 | "actionerRules": [ 221 | { 222 | "type": "target_label", 223 | "labelNames": "财务", 224 | "labels": 427529103, 225 | "isEmpty": false, 226 | "memberCount": 1, 227 | "actType": "or" 228 | } 229 | ], 230 | "noneActionerAction": "auto" 231 | } 232 | } 233 | } 234 | ], 235 | "properties": {} 236 | } 237 | } 238 | } 239 | ], 240 | "properties": {} 241 | } 242 | } -------------------------------------------------------------------------------- /workflow-engine/service/procInstService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/jinzhu/gorm" 9 | 10 | "github.com/go-workflow/go-workflow/workflow-engine/flow" 11 | "github.com/go-workflow/go-workflow/workflow-engine/model" 12 | "github.com/mumushuiding/util" 13 | ) 14 | 15 | // ProcessReceiver 接收页面传递参数 16 | type ProcessReceiver struct { 17 | UserID string `json:"userId"` 18 | ProcInstID string `json:"procInstID"` 19 | Username string `json:"username"` 20 | Company string `json:"company"` 21 | ProcName string `json:"procName"` 22 | Title string `json:"title"` 23 | Department string `json:"department"` 24 | Var *map[string]string `json:"var"` 25 | } 26 | 27 | // ProcessPageReceiver 分页参数 28 | type ProcessPageReceiver struct { 29 | util.Page 30 | // 我分管的部门 31 | Departments []string `json:"departments"` 32 | // 我所属于的用户组或者角色 33 | Groups []string `josn:"groups"` 34 | UserID string `json:"userID"` 35 | Username string `json:"username"` 36 | Company string `json:"company"` 37 | ProcName string `json:"procName"` 38 | ProcInstID string `json:"procInstID"` 39 | } 40 | 41 | var copyLock sync.Mutex 42 | 43 | // GetDefaultProcessPageReceiver GetDefaultProcessPageReceiver 44 | func GetDefaultProcessPageReceiver() *ProcessPageReceiver { 45 | var p = ProcessPageReceiver{} 46 | p.PageIndex = 1 47 | p.PageSize = 10 48 | return &p 49 | } 50 | func findAll(pr *ProcessPageReceiver) ([]*model.ProcInst, int, error) { 51 | var page = util.Page{} 52 | page.PageRequest(pr.PageIndex, pr.PageSize) 53 | return model.FindProcInsts(pr.UserID, pr.ProcName, pr.Company, pr.Groups, pr.Departments, pr.PageIndex, pr.PageSize) 54 | } 55 | 56 | // FindProcInstByID FindProcInstByID 57 | func FindProcInstByID(id int) (string, error) { 58 | data, err := model.FindProcInstByID(id) 59 | if err != nil { 60 | return "", err 61 | } 62 | return util.ToJSONStr(data) 63 | } 64 | 65 | // FindAllPageAsJSON FindAllPageAsJSON 66 | func FindAllPageAsJSON(pr *ProcessPageReceiver) (string, error) { 67 | datas, count, err := findAll(pr) 68 | if err != nil { 69 | return "", err 70 | } 71 | return util.ToPageJSON(datas, count, pr.PageIndex, pr.PageSize) 72 | } 73 | 74 | // FindMyProcInstByToken FindMyProcInstByToken 75 | // 根据token获取流程信息 76 | func FindMyProcInstByToken(token string, receiver *ProcessPageReceiver) (string, error) { 77 | // 根据 token 获取用户信息 78 | userinfo, err := GetUserinfoFromRedis(token) 79 | if err != nil { 80 | return "", err 81 | } 82 | if len(userinfo.Company) == 0 { 83 | return "", errors.New("保存在redis中的【用户信息 userinfo】字段 company 不能为空") 84 | } 85 | if len(userinfo.ID) == 0 { 86 | return "", errors.New("保存在redis中的【用户信息 userinfo】字段 ID 不能为空") 87 | } 88 | receiver.Company = userinfo.Company 89 | receiver.Departments = userinfo.Departments 90 | receiver.Groups = userinfo.Roles 91 | receiver.UserID = userinfo.ID 92 | // str, _ = util.ToJSONStr(receiver) 93 | // fmt.Printf("receiver:%s\n", str) 94 | return FindAllPageAsJSON(receiver) 95 | } 96 | 97 | // StartProcessInstanceByToken 启动流程 98 | func StartProcessInstanceByToken(token string, p *ProcessReceiver) (int, error) { 99 | // 根据 token 获取用户信息 100 | userinfo, err := GetUserinfoFromRedis(token) 101 | if err != nil { 102 | return 0, err 103 | } 104 | if len(userinfo.Company) == 0 { 105 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 company 不能为空") 106 | } 107 | if len(userinfo.Username) == 0 { 108 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 username 不能为空") 109 | } 110 | if len(userinfo.ID) == 0 { 111 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 ID 不能为空") 112 | } 113 | if len(userinfo.Department) == 0 { 114 | return 0, errors.New("保存在redis中的【用户信息 userinfo】字段 department 不能为空") 115 | } 116 | p.Company = userinfo.Company 117 | p.Department = userinfo.Department 118 | p.UserID = userinfo.ID 119 | p.Username = userinfo.Username 120 | return p.StartProcessInstanceByID(p.Var) 121 | } 122 | 123 | // StartProcessInstanceByID 启动流程 124 | func (p *ProcessReceiver) StartProcessInstanceByID(variable *map[string]string) (int, error) { 125 | // times := time.Now() 126 | // runtime.GOMAXPROCS(2) 127 | // 获取流程定义 128 | node, prodefID, procdefName, err := GetResourceByNameAndCompany(p.ProcName, p.Company) 129 | if err != nil { 130 | return 0, err 131 | } 132 | // fmt.Printf("获取流程定义耗时:%v", time.Since(times)) 133 | //--------以下需要添加事务----------------- 134 | step := 0 // 0 为开始节点 135 | tx := model.GetTx() 136 | // 新建流程实例 137 | var procInst = model.ProcInst{ 138 | ProcDefID: prodefID, 139 | ProcDefName: procdefName, 140 | Title: p.Title, 141 | Department: p.Department, 142 | StartTime: util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS), 143 | StartUserID: p.UserID, 144 | StartUserName: p.Username, 145 | Company: p.Company, 146 | } //开启事务 147 | // times = time.Now() 148 | procInstID, err := CreateProcInstTx(&procInst, tx) // 事务 149 | // fmt.Printf("启动流程实例耗时:%v", time.Since(times)) 150 | exec := &model.Execution{ 151 | ProcDefID: prodefID, 152 | ProcInstID: procInstID, 153 | } 154 | task := &model.Task{ 155 | NodeID: "开始", 156 | ProcInstID: procInstID, 157 | Assignee: p.UserID, 158 | IsFinished: true, 159 | ClaimTime: util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS), 160 | Step: step, 161 | MemberCount: 1, 162 | UnCompleteNum: 0, 163 | ActType: "or", 164 | AgreeNum: 1, 165 | } 166 | // 生成执行流,一串运行节点 167 | _, err = GenerateExec(exec, node, p.UserID, variable, tx) //事务 168 | if err != nil { 169 | tx.Rollback() 170 | return 0, err 171 | } 172 | // 获取执行流信息 173 | var nodeinfos []*flow.NodeInfo 174 | err = util.Str2Struct(exec.NodeInfos, &nodeinfos) 175 | if err != nil { 176 | tx.Rollback() 177 | return 0, err 178 | } 179 | // fmt.Printf("生成执行流耗时:%v", time.Since(times)) 180 | // -----------------生成新任务------------- 181 | // times = time.Now() 182 | if nodeinfos[0].ActType == "and" { 183 | task.UnCompleteNum = nodeinfos[0].MemberCount 184 | task.MemberCount = nodeinfos[0].MemberCount 185 | } 186 | _, err = NewTaskTx(task, tx) 187 | if err != nil { 188 | tx.Rollback() 189 | return 0, err 190 | } 191 | // fmt.Printf("生成新任务耗时:%v", time.Since(times)) 192 | //--------------------流转------------------ 193 | // times = time.Now() 194 | // 流程移动到下一环节 195 | err = MoveStage(nodeinfos, p.UserID, p.Username, p.Company, "启动流程", "", task.ID, procInstID, step, true, tx) 196 | if err != nil { 197 | tx.Rollback() 198 | return 0, err 199 | } 200 | // fmt.Printf("流转到下一流程耗时:%v", time.Since(times)) 201 | // fmt.Println("--------------提交事务----------") 202 | tx.Commit() //结束事务 203 | return procInstID, err 204 | } 205 | 206 | // CreateProcInstByID 新建流程实例 207 | // func CreateProcInstByID(processDefinitionID int, startUserID string) (int, error) { 208 | // var procInst = model.ProcInst{ 209 | // ProcDefID: processDefinitionID, 210 | // StartTime: util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS), 211 | // StartUserID: startUserID, 212 | // } 213 | // return procInst.Save() 214 | // } 215 | 216 | // CreateProcInstTx CreateProcInstTx 217 | // 开户事务 218 | func CreateProcInstTx(procInst *model.ProcInst, tx *gorm.DB) (int, error) { 219 | 220 | return procInst.SaveTx(tx) 221 | } 222 | 223 | // SetProcInstFinish SetProcInstFinish 224 | // 设置流程结束 225 | func SetProcInstFinish(procInstID int, endTime string, tx *gorm.DB) error { 226 | var p = &model.ProcInst{} 227 | p.ID = procInstID 228 | p.EndTime = endTime 229 | p.IsFinished = true 230 | return p.UpdateTx(tx) 231 | } 232 | 233 | // StartByMyself 我发起的流程 234 | func StartByMyself(receiver *ProcessPageReceiver) (string, error) { 235 | var page = util.Page{} 236 | page.PageRequest(receiver.PageIndex, receiver.PageSize) 237 | datas, count, err := model.StartByMyself(receiver.UserID, receiver.Company, receiver.PageIndex, receiver.PageSize) 238 | if err != nil { 239 | return "", err 240 | } 241 | return util.ToPageJSON(datas, count, receiver.PageIndex, receiver.PageSize) 242 | } 243 | 244 | // FindProcNotify 查询抄送我的 245 | func FindProcNotify(receiver *ProcessPageReceiver) (string, error) { 246 | var page = util.Page{} 247 | page.PageRequest(receiver.PageIndex, receiver.PageSize) 248 | datas, count, err := model.FindProcNotify(receiver.UserID, receiver.Company, receiver.Groups, receiver.PageIndex, receiver.PageSize) 249 | if err != nil { 250 | return "", err 251 | } 252 | return util.ToPageJSON(datas, count, receiver.PageIndex, receiver.PageSize) 253 | } 254 | 255 | // UpdateProcInst UpdateProcInst 256 | // 更新流程实例 257 | func UpdateProcInst(procInst *model.ProcInst, tx *gorm.DB) error { 258 | return procInst.UpdateTx(tx) 259 | } 260 | 261 | // MoveFinishedProcInstToHistory MoveFinishedProcInstToHistory 262 | func MoveFinishedProcInstToHistory() error { 263 | // 要注意并发,可能会运行多个app实例 264 | // 加锁 265 | copyLock.Lock() 266 | defer copyLock.Unlock() 267 | // 从pro_inst表查询已经结束的流程 268 | proinsts, err := model.FindFinishedProc() 269 | if err != nil { 270 | return err 271 | } 272 | if len(proinsts) == 0 { 273 | return nil 274 | } 275 | for _, v := range proinsts { 276 | // 复制 proc_inst 277 | duration, err := util.TimeStrSub(v.EndTime, v.StartTime, util.YYYY_MM_DD_HH_MM_SS) 278 | if err != nil { 279 | return err 280 | } 281 | v.Duration = duration 282 | err = copyProcToHistory(v) 283 | if err != nil { 284 | return err 285 | } 286 | tx := model.GetTx() 287 | // 流程实例的task移至历史纪录 288 | err = copyTaskToHistoryByProInstID(v.ID, tx) 289 | if err != nil { 290 | tx.Rollback() 291 | DelProcInstHistoryByID(v.ID) 292 | return err 293 | } 294 | // execution移至历史纪录 295 | err = copyExecutionToHistoryByProcInstID(v.ID, tx) 296 | if err != nil { 297 | tx.Rollback() 298 | DelProcInstHistoryByID(v.ID) 299 | return err 300 | } 301 | // identitylink移至历史纪录 302 | err = copyIdentitylinkToHistoryByProcInstID(v.ID, tx) 303 | if err != nil { 304 | tx.Rollback() 305 | DelProcInstHistoryByID(v.ID) 306 | return err 307 | } 308 | // 删除流程实例 309 | err = DelProcInstByIDTx(v.ID, tx) 310 | if err != nil { 311 | tx.Rollback() 312 | DelProcInstHistoryByID(v.ID) 313 | return err 314 | } 315 | tx.Commit() 316 | } 317 | 318 | return nil 319 | } 320 | 321 | // DelProcInstByIDTx DelProcInstByIDTx 322 | func DelProcInstByIDTx(procInstID int, tx *gorm.DB) error { 323 | return model.DelProcInstByIDTx(procInstID, tx) 324 | } 325 | func copyIdentitylinkToHistoryByProcInstID(procInstID int, tx *gorm.DB) error { 326 | return model.CopyIdentitylinkToHistoryByProcInstID(procInstID, tx) 327 | } 328 | func copyExecutionToHistoryByProcInstID(procInstID int, tx *gorm.DB) error { 329 | return model.CopyExecutionToHistoryByProcInstIDTx(procInstID, tx) 330 | } 331 | func copyProcToHistory(procInst *model.ProcInst) error { 332 | return model.SaveProcInstHistory(procInst) 333 | 334 | } 335 | func copyTaskToHistoryByProInstID(procInstID int, tx *gorm.DB) error { 336 | return model.CopyTaskToHistoryByProInstID(procInstID, tx) 337 | } 338 | -------------------------------------------------------------------------------- /workflow-engine/service/taskService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/go-workflow/go-workflow/workflow-engine/flow" 12 | 13 | "github.com/jinzhu/gorm" 14 | 15 | "github.com/go-workflow/go-workflow/workflow-engine/model" 16 | "github.com/mumushuiding/util" 17 | ) 18 | 19 | // TaskReceiver 任务 20 | type TaskReceiver struct { 21 | TaskID int `json:"taskID"` 22 | UserID string `json:"userID,omitempty"` 23 | UserName string `json:"username,omitempty"` 24 | Pass string `json:"pass,omitempty"` 25 | Company string `json:"company,omitempty"` 26 | ProcInstID int `json:"procInstID,omitempty"` 27 | Comment string `json:"comment,omitempty"` 28 | Candidate string `json:"candidate,omitempty"` 29 | } 30 | 31 | var completeLock sync.Mutex 32 | 33 | // NewTask 新任务 34 | func NewTask(t *model.Task) (int, error) { 35 | if len(t.NodeID) == 0 { 36 | return 0, errors.New("request param nodeID can not be null / 任务当前所在节点nodeId不能为空!") 37 | } 38 | t.CreateTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 39 | return t.NewTask() 40 | } 41 | 42 | // NewTaskTx NewTaskTx 43 | // 开启事务 44 | func NewTaskTx(t *model.Task, tx *gorm.DB) (int, error) { 45 | if len(t.NodeID) == 0 { 46 | return 0, errors.New("request param nodeID can not be null / 任务当前所在节点nodeId不能为空!") 47 | } 48 | t.CreateTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 49 | return t.NewTaskTx(tx) 50 | } 51 | 52 | // DeleteTask 删除任务 53 | func DeleteTask(id int) error { 54 | return model.DeleteTask(id) 55 | } 56 | 57 | // GetTaskByID GetTaskById 58 | func GetTaskByID(id int) (task *model.Task, err error) { 59 | return model.GetTaskByID(id) 60 | } 61 | 62 | // GetTaskLastByProInstID GetTaskLastByProInstID 63 | func GetTaskLastByProInstID(procInstID int) (*model.Task, error) { 64 | return model.GetTaskLastByProInstID(procInstID) 65 | } 66 | 67 | // CompleteByToken 通过token 审批任务 68 | func CompleteByToken(token string, receiver *TaskReceiver) error { 69 | userinfo, err := GetUserinfoFromRedis(token) 70 | if err != nil { 71 | return err 72 | } 73 | pass, err := strconv.ParseBool(receiver.Pass) 74 | if err != nil { 75 | return err 76 | } 77 | err = Complete(receiver.TaskID, userinfo.ID, userinfo.Username, userinfo.Company, receiver.Comment, receiver.Candidate, pass) 78 | if err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | // Complete Complete 85 | // 审批 86 | func Complete(taskID int, userID, username, company, comment, candidate string, pass bool) error { 87 | tx := model.GetTx() 88 | err := CompleteTaskTx(taskID, userID, username, company, comment, candidate, pass, tx) 89 | if err != nil { 90 | tx.Rollback() 91 | return err 92 | } 93 | tx.Commit() 94 | return nil 95 | } 96 | 97 | // UpdateTaskWhenComplete UpdateTaskWhenComplete 98 | func UpdateTaskWhenComplete(taskID int, userID string, pass bool, tx *gorm.DB) (*model.Task, error) { 99 | // 获取task 100 | completeLock.Lock() // 关锁 101 | defer completeLock.Unlock() //解锁 102 | // 查询任务 103 | task, err := GetTaskByID(taskID) 104 | if err != nil { 105 | return nil, err 106 | } 107 | if task == nil { 108 | return nil, errors.New("任务【" + fmt.Sprintf("%d", task.ID) + "】不存在") 109 | } 110 | // 判断是否已经结束 111 | if task.IsFinished == true { 112 | if task.NodeID == "结束" { 113 | return nil, errors.New("流程已经结束") 114 | } 115 | return nil, errors.New("任务【" + fmt.Sprintf("%d", taskID) + "】已经被审批过了!!") 116 | } 117 | // 设置处理人和处理时间 118 | task.Assignee = userID 119 | task.ClaimTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 120 | // ----------------会签 (默认全部通过才结束),只要存在一个不通过,就结束,然后流转到上一步 121 | //同意 122 | if pass { 123 | task.AgreeNum++ 124 | } else { 125 | task.IsFinished = true 126 | } 127 | // 未审批人数减一 128 | task.UnCompleteNum-- 129 | // 判断是否结束 130 | if task.UnCompleteNum == 0 { 131 | task.IsFinished = true 132 | } 133 | err = task.UpdateTx(tx) 134 | // str, _ := util.ToJSONStr(task) 135 | // log.Println(str) 136 | if err != nil { 137 | return nil, err 138 | } 139 | return task, nil 140 | } 141 | 142 | // CompleteTaskTx CompleteTaskTx 143 | // 执行任务 144 | func CompleteTaskTx(taskID int, userID, username, company, comment, candidate string, pass bool, tx *gorm.DB) error { 145 | 146 | //更新任务 147 | task, err := UpdateTaskWhenComplete(taskID, userID, pass, tx) 148 | if err != nil { 149 | return err 150 | } 151 | // 如果是会签 152 | if task.ActType == "and" { 153 | // fmt.Println("------------------是会签,判断用户是否已经审批过了,避免重复审批-------") 154 | // 判断用户是否已经审批过了(存在会签的情况) 155 | yes, err := IfParticipantByTaskID(userID, company, taskID) 156 | if err != nil { 157 | tx.Rollback() 158 | return err 159 | } 160 | if yes { 161 | tx.Rollback() 162 | return errors.New("您已经审批过了,请等待他人审批!)") 163 | } 164 | } 165 | 166 | // 查看任务的未审批人数是否为0,不为0就不流转 167 | if task.UnCompleteNum > 0 && pass == true { // 默认是全部通过 168 | // 添加参与人 169 | err := AddParticipantTx(userID, username, company, comment, task.ID, task.ProcInstID, task.Step, tx) 170 | if err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | // 流转到下一流程 176 | // nodeInfos, err := GetExecNodeInfosByProcInstID(task.ProcInstID) 177 | // if err != nil { 178 | // return err 179 | // } 180 | err = MoveStageByProcInstID(userID, username, company, comment, candidate, task.ID, task.ProcInstID, task.Step, pass, tx) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // WithDrawTaskByToken 撤回任务 189 | func WithDrawTaskByToken(token string, receiver *TaskReceiver) error { 190 | userinfo, err := GetUserinfoFromRedis(token) 191 | if err != nil { 192 | return err 193 | } 194 | if len(userinfo.ID) == 0 { 195 | return errors.New("保存在redis中的【用户信息 userinfo】字段 ID 不能为空!!") 196 | } 197 | if len(userinfo.Username) == 0 { 198 | return errors.New("保存在redis中的【用户信息 userinfo】字段 username 不能为空!!") 199 | } 200 | if len(userinfo.Company) == 0 { 201 | return errors.New("保存在redis中的【用户信息 userinfo】字段 company 不能为空") 202 | } 203 | return WithDrawTask(receiver.TaskID, receiver.ProcInstID, userinfo.ID, userinfo.Username, userinfo.Company, receiver.Comment) 204 | } 205 | 206 | // WithDrawTask 撤回任务 207 | func WithDrawTask(taskID, procInstID int, userID, username, company, comment string) error { 208 | var err1, err2 error 209 | var currentTask, lastTask *model.Task 210 | var timesx time.Time 211 | var wg sync.WaitGroup 212 | timesx = time.Now() 213 | wg.Add(2) 214 | go func() { 215 | currentTask, err1 = GetTaskByID(taskID) 216 | wg.Done() 217 | }() 218 | go func() { 219 | lastTask, err2 = GetTaskLastByProInstID(procInstID) 220 | wg.Done() 221 | }() 222 | wg.Wait() 223 | if err1 != nil { 224 | if err1 == gorm.ErrRecordNotFound { 225 | return errors.New("任务不存在") 226 | } 227 | return err1 228 | } 229 | if err2 != nil { 230 | if err2 == gorm.ErrRecordNotFound { 231 | return errors.New("找不到流程实例id为【" + fmt.Sprintf("%d", procInstID) + "】的任务,无权撤回") 232 | } 233 | return err2 234 | } 235 | // str1,_:=util.ToJSONStr(currentTask) 236 | // str2,_:=util.ToJSONStr(lastTask) 237 | // fmt.Println(str1) 238 | // fmt.Println(str2) 239 | if currentTask.Step == 0 { 240 | return errors.New("开始位置无法撤回") 241 | } 242 | if lastTask.Assignee != userID { 243 | return errors.New("只能撤回本人审批过的任务!!") 244 | } 245 | if currentTask.IsFinished { 246 | return errors.New("已经审批结束,无法撤回!") 247 | } 248 | if currentTask.UnCompleteNum != currentTask.MemberCount { 249 | return errors.New("已经有人审批过了,无法撤回!") 250 | } 251 | sub := currentTask.Step - lastTask.Step 252 | if math.Abs(float64(sub)) != 1 { 253 | return errors.New("只能撤回相邻的任务!!") 254 | } 255 | var pass = false 256 | if sub < 0 { 257 | pass = true 258 | } 259 | fmt.Printf("判断是否可以撤回,耗时:%v\n", time.Since(timesx)) 260 | timesx = time.Now() 261 | tx := model.GetTx() 262 | // 更新当前的任务 263 | currentTask.IsFinished = true 264 | err := currentTask.UpdateTx(tx) 265 | if err != nil { 266 | tx.Rollback() 267 | return err 268 | } 269 | // 撤回 270 | err = MoveStageByProcInstID(userID, username, company, comment, "", currentTask.ID, procInstID, currentTask.Step, pass, tx) 271 | if err != nil { 272 | tx.Rollback() 273 | return err 274 | } 275 | tx.Commit() 276 | fmt.Printf("撤回流程耗时:%v\n", time.Since(timesx)) 277 | return nil 278 | } 279 | 280 | // MoveStageByProcInstID MoveStageByProcInstID 281 | func MoveStageByProcInstID(userID, username, company, comment, candidate string, taskID, procInstID, step int, pass bool, tx *gorm.DB) (err error) { 282 | nodeInfos, err := GetExecNodeInfosByProcInstID(procInstID) 283 | if err != nil { 284 | return err 285 | } 286 | return MoveStage(nodeInfos, userID, username, company, comment, candidate, taskID, procInstID, step, pass, tx) 287 | } 288 | 289 | // MoveStage MoveStage 290 | // 流程流转 291 | func MoveStage(nodeInfos []*flow.NodeInfo, userID, username, company, comment, candidate string, taskID, procInstID, step int, pass bool, tx *gorm.DB) (err error) { 292 | // 添加上一步的参与人 293 | err = AddParticipantTx(userID, username, company, comment, taskID, procInstID, step, tx) 294 | if err != nil { 295 | return err 296 | } 297 | if pass { 298 | step++ 299 | if step-1 > len(nodeInfos) { 300 | return errors.New("已经结束无法流转到下一个节点") 301 | } 302 | } else { 303 | step-- 304 | if step < 0 { 305 | return errors.New("处于开始位置,无法回退到上一个节点") 306 | } 307 | } 308 | // 指定下一步执行人 309 | if len(candidate) > 0 { 310 | nodeInfos[step].Aprover = candidate 311 | } 312 | // 判断下一流程: 如果是审批人是:抄送人 313 | // fmt.Printf("下一审批人类型:%s\n", nodeInfos[step].AproverType) 314 | // fmt.Println(nodeInfos[step].AproverType == flow.NodeTypes[flow.NOTIFIER]) 315 | if nodeInfos[step].AproverType == flow.NodeTypes[flow.NOTIFIER] { 316 | // 生成新的任务 317 | var task = model.Task{ 318 | NodeID: flow.NodeTypes[flow.NOTIFIER], 319 | Step: step, 320 | ProcInstID: procInstID, 321 | IsFinished: true, 322 | } 323 | task.IsFinished = true 324 | _, err := task.NewTaskTx(tx) 325 | if err != nil { 326 | return err 327 | } 328 | // 添加抄送人 329 | err = AddNotifierTx(nodeInfos[step].Aprover, company, step, procInstID, tx) 330 | if err != nil { 331 | return err 332 | } 333 | return MoveStage(nodeInfos, userID, username, company, comment, candidate, taskID, procInstID, step, pass, tx) 334 | } 335 | if pass { 336 | return MoveToNextStage(nodeInfos, userID, company, taskID, procInstID, step, tx) 337 | } 338 | return MoveToPrevStage(nodeInfos, userID, company, taskID, procInstID, step, tx) 339 | } 340 | 341 | // MoveToNextStage MoveToNextStage 342 | //通过 343 | func MoveToNextStage(nodeInfos []*flow.NodeInfo, userID, company string, currentTaskID, procInstID, step int, tx *gorm.DB) error { 344 | var currentTime = util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS) 345 | var task = getNewTask(nodeInfos, step, procInstID, currentTime) //新任务 346 | var procInst = &model.ProcInst{ // 流程实例要更新的字段 347 | NodeID: nodeInfos[step].NodeID, 348 | Candidate: nodeInfos[step].Aprover, 349 | } 350 | procInst.ID = procInstID 351 | if (step + 1) != len(nodeInfos) { // 下一步不是【结束】 352 | // 生成新的任务 353 | taksID, err := task.NewTaskTx(tx) 354 | if err != nil { 355 | return err 356 | } 357 | // 添加candidate group 358 | err = AddCandidateGroupTx(nodeInfos[step].Aprover, company, step, taksID, procInstID, tx) 359 | if err != nil { 360 | return err 361 | } 362 | // 更新流程实例 363 | procInst.TaskID = taksID 364 | err = UpdateProcInst(procInst, tx) 365 | if err != nil { 366 | return err 367 | } 368 | } else { // 最后一步直接结束 369 | // 生成新的任务 370 | task.IsFinished = true 371 | task.ClaimTime = currentTime 372 | taksID, err := task.NewTaskTx(tx) 373 | if err != nil { 374 | return err 375 | } 376 | // 删除候选用户组 377 | err = DelCandidateByProcInstID(procInstID, tx) 378 | if err != nil { 379 | return err 380 | } 381 | // 更新流程实例 382 | procInst.TaskID = taksID 383 | procInst.EndTime = currentTime 384 | procInst.IsFinished = true 385 | procInst.Candidate = "审批结束" 386 | err = UpdateProcInst(procInst, tx) 387 | if err != nil { 388 | return err 389 | } 390 | } 391 | return nil 392 | } 393 | 394 | // MoveToPrevStage MoveToPrevStage 395 | // 驳回 396 | func MoveToPrevStage(nodeInfos []*flow.NodeInfo, userID, company string, currentTaskID, procInstID, step int, tx *gorm.DB) error { 397 | // 生成新的任务 398 | var task = getNewTask(nodeInfos, step, procInstID, util.FormatDate(time.Now(), util.YYYY_MM_DD_HH_MM_SS)) //新任务 399 | taksID, err := task.NewTaskTx(tx) 400 | if err != nil { 401 | return err 402 | } 403 | var procInst = &model.ProcInst{ // 流程实例要更新的字段 404 | NodeID: nodeInfos[step].NodeID, 405 | Candidate: nodeInfos[step].Aprover, 406 | TaskID: taksID, 407 | } 408 | procInst.ID = procInstID 409 | err = UpdateProcInst(procInst, tx) 410 | if err != nil { 411 | return err 412 | } 413 | if step == 0 { // 流程回到起始位置,注意起始位置为0, 414 | err = AddCandidateUserTx(nodeInfos[step].Aprover, company, step, taksID, procInstID, tx) 415 | if err != nil { 416 | return err 417 | } 418 | return nil 419 | } 420 | // 添加candidate group 421 | err = AddCandidateGroupTx(nodeInfos[step].Aprover, company, step, taksID, procInstID, tx) 422 | if err != nil { 423 | return err 424 | } 425 | return nil 426 | } 427 | func getNewTask(nodeInfos []*flow.NodeInfo, step, procInstID int, currentTime string) *model.Task { 428 | var task = &model.Task{ // 新任务 429 | NodeID: nodeInfos[step].NodeID, 430 | Step: step, 431 | CreateTime: currentTime, 432 | ProcInstID: procInstID, 433 | MemberCount: nodeInfos[step].MemberCount, 434 | UnCompleteNum: nodeInfos[step].MemberCount, 435 | ActType: nodeInfos[step].ActType, 436 | } 437 | return task 438 | } 439 | -------------------------------------------------------------------------------- /workflow-engine/flow/node.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "container/list" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/mumushuiding/util" 14 | ) 15 | 16 | // Node represents a specific logical unit of processing and routing 17 | // in a workflow. 18 | // 流程中的一个节点 19 | type Node struct { 20 | Name string `json:"name,omitempty"` 21 | Type string `json:"type,omitempty"` 22 | NodeID string `json:"nodeId,omitempty"` 23 | PrevID string `json:"prevId,omitempty"` 24 | ChildNode *Node `json:"childNode,omitempty"` 25 | ConditionNodes []*Node `json:"conditionNodes,omitempty"` 26 | Properties *NodeProperties `json:"properties,omitempty"` 27 | } 28 | 29 | // ActionConditionType 条件类型 30 | type ActionConditionType int 31 | 32 | const ( 33 | // RANGE 条件类型: 范围 34 | RANGE ActionConditionType = iota 35 | // VALUE 条件类型: 值 36 | VALUE 37 | ) 38 | 39 | // ActionConditionTypes 所有条件类型 40 | var ActionConditionTypes = [...]string{RANGE: "dingtalk_actioner_range_condition", VALUE: "dingtalk_actioner_value_condition"} 41 | 42 | // NodeType 节点类型 43 | type NodeType int 44 | 45 | const ( 46 | // START 类型start 47 | START NodeType = iota 48 | ROUTE 49 | CONDITION 50 | APPROVER 51 | NOTIFIER 52 | ) 53 | 54 | // ActionRuleType 审批人类型 55 | type ActionRuleType int 56 | 57 | const ( 58 | MANAGER ActionRuleType = iota 59 | LABEL 60 | ) 61 | 62 | // NodeTypes 节点类型 63 | var NodeTypes = [...]string{START: "start", ROUTE: "route", CONDITION: "condition", APPROVER: "approver", NOTIFIER: "notifier"} 64 | var actionRuleTypes = [...]string{MANAGER: "target_management", LABEL: "target_label"} 65 | 66 | type NodeInfoType int 67 | 68 | const ( 69 | STARTER NodeInfoType = iota 70 | ) 71 | 72 | var NodeInfoTypes = [...]string{STARTER: "starter"} 73 | 74 | type ActionerRule struct { 75 | Type string `json:"type,omitempty"` 76 | LabelNames string `json:"labelNames,omitempty"` 77 | Labels int `json:"labels,omitempty"` 78 | IsEmpty bool `json:"isEmpty,omitempty"` 79 | // 表示需要通过的人数 如果是会签 80 | MemberCount int8 `json:"memberCount,omitempty"` 81 | // and 表示会签 or表示或签,默认为或签 82 | ActType string `json:"actType,omitempty"` 83 | Level int8 `json:"level,omitempty"` 84 | AutoUp bool `json:"autoUp,omitempty"` 85 | } 86 | type NodeProperties struct { 87 | // ONE_BY_ONE 代表依次审批 88 | ActivateType string `json:"activateType,omitempty"` 89 | AgreeAll bool `json:"agreeAll,omitempty"` 90 | Conditions [][]*NodeCondition `json:"conditions,omitempty"` 91 | ActionerRules []*ActionerRule `json:"actionerRules,omitempty"` 92 | NoneActionerAction string `json:"noneActionerAction,omitempty"` 93 | } 94 | type NodeCondition struct { 95 | Type string `json:"type,omitempty"` 96 | ParamKey string `json:"paramKey,omitempty"` 97 | ParamLabel string `json:"paramLabel,omitempty"` 98 | IsEmpty bool `json:"isEmpty,omitempty"` 99 | // 类型为range 100 | LowerBound string `json:"lowerBound,omitempty"` 101 | LowerBoundEqual string `json:"lowerBoundEqual,omitempty"` 102 | UpperBoundEqual string `json:"upperBoundEqual,omitempty"` 103 | UpperBound string `json:"upperBound,omitempty"` 104 | BoundEqual string `json:"boundEqual,omitempty"` 105 | Unit string `json:"unit,omitempty"` 106 | // 类型为 value 107 | ParamValues []string `json:"paramValues,omitempty"` 108 | OriValue []string `json:"oriValue,omitempty"` 109 | Conds []*NodeCond `json:"conds,omitempty"` 110 | } 111 | type NodeCond struct { 112 | Type string `json:"type,omitempty"` 113 | Value string `json:"value,omitempty"` 114 | Attrs *NodeUser `json:"attrs,omitempty"` 115 | } 116 | type NodeUser struct { 117 | Name string `json:"name,omitempty"` 118 | Avatar string `json:"avatar,omitempty"` 119 | } 120 | 121 | // NodeInfo 节点信息 122 | type NodeInfo struct { 123 | NodeID string `json:"nodeId"` 124 | Type string `json:"type"` 125 | Aprover string `json:"approver"` 126 | AproverType string `json:"aproverType"` 127 | MemberCount int8 `json:"memberCount"` 128 | Level int8 `json:"level"` 129 | ActType string `json:"actType"` 130 | } 131 | 132 | // GetProcessConfigFromJSONFile test 133 | func (n *Node) GetProcessConfigFromJSONFile() { 134 | file, err := os.Open("D:/Workspaces/go/src/github.com/go-workflow/go-workflow/processConfig2.json") 135 | if err != nil { 136 | log.Printf("cannot open file processConfig.json:%v", err) 137 | panic(err) 138 | } 139 | decoder := json.NewDecoder(file) 140 | err = decoder.Decode(n) 141 | if err != nil { 142 | log.Printf("decode processConfig.json failed:%v", err) 143 | } 144 | } 145 | func (n *Node) add2ExecutionList(list *list.List) { 146 | switch n.Type { 147 | case NodeTypes[APPROVER], NodeTypes[NOTIFIER]: 148 | var aprover string 149 | if n.Properties.ActionerRules[0].Type == actionRuleTypes[MANAGER] { 150 | aprover = "主管" 151 | } else { 152 | aprover = n.Properties.ActionerRules[0].LabelNames 153 | } 154 | list.PushBack(NodeInfo{ 155 | NodeID: n.NodeID, 156 | Type: n.Properties.ActionerRules[0].Type, 157 | Aprover: aprover, 158 | AproverType: n.Type, 159 | MemberCount: n.Properties.ActionerRules[0].MemberCount, 160 | ActType: n.Properties.ActionerRules[0].ActType, 161 | }) 162 | break 163 | default: 164 | } 165 | } 166 | 167 | // IfProcessConifgIsValid 检查流程配置是否有效 168 | func IfProcessConifgIsValid(node *Node) error { 169 | // 节点名称是否有效 170 | if len(node.NodeID) == 0 { 171 | return errors.New("节点的【nodeId】不能为空!!") 172 | } 173 | // 检查类型是否有效 174 | if len(node.Type) == 0 { 175 | return errors.New("节点【" + node.NodeID + "】的类型【type】不能为空") 176 | } 177 | var flag = false 178 | for _, val := range NodeTypes { 179 | if val == node.Type { 180 | flag = true 181 | break 182 | } 183 | } 184 | if !flag { 185 | str, _ := util.ToJSONStr(NodeTypes) 186 | return errors.New("节点【" + node.NodeID + "】的类型为【" + node.Type + "】,为无效类型,有效类型为" + str) 187 | } 188 | // 当前节点是否设置有审批人 189 | if node.Type == NodeTypes[APPROVER] || node.Type == NodeTypes[NOTIFIER] { 190 | if node.Properties == nil || node.Properties.ActionerRules == nil { 191 | return errors.New("节点【" + node.NodeID + "】的Properties属性不能为空,如:`\"properties\": {\"actionerRules\": [{\"type\": \"target_label\",\"labelNames\": \"人事\",\"memberCount\": 1,\"actType\": \"and\"}],}`") 192 | } 193 | } 194 | // 条件节点是否存在 195 | if node.ConditionNodes != nil { // 存在条件节点 196 | if len(node.ConditionNodes) == 1 { 197 | return errors.New("节点【" + node.NodeID + "】条件节点下的节点数必须大于1") 198 | } 199 | // 根据条件变量选择节点索引 200 | err := CheckConditionNode(node.ConditionNodes) 201 | if err != nil { 202 | return err 203 | } 204 | } 205 | 206 | // 子节点是否存在 207 | if node.ChildNode != nil { 208 | return IfProcessConifgIsValid(node.ChildNode) 209 | } 210 | return nil 211 | } 212 | 213 | // CheckConditionNode 检查条件节点 214 | func CheckConditionNode(nodes []*Node) error { 215 | for _, node := range nodes { 216 | if node.Properties == nil { 217 | return errors.New("节点【" + node.NodeID + "】的Properties对象为空值!!") 218 | } 219 | if len(node.Properties.Conditions) == 0 { 220 | return errors.New("节点【" + node.NodeID + "】的Conditions对象为空值!!") 221 | } 222 | err := IfProcessConifgIsValid(node) 223 | if err != nil { 224 | return err 225 | } 226 | } 227 | return nil 228 | } 229 | 230 | // ParseProcessConfig 解析流程定义json数据 231 | func ParseProcessConfig(node *Node, variable *map[string]string) (*list.List, error) { 232 | // defer fmt.Println("----------解析结束--------") 233 | list := list.New() 234 | err := parseProcessConfig(node, variable, list) 235 | return list, err 236 | } 237 | func parseProcessConfig(node *Node, variable *map[string]string, list *list.List) (err error) { 238 | // fmt.Printf("nodeId=%s\n", node.NodeID) 239 | node.add2ExecutionList(list) 240 | // 存在条件节点 241 | if node.ConditionNodes != nil { 242 | // 如果条件节点只有一个或者条件只有一个,直接返回第一个 243 | if variable == nil || len(node.ConditionNodes) == 1 { 244 | err = parseProcessConfig(node.ConditionNodes[0].ChildNode, variable, list) 245 | if err != nil { 246 | return err 247 | } 248 | } else { 249 | // 根据条件变量选择节点索引 250 | condNode, err := GetConditionNode(node.ConditionNodes, variable) 251 | if err != nil { 252 | return err 253 | } 254 | if condNode == nil { 255 | str, _ := util.ToJSONStr(variable) 256 | return errors.New("节点【" + node.NodeID + "】找不到符合条件的子节点,检查变量【var】值是否匹配," + str) 257 | // panic(err) 258 | } 259 | err = parseProcessConfig(condNode, variable, list) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | } 265 | } 266 | // 存在子节点 267 | if node.ChildNode != nil { 268 | err = parseProcessConfig(node.ChildNode, variable, list) 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | 276 | // GetConditionNode 获取条件节点 277 | func GetConditionNode(nodes []*Node, maps *map[string]string) (result *Node, err error) { 278 | map2 := *maps 279 | for _, node := range nodes { 280 | var flag int 281 | for _, v := range node.Properties.Conditions[0] { 282 | paramValue := map2[v.ParamKey] 283 | if len(paramValue) == 0 { 284 | return nil, errors.New("流程启动变量【var】的key【" + v.ParamKey + "】的值不能为空") 285 | } 286 | yes, err := checkConditions(v, paramValue) 287 | if err != nil { 288 | return nil, err 289 | } 290 | if yes { 291 | flag++ 292 | } 293 | } 294 | // fmt.Printf("flag=%d\n", flag) 295 | // 满足所有条件 296 | if flag == len(node.Properties.Conditions[0]) { 297 | result = node 298 | } 299 | } 300 | return result, nil 301 | } 302 | func getConditionNode(nodes []*Node, maps *map[string]string) (result *Node, err error) { 303 | map2 := *maps 304 | // 获取所有conditionNodes 305 | getNodesChan := func() <-chan *Node { 306 | nodesChan := make(chan *Node, len(nodes)) 307 | go func() { 308 | // defer fmt.Println("关闭nodeChan通道") 309 | defer close(nodesChan) 310 | for _, v := range nodes { 311 | nodesChan <- v 312 | } 313 | }() 314 | return nodesChan 315 | } 316 | 317 | //获取所有conditions 318 | getConditionNode := func(nodesChan <-chan *Node, done <-chan interface{}) <-chan *Node { 319 | resultStream := make(chan *Node, 2) 320 | go func() { 321 | // defer fmt.Println("关闭resultStream通道") 322 | defer close(resultStream) 323 | for { 324 | select { 325 | case <-done: 326 | return 327 | case <-time.After(10 * time.Millisecond): 328 | fmt.Println("Time out.") 329 | case node, ok := <-nodesChan: 330 | if ok { 331 | // for _, v := range node.Properties.Conditions[0] { 332 | // conStream <- v 333 | // fmt.Printf("接收 condition:%s\n", v.Type) 334 | // } 335 | var flag int 336 | for _, v := range node.Properties.Conditions[0] { 337 | // fmt.Println(v.ParamKey) 338 | // fmt.Println(map2[v.ParamKey]) 339 | paramValue := map2[v.ParamKey] 340 | if len(paramValue) == 0 { 341 | log.Printf("key:%s的值为空\n", v.ParamKey) 342 | // nodeAndErr.Err = errors.New("key:" + v.ParamKey + "的值为空") 343 | break 344 | } 345 | yes, err := checkConditions(v, paramValue) 346 | if err != nil { 347 | // nodeAndErr.Err = err 348 | break 349 | } 350 | if yes { 351 | flag++ 352 | } 353 | } 354 | // fmt.Printf("flag=%d\n", flag) 355 | // 满足所有条件 356 | if flag == len(node.Properties.Conditions[0]) { 357 | // fmt.Printf("flag=%d\n,send node:%s\n", flag, node.NodeID) 358 | resultStream <- node 359 | } else { 360 | // fmt.Println("条件不完全满足") 361 | } 362 | } 363 | } 364 | } 365 | }() 366 | return resultStream 367 | } 368 | done := make(chan interface{}) 369 | // defer fmt.Println("结束所有goroutine") 370 | defer close(done) 371 | nodeStream := getNodesChan() 372 | // for i := len(nodes); i > 0; i-- { 373 | // getConditionNode(resultStream, nodeStream, done) 374 | // } 375 | resultStream := getConditionNode(nodeStream, done) 376 | // for node := range resultStream { 377 | // return node, nil 378 | // } 379 | for { 380 | select { 381 | case <-time.After(1 * time.Second): 382 | fmt.Println("Time out") 383 | return 384 | case node := <-resultStream: 385 | // result = node 386 | return node, nil 387 | } 388 | } 389 | // setResult(resultStream, done) 390 | // time.Sleep(1 * time.Second) 391 | // log.Println("----------寻找节点结束--------") 392 | // return result, err 393 | } 394 | func checkConditions(cond *NodeCondition, value string) (bool, error) { 395 | // 判断类型 396 | switch cond.Type { 397 | case ActionConditionTypes[RANGE]: 398 | val, err := strconv.Atoi(value) 399 | if err != nil { 400 | return false, err 401 | } 402 | if len(cond.LowerBound) == 0 && len(cond.UpperBound) == 0 && len(cond.LowerBoundEqual) == 0 && len(cond.UpperBoundEqual) == 0 && len(cond.BoundEqual) == 0 { 403 | return false, errors.New("条件【" + cond.Type + "】的上限或者下限值不能全为空") 404 | } 405 | // 判断下限,lowerBound 406 | if len(cond.LowerBound) > 0 { 407 | low, err := strconv.Atoi(cond.LowerBound) 408 | if err != nil { 409 | return false, err 410 | } 411 | if val <= low { 412 | // fmt.Printf("val:%d小于lowerBound:%d\n", val, low) 413 | return false, nil 414 | } 415 | } 416 | if len(cond.LowerBoundEqual) > 0 { 417 | le, err := strconv.Atoi(cond.LowerBoundEqual) 418 | if err != nil { 419 | return false, err 420 | } 421 | if val < le { 422 | // fmt.Printf("val:%d小于lowerBound:%d\n", val, low) 423 | return false, nil 424 | } 425 | } 426 | // 判断上限,upperBound包含等于 427 | if len(cond.UpperBound) > 0 { 428 | upper, err := strconv.Atoi(cond.UpperBound) 429 | if err != nil { 430 | return false, err 431 | } 432 | if val >= upper { 433 | return false, nil 434 | } 435 | } 436 | if len(cond.UpperBoundEqual) > 0 { 437 | ge, err := strconv.Atoi(cond.UpperBoundEqual) 438 | if err != nil { 439 | return false, err 440 | } 441 | if val > ge { 442 | return false, nil 443 | } 444 | } 445 | if len(cond.BoundEqual) > 0 { 446 | equal, err := strconv.Atoi(cond.BoundEqual) 447 | if err != nil { 448 | return false, err 449 | } 450 | if val != equal { 451 | return false, nil 452 | } 453 | } 454 | return true, nil 455 | case ActionConditionTypes[VALUE]: 456 | if len(cond.ParamValues) == 0 { 457 | return false, errors.New("条件节点【" + cond.Type + "】的 【paramValues】数组不能为空,值如:'paramValues:['调休','年假']") 458 | } 459 | for _, val := range cond.ParamValues { 460 | if value == val { 461 | return true, nil 462 | } 463 | } 464 | // log.Printf("key:" + cond.ParamKey + "找不到对应的值") 465 | return false, nil 466 | default: 467 | str, _ := util.ToJSONStr(ActionConditionTypes) 468 | return false, errors.New("未知的NodeCondition类型【" + cond.Type + "】,正确类型应为以下中的一个:" + str) 469 | } 470 | } 471 | --------------------------------------------------------------------------------