├── Makefile ├── misc └── migrate.sql ├── .gitignore ├── src ├── model │ ├── request │ │ ├── taskrequest.go │ │ ├── handlerequest.go │ │ ├── pagingrequest.go │ │ ├── roleusersrequest.go │ │ ├── instancerequest.go │ │ └── definitionrequest.go │ ├── response │ │ ├── taskresponse.go │ │ ├── pagineresponse.go │ │ └── instanceresponse.go │ ├── classify.go │ ├── dto │ │ ├── parallelinfo.go │ │ ├── edge.go │ │ ├── node.go │ │ ├── structure.go │ │ └── statearray.go │ ├── tenant.go │ ├── entitybase.go │ ├── circulationhistory.go │ ├── definition.go │ ├── userrole.go │ └── instance.go ├── util │ ├── uuid.go │ ├── time.go │ ├── file.go │ ├── math.go │ ├── expression.go │ ├── user.go │ ├── slice.go │ ├── int.go │ ├── pagingutil.go │ ├── converter.go │ ├── error.go │ └── envloader.go ├── initialize │ ├── init.go │ ├── cache.go │ ├── logger.go │ ├── config.go │ └── db.go ├── config │ ├── appsettings.yaml │ └── config.go ├── global │ ├── global.go │ ├── constant │ │ ├── nodetype.go │ │ └── enum.go │ ├── shared │ │ └── dbutil.go │ └── response │ │ └── httpresponse.go ├── router │ ├── router.swagger.go │ ├── router.go │ └── router.apis.go ├── middleware │ ├── authmiddleware.go │ └── tenantmiddleware.go ├── controller │ ├── indexcontroller.go │ ├── historycontroller.go │ ├── roleuserscontroller.go │ ├── instancecontroller.go │ └── definitioncontroller.go ├── service │ ├── engine │ │ ├── processengine.start.go │ │ ├── processengine.history.go │ │ ├── processengine.validator.go │ │ ├── processengine.circulation.go │ │ ├── processengine.exclusive.go │ │ ├── processengine.countersign.go │ │ ├── processengine.parallel.go │ │ └── processengine.go │ ├── historyservice.go │ ├── roleusersservice.go │ ├── definitionservice.go │ └── instanceservice.go └── docs │ ├── swagger.yaml │ └── swagger.json ├── main.go ├── go.mod ├── Dockerfile ├── LICENSE ├── README-ZH.md ├── README.md └── go.sum /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: start build 2 | 3 | swagger: 4 | swag init -o ./src/docs -------------------------------------------------------------------------------- /misc/migrate.sql: -------------------------------------------------------------------------------- 1 | create index on wf.process_instance using gin ((state->'processor')); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | coverage.txt 3 | .idea 4 | .vscode 5 | log 6 | appsettings.*.yaml 7 | *.pdf 8 | *.png 9 | node_modules/ 10 | workflow 11 | workflow.exe -------------------------------------------------------------------------------- /src/model/request/taskrequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/2/9 18:43 4 | * @Desc: 5 | */ 6 | package request 7 | 8 | type TaskRequest struct { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/model/response/taskresponse.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/2/9 18:53 4 | * @Desc: 5 | */ 6 | package response 7 | 8 | type TaskResponse struct { 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/model/classify.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 13:12 4 | * @Desc: 流程分类 5 | */ 6 | package model 7 | 8 | // 流程分类 9 | type Classify struct { 10 | AuditableBase 11 | Name string `json:"name" form:"name"` // 分类名称 12 | } 13 | -------------------------------------------------------------------------------- /src/model/dto/parallelinfo.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/28 21:37 4 | * @Desc: 5 | */ 6 | package dto 7 | 8 | // 并行网关传参 9 | type RelationInfo struct { 10 | SourceNode Node 11 | LinkedEdge Edge 12 | TargetNode Node 13 | } 14 | -------------------------------------------------------------------------------- /src/util/uuid.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/19 0:09 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "strings" 10 | 11 | uuid "github.com/satori/go.uuid" 12 | ) 13 | 14 | func GenUUID() string { 15 | return strings.Replace(uuid.NewV4().String(), "-", "", -1) 16 | } 17 | -------------------------------------------------------------------------------- /src/initialize/init.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/27 16:47 4 | * @Desc: 应用启动时初始化相关的依赖 5 | */ 6 | package initialize 7 | 8 | func init() { 9 | // logger 10 | setupLogger() 11 | 12 | // 配置 13 | setupConfig() 14 | 15 | // 数据库连接 16 | setupDbConn() 17 | 18 | // 内存缓存 19 | setupCache() 20 | } 21 | -------------------------------------------------------------------------------- /src/model/tenant.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 14:59 4 | * @Desc: 5 | */ 6 | package model 7 | 8 | import "time" 9 | 10 | type Tenant struct { 11 | EntityBase 12 | Name string `json:"name"` 13 | CreateTime time.Time `gorm:"default:now();type:timestamp" json:"createTime" form:"createTime"` 14 | } 15 | -------------------------------------------------------------------------------- /src/config/appsettings.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: 'workflow-engine' 3 | 4 | db: 5 | host: 127.0.0.1 6 | port: 5432 7 | database: postgres 8 | username: postgres 9 | password: pwd 10 | auto_migrate: false # 自动迁移 11 | max_idle_conn: 20 # 最多的空闲连接数,默认2 12 | max_open_conn: 50 # 最多的打开连接数,默认0(无限制) 13 | log_mode: true # false的话gorm只会输出错误日志,true会输出详细日志 14 | -------------------------------------------------------------------------------- /src/model/response/pagineresponse.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/17 22:42 4 | * @Desc: 5 | */ 6 | package response 7 | 8 | type PagingResponse struct { 9 | TotalCount int64 `json:"totalCount"` // 总数 10 | CurrentCount int64 `json:"currentCount"` // 当前数量 11 | Data interface{} `json:"data"` // 具体数据 12 | } 13 | -------------------------------------------------------------------------------- /src/util/time.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/19 19:05 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | // 时间格式化 14 | func FmtDuration(d time.Duration) string { 15 | d = d.Round(time.Minute) 16 | h := d / time.Hour 17 | d -= h * time.Hour 18 | m := d / time.Minute 19 | 20 | return fmt.Sprintf("%02d小时 %02d分钟", h, m) 21 | } 22 | -------------------------------------------------------------------------------- /src/global/global.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/01/12 15:11:13 4 | * @Desc: 全局对象, 将在程序启动后初始化 5 | */ 6 | package global 7 | 8 | import ( 9 | "github.com/patrickmn/go-cache" 10 | "github.com/sirupsen/logrus" 11 | "gorm.io/gorm" 12 | 13 | "workflow/src/config" 14 | ) 15 | 16 | var ( 17 | BankConfig config.Config 18 | BankLogger *logrus.Logger 19 | BankDb *gorm.DB 20 | BankCache *cache.Cache 21 | ) 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | _ "workflow/src/docs" 9 | _ "workflow/src/initialize" 10 | "workflow/src/router" 11 | ) 12 | 13 | func main() { 14 | r := router.Setup() 15 | 16 | port := os.Getenv("PORT") 17 | if port == "" { 18 | port = "8082" 19 | } 20 | 21 | log.Printf("应用启动, 监听的端口为: %s", port) 22 | log.Fatalf("应用启动失败,原因: %s\n", http.ListenAndServe(":"+port, r).Error()) 23 | } 24 | -------------------------------------------------------------------------------- /src/util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "os" 4 | 5 | func PathExists(path string) bool { 6 | _, err := os.Stat(path) //os.Stat get the file info 7 | if err != nil { 8 | if os.IsExist(err) { 9 | return true 10 | } 11 | return false 12 | } 13 | return true 14 | } 15 | 16 | func IsDir(path string) bool { 17 | s, err := os.Stat(path) 18 | if err != nil { 19 | return false 20 | } 21 | return s.IsDir() 22 | } 23 | 24 | func IsFile(path string) bool { 25 | return !IsDir(path) 26 | } -------------------------------------------------------------------------------- /src/global/constant/nodetype.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/19 16:36 4 | * @Desc: 5 | */ 6 | package constant 7 | 8 | const ( 9 | ExclusiveGateway = "exclusiveGateway" // 排他网关 10 | ParallelGateway = "parallelGateway" // 并行网关 11 | InclusiveGateway = "inclusiveGateway" // 包容网关 12 | START = "start" // 开始事件 13 | UserTask = "userTask" // 用户任务 14 | ReceiveTask = "receiveTask" // 接收任务 15 | ScriptTask = "scriptTask" // 脚本任务 16 | End = "end" // 结束事件 17 | ) 18 | -------------------------------------------------------------------------------- /src/util/math.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/23 13:57 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | func Pow(x int, n int) int { 9 | if x == 0 { 10 | return 0 11 | } 12 | result := calPow(x, n) 13 | if n < 0 { 14 | result = 1 / result 15 | } 16 | return result 17 | } 18 | 19 | func calPow(x int, n int) int { 20 | if n == 0 { 21 | return 1 22 | } 23 | if n == 1 { 24 | return x 25 | } 26 | 27 | // 向右移动一位 28 | result := calPow(x, n>>1) 29 | result *= result 30 | 31 | // 如果n是奇数 32 | if n&1 == 1 { 33 | result *= x 34 | } 35 | 36 | return result 37 | } 38 | -------------------------------------------------------------------------------- /src/model/entitybase.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/14 23:11 4 | * @Desc: 5 | */ 6 | package model 7 | 8 | import "time" 9 | 10 | type EntityBase struct { 11 | Id int `gorm:"primarykey" json:"id" form:"id"` 12 | } 13 | 14 | type AuditableBase struct { 15 | EntityBase 16 | CreateTime time.Time `gorm:"default:now();type:timestamp" json:"createTime" form:"createTime"` 17 | UpdateTime time.Time `gorm:"default:now();type:timestamp" json:"updateTime" form:"updateTime"` 18 | CreateBy string `json:"createBy" form:"createBy"` 19 | UpdateBy string `json:"updateBy" form:"updateBy"` 20 | } 21 | -------------------------------------------------------------------------------- /src/router/router.swagger.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 15:47 4 | * @Desc: 5 | */ 6 | package router 7 | 8 | import ( 9 | "net/http" 10 | "path" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/swaggo/echo-swagger" 14 | ) 15 | 16 | const ( 17 | _SWAGGER_BASE_PATH = "/api/wf/swagger" 18 | ) 19 | 20 | func RegisterSwagger(r *echo.Echo) { 21 | r.GET(path.Join(_SWAGGER_BASE_PATH, "/*"), echoSwagger.WrapHandler) 22 | r.GET(_SWAGGER_BASE_PATH, func(c echo.Context) error { 23 | return c.Redirect(http.StatusMovedPermanently, path.Join(_SWAGGER_BASE_PATH, "index.html")) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/config/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2020/9/30 15:13 4 | * @Desc: config model 5 | */ 6 | package config 7 | 8 | type Config struct { 9 | App App `yaml:"app"` 10 | Db Db `yaml:"db"` 11 | } 12 | 13 | type App struct { 14 | Name string `yaml:"name"` 15 | EnableSwagger bool `yaml:"enable_swagger"` 16 | } 17 | 18 | type Db struct { 19 | Host string `yaml:"host"` 20 | Port int `yaml:"port"` 21 | Database string `yaml:"database"` 22 | Username string `yaml:"username"` 23 | Password string `yaml:"password"` 24 | LogMode bool `yaml:"log_mode"` 25 | AutoMigrate bool `yaml:"auto_migrate"` 26 | } 27 | -------------------------------------------------------------------------------- /src/initialize/cache.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 15:44 4 | * @Desc: 初始化内存缓存 并 加载租户信息到缓存 5 | */ 6 | package initialize 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/patrickmn/go-cache" 12 | 13 | "workflow/src/global" 14 | "workflow/src/model" 15 | ) 16 | 17 | func setupCache() { 18 | c := cache.New(-1, -1) 19 | global.BankCache = c 20 | 21 | var tenants []model.Tenant 22 | err := global.BankDb. 23 | Model(&model.Tenant{}). 24 | Find(&tenants). 25 | Error 26 | if err != nil { 27 | log.Fatalf("初始化租户失败, err:%s", err.Error()) 28 | } 29 | 30 | c.SetDefault("tenants", tenants) 31 | log.Printf("-------租户列表缓存成功,当前租户个数:%d--------\n", len(tenants)) 32 | } 33 | -------------------------------------------------------------------------------- /src/model/dto/edge.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/26 23:46 4 | * @Desc: 5 | */ 6 | package dto 7 | 8 | type Edge struct { 9 | Id string `json:"id"` 10 | Sort string `json:"sort"` 11 | Clazz string `json:"clazz"` 12 | Label string `json:"label"` 13 | Shape string `json:"shape"` 14 | Source string `json:"source"` 15 | Target string `json:"target"` 16 | SourceAnchor int64 `json:"sourceAnchor"` 17 | TargetAnchor int64 `json:"targetAnchor"` 18 | FlowProperties string `json:"flowProperties"` 19 | ConditionExpression string `json:"conditionExpression,omitempty"` // 表达式 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module workflow 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/ahmetb/go-linq/v3 v3.2.0 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 8 | github.com/antonmedv/expr v1.8.9 9 | github.com/jackc/pgx/v4 v4.10.1 10 | github.com/jinzhu/configor v1.2.1 11 | github.com/labstack/echo/v4 v4.1.17 12 | github.com/labstack/gommon v0.3.0 13 | github.com/lib/pq v1.3.0 14 | github.com/patrickmn/go-cache v2.1.0+incompatible 15 | github.com/pkg/errors v0.8.1 16 | github.com/satori/go.uuid v1.2.0 17 | github.com/sirupsen/logrus v1.4.2 18 | github.com/swaggo/echo-swagger v1.1.0 19 | github.com/swaggo/swag v1.7.0 20 | gorm.io/datatypes v1.0.0 21 | gorm.io/driver/postgres v1.0.6 22 | gorm.io/gorm v1.20.11 23 | ) 24 | -------------------------------------------------------------------------------- /src/util/expression.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/21 22:02 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/antonmedv/expr" 12 | ) 13 | 14 | func CalculateExpression(expression string, env map[string]interface{}) (result bool, err error) { 15 | program, err := expr.Compile(expression, expr.Env(env)) 16 | if err != nil { 17 | err = fmt.Errorf("处理失败, 请检查表达式和变量") 18 | return 19 | } 20 | 21 | output, err := expr.Run(program, env) 22 | if err != nil { 23 | err = fmt.Errorf("处理失败, 请检查表达式和变量") 24 | return 25 | } 26 | 27 | if v, succeed := output.(bool); succeed { 28 | result = v 29 | return 30 | } 31 | 32 | err = fmt.Errorf("处理失败, 请检查表达式和变量") 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /src/model/dto/node.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/26 23:58 4 | * @Desc: 5 | */ 6 | package dto 7 | 8 | type Node struct { 9 | X float64 `json:"x"` 10 | Y float64 `json:"y"` 11 | Id string `json:"id"` 12 | Size []int `json:"size"` 13 | Sort string `json:"sort"` 14 | Clazz string `json:"clazz"` 15 | Label string `json:"label"` 16 | Shape string `json:"shape"` 17 | IsHideNode bool `json:"isHideNode,omitempty"` 18 | AssignType string `json:"assignType,omitempty"` 19 | ActiveOrder bool `json:"activeOrder,omitempty"` 20 | AssignValue []string `json:"assignValue,omitempty"` 21 | IsCounterSign bool `json:"isCounterSign,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /src/util/user.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 18:18 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "encoding/json" 10 | 11 | "github.com/labstack/echo/v4" 12 | 13 | "workflow/src/model" 14 | ) 15 | 16 | func GetUserIdentifier(c echo.Context) string { 17 | u := c.Get("currentUser").(string) 18 | 19 | return u 20 | } 21 | 22 | func GetCurrentTenant(c echo.Context) (tenants model.Tenant) { 23 | u := c.Get("currentTenant") 24 | bytes := MarshalToBytes(u) 25 | _ = json.Unmarshal(bytes, &tenants) 26 | 27 | return 28 | } 29 | 30 | func GetCurrentTenantId(c echo.Context) int { 31 | return GetCurrentTenant(c).Id 32 | } 33 | 34 | func GetWorkContext(c echo.Context) (int, string) { 35 | return GetCurrentTenantId(c), GetUserIdentifier(c) 36 | } 37 | -------------------------------------------------------------------------------- /src/initialize/logger.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2020/9/30 15:25 4 | * @Desc: auto load logger after app start 5 | */ 6 | package initialize 7 | 8 | import ( 9 | "os" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "workflow/src/global" 15 | ) 16 | 17 | func setupLogger() { 18 | logger := log.New() 19 | log.SetOutput(os.Stdout) 20 | log.SetReportCaller(true) // 日志中添加调用方法 21 | 22 | // 配置格式化器 23 | if os.Getenv("APP_ENV") == "Production" { 24 | log.SetFormatter(&log.JSONFormatter{}) 25 | log.SetLevel(log.InfoLevel) 26 | } else { 27 | log.SetFormatter(&log.TextFormatter{ 28 | ForceColors: true, 29 | FullTimestamp: true, 30 | TimestampFormat: time.RFC3339, 31 | }) 32 | log.SetLevel(log.DebugLevel) 33 | } 34 | 35 | global.BankLogger = logger 36 | } 37 | -------------------------------------------------------------------------------- /src/middleware/authmiddleware.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 15:42 4 | * @Desc: 验证中间件 5 | */ 6 | package middleware 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | 13 | "workflow/src/global/response" 14 | "workflow/src/util" 15 | ) 16 | 17 | // 先使用此种方式传递当前用户的标识id 18 | func Auth(next echo.HandlerFunc) echo.HandlerFunc { 19 | return func(c echo.Context) error { 20 | currentUserId := c.Request().Header.Get("WF-CURRENT-USER") 21 | if currentUserId == "" { 22 | return response.FailWithMsg(c, http.StatusUnauthorized, "未指定当前用户") 23 | } 24 | 25 | if util.StringToUint(currentUserId) == 0 { 26 | return response.FailWithMsg(c, http.StatusUnauthorized, "指定的当前用户不合法") 27 | } 28 | 29 | c.Set("currentUser", currentUserId) 30 | 31 | return next(c) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/controller/indexcontroller.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/01/07 14:22 4 | * @Desc: home page controller 5 | */ 6 | package controller 7 | 8 | import ( 9 | "time" 10 | 11 | "workflow/src/global/response" 12 | 13 | "github.com/labstack/echo/v4" 14 | ) 15 | 16 | func Index(c echo.Context) error { 17 | return response.OkWithData(c, time.Now().Local()) 18 | } 19 | 20 | // @Tags health 21 | // @Accept json 22 | // @Produce json 23 | // @Success 200 {object} response.HttpResponse 24 | // @Router /health/alive [GET] 25 | func Liveliness(c echo.Context) error { 26 | return response.Ok(c) 27 | } 28 | 29 | // @Tags health 30 | // @Accept json 31 | // @Produce json 32 | // @Success 200 {object} response.HttpResponse 33 | // @Router /health/ready [GET] 34 | func Readiness(c echo.Context) error { 35 | return response.Ok(c) 36 | } 37 | -------------------------------------------------------------------------------- /src/util/slice.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/27 20:45 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | // 取差集 9 | func SliceDiff(a, b []string) (diff []string) { 10 | m := make(map[string]bool) 11 | 12 | for _, item := range b { 13 | m[item] = true 14 | } 15 | 16 | for _, item := range a { 17 | if _, ok := m[item]; !ok { 18 | diff = append(diff, item) 19 | } 20 | } 21 | return 22 | } 23 | 24 | func SliceAnyString(s []string, it string) bool { 25 | for _, item := range s { 26 | if item == it { 27 | return true 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | func SliceMinMax(array []int) (min int, max int) { 35 | max = array[0] 36 | min = array[0] 37 | 38 | for _, value := range array { 39 | if max < value { 40 | max = value 41 | } 42 | if min > value { 43 | min = value 44 | } 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /src/global/constant/enum.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/16 11:21 4 | * @Desc: 5 | */ 6 | package constant 7 | 8 | // event的类别常量 9 | const ( 10 | StartEvent = iota + 1 11 | EndEvent 12 | ) 13 | 14 | // process instance 的type类别 15 | const ( 16 | I_MyToDo = iota + 1 // 我的待办 17 | I_ICreated // 我创建的 18 | I_IRelated // 和我相关的 19 | I_All // 所有 20 | ) 21 | 22 | // process definition 的type类别 23 | const ( 24 | D_ICreated = iota + 1 25 | D_All 26 | ) 27 | 28 | type ChainNodeStatus int 29 | 30 | const ( 31 | Processed ChainNodeStatus = iota + 1 // 已处理 32 | CurrentNode // 当前节点 33 | Unreachable // 后续节点 34 | ) 35 | 36 | const ( 37 | VariableNumber = iota + 1 38 | VariableString 39 | VariableBool 40 | ) 41 | 42 | // 流转历史的类型 43 | const ( 44 | HistoryTypeFull = iota + 1 45 | HistoryTypeSimple 46 | ) 47 | -------------------------------------------------------------------------------- /src/model/dto/structure.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/26 23:46 4 | * @Desc: 5 | */ 6 | package dto 7 | 8 | import ( 9 | "database/sql/driver" 10 | "encoding/json" 11 | "fmt" 12 | ) 13 | 14 | type Structure struct { 15 | Edges []Edge `json:"edges"` 16 | Nodes []Node `json:"nodes"` 17 | Groups []interface{} `json:"groups"` 18 | } 19 | 20 | // 实现 sql.Scanner 接口,Scan 将 value 扫描至 StateArray 21 | func (j *Structure) Scan(value interface{}) error { 22 | bytes, ok := value.([]byte) 23 | if !ok { 24 | return fmt.Errorf("Failed to unmarshal dto.Structure value: %v ", value) 25 | } 26 | 27 | var result Structure 28 | err := json.Unmarshal(bytes, &result) 29 | *j = result 30 | 31 | return err 32 | } 33 | 34 | // 实现 driver.Valuer 接口,Value 返回 json value 35 | func (j Structure) Value() (driver.Value, error) { 36 | v, err := json.Marshal(j) 37 | return string(v), err 38 | } 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang:1.16 as builder 3 | 4 | ENV GO111MODULE=on \ 5 | GOPROXY=https://goproxy.cn,direct 6 | 7 | WORKDIR /app 8 | 9 | COPY . . 10 | 11 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" . 12 | 13 | RUN mkdir publish && cp workflow publish && \ 14 | mkdir publish/config && \ 15 | cp src/config/appsettings.yaml publish/config/ 16 | 17 | FROM alpine:3.12 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=builder /app . 22 | 23 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ 24 | apk update 25 | 26 | # set timezone to Asia/Shanghai 27 | RUN apk update && \ 28 | apk add tzdata && \ 29 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 30 | echo "Asia/Shanghai" > /etc/timezone 31 | ENV TZ Asia/Shanghai 32 | 33 | ENV APP_ENV=Production \ 34 | PORT=5000 35 | 36 | EXPOSE 5000 37 | 38 | ENTRYPOINT ["./workflow"] -------------------------------------------------------------------------------- /src/model/request/handlerequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/18 22:00 4 | * @Desc: 审批/处理流程实例的接口的请求体 5 | */ 6 | package request 7 | 8 | import "workflow/src/model" 9 | 10 | // 审批/处理流程实例的接口的请求体 11 | type HandleInstancesRequest struct { 12 | EdgeId string `json:"edgeId" form:"edgeId"` // 走的流程的id 13 | ProcessInstanceId int `json:"processInstanceId" form:"processInstanceId"` // 流程实例的id 14 | Remarks string `json:"remarks" form:"remarks"` // 备注 15 | Variables []model.InstanceVariable `json:"variables"` // 变量 16 | } 17 | 18 | // 否决流程的请求体 19 | type DenyInstanceRequest struct { 20 | ProcessInstanceId int `json:"processInstanceId" form:"processInstanceId"` // 流程实例的id 21 | NodeId string `json:"nodeId" form:"nodeId"` // 所在节点的id 22 | Remarks string `json:"remarks" form:"remarks"` // 备注 23 | } 24 | -------------------------------------------------------------------------------- /src/model/response/instanceresponse.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/17 21:34 4 | * @Desc: 5 | */ 6 | package response 7 | 8 | import ( 9 | "workflow/src/global/constant" 10 | "workflow/src/model" 11 | ) 12 | 13 | type InstanceVariableResponse struct { 14 | Name string `json:"name"` 15 | Type string `json:"type"` 16 | Value interface{} `json:"value"` 17 | } 18 | 19 | type ProcessInstanceResponse struct { 20 | model.ProcessInstance 21 | ProcessChainNodes []ProcessChainNode `json:"processChainNodes,omitempty"` // 流程链路【包括全部节点和当前节点】 22 | } 23 | 24 | type ProcessChainNode struct { 25 | Name string `json:"name"` 26 | Id string `json:"id"` 27 | Status constant.ChainNodeStatus `json:"status"` // 1: 已处理 2: 当前节点 3: 后续节点 28 | Sort int `json:"sort"` // 排序 29 | NodeType int `json:"nodeType"` // 1. 开始事件 2. 用户任务 3. 排他网关 4. 结束事件 30 | Obligatory bool `json:"obligatory"` // 是否必经节点 31 | } 32 | -------------------------------------------------------------------------------- /src/router/router.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2020/9/30 13:44 4 | * @Desc: application main router 5 | */ 6 | package router 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | 12 | "workflow/src/controller" 13 | "workflow/src/global" 14 | customMiddleware "workflow/src/middleware" 15 | ) 16 | 17 | func Setup() *echo.Echo { 18 | r := echo.New() 19 | r.Use(middleware.Logger()) 20 | r.Use(middleware.Recover()) 21 | r.Use(middleware.CORS()) 22 | 23 | // probe 24 | r.GET("/", controller.Index) 25 | r.GET("/health/ready", controller.Readiness) 26 | r.GET("/health/alive", controller.Liveliness) 27 | 28 | // swagger 29 | if global.BankConfig.App.EnableSwagger { 30 | RegisterSwagger(r) 31 | } 32 | 33 | // apis 34 | g := r.Group("/api/wf", customMiddleware.MultiTenant, customMiddleware.Auth) 35 | { 36 | RegisterProcessDefinition(g) // 流程定义 37 | RegisterProcessInstance(g) // 流程实例 38 | RegisterRoleUsers(g) // 外部系统的角色用户映射 39 | } 40 | 41 | return r 42 | } 43 | -------------------------------------------------------------------------------- /src/model/circulationhistory.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/11 15:09 4 | * @Desc: 工单流转历史 5 | */ 6 | package model 7 | 8 | // 工单流转历史 9 | type CirculationHistory struct { 10 | AuditableBase 11 | Title string `json:"title" form:"title"` // 工单标题 12 | ProcessInstanceId int `json:"processInstanceId" form:"processInstanceId"` // 工单ID 13 | SourceState string `json:"state" form:"state"` // 源节点label 14 | SourceId string `json:"sourceId" form:"sourceId"` // 源节点ID 15 | TargetId string `json:"targetId" form:"targetId"` // 目标节点ID 16 | Circulation string `json:"circulation" form:"circulation"` // 流转说明 17 | ProcessorId string `gorm:"index" json:"processorId" form:"processorId"` // 处理人外部系统ID 18 | CostDuration string `json:"costDuration" form:"costDuration"` // 本条记录的处理时长(每次有新的一条的时候更新这个字段) 19 | Remarks string `json:"remarks" form:"remarks"` // 备注 20 | } 21 | -------------------------------------------------------------------------------- /src/util/int.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 11:50 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import "strconv" 9 | 10 | func StringToUint(str string) uint { 11 | return uint(StringToInt(str)) 12 | } 13 | 14 | func StringToInt(str string) int { 15 | i, err := strconv.Atoi(str) 16 | if err != nil { 17 | return 0 18 | } 19 | 20 | return i 21 | } 22 | 23 | func InterfaceToUint(i interface{}) uint { 24 | str, succeed := i.(string) 25 | if !succeed { 26 | return 0 27 | } 28 | 29 | return StringToUint(str) 30 | } 31 | 32 | func ParseToInt64Array(arr []int) []int64 { 33 | intArr := make([]int64, len(arr)) 34 | for index, item := range arr { 35 | intArr[index] = int64(item) 36 | } 37 | 38 | return intArr 39 | } 40 | 41 | func IsInteger(f float64) bool { 42 | f2 := float64(int64(f)) 43 | 44 | return f == f2 45 | } 46 | 47 | func ParseToIntArray(arr []interface{}) []int { 48 | intArr := make([]int, len(arr)) 49 | for index, item := range arr { 50 | intArr[index] = int(item.(float64)) 51 | } 52 | 53 | return intArr 54 | } 55 | -------------------------------------------------------------------------------- /src/initialize/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2020/9/30 15:17 4 | * @Desc: auto load config setting after app start 5 | */ 6 | package initialize 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "os" 12 | 13 | "workflow/src/global" 14 | "workflow/src/util" 15 | 16 | "github.com/jinzhu/configor" 17 | ) 18 | 19 | func setupConfig() { 20 | envCode := getEnvCode() 21 | overrideConfigFileName := fmt.Sprintf("src/config/appsettings.%s.yaml", envCode) 22 | 23 | var err error 24 | if util.PathExists(overrideConfigFileName) { 25 | err = configor.Load(&global.BankConfig, "src/config/appsettings.yaml", overrideConfigFileName) 26 | } else { 27 | err = configor.Load(&global.BankConfig, "src/config/appsettings.yaml") 28 | } 29 | 30 | if err != nil { 31 | log.Fatalf("配置初始化失败, 原因:%s", err.Error()) 32 | } 33 | 34 | // 加载环境变量 35 | util.LoadEnv(&global.BankConfig) 36 | } 37 | 38 | func getEnvCode() string { 39 | envMode := os.Getenv("APP_ENV") 40 | 41 | if envMode == "Production" { 42 | return "Production" 43 | } 44 | 45 | return "Development" 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 github.com/lzw5399 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/service/engine/processengine.start.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/22 16:41 4 | * @Desc: 新建流程实例相关的方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | "fmt" 10 | 11 | "workflow/src/model" 12 | ) 13 | 14 | // 创建实例化相关信息 15 | func (engine *ProcessEngine) CreateProcessInstance() error { 16 | // 创建 17 | err := engine.tx.Create(&engine.ProcessInstance).Error 18 | if err != nil { 19 | return fmt.Errorf("创建工单失败,%v", err.Error()) 20 | } 21 | 22 | // 创建历史记录 23 | initialNode, _ := engine.GetInitialNode() 24 | nextNodes, _ := engine.GetTargetNodes(initialNode) 25 | nextNode := nextNodes[0] // 开始节点后面只会直连一个节点 26 | engine.SetCurrentNodeEdgeInfo(&initialNode, nil, &nextNode) 27 | err = engine.CreateHistory("", false) 28 | if err != nil { 29 | return fmt.Errorf("新建历史记录失败,%v", err.Error()) 30 | } 31 | 32 | // 更新process_definition表的提交数量统计 33 | err = engine.tx.Model(&model.ProcessDefinition{}). 34 | Where("id = ?", engine.ProcessInstance.ProcessDefinitionId). 35 | Update("submit_count", engine.ProcessDefinition.SubmitCount+1).Error 36 | if err != nil { 37 | return fmt.Errorf("更新流程提交数量统计失败,%v", err.Error()) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /src/util/pagingutil.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/17 22:59 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "fmt" 10 | "reflect" 11 | ) 12 | 13 | type PagingOption struct { 14 | offset int 15 | limit int 16 | originList interface{} 17 | } 18 | 19 | func NewPaging(list interface{}) *PagingOption { 20 | return &PagingOption{ 21 | limit: -1, 22 | originList: &list, 23 | } 24 | } 25 | 26 | func (option *PagingOption) Offset(offset int) *PagingOption { 27 | option.offset = offset 28 | return option 29 | } 30 | 31 | func (option *PagingOption) Limit(limit int) *PagingOption { 32 | option.limit = limit 33 | return option 34 | } 35 | 36 | func (option *PagingOption) Get(finalList interface{}) { 37 | //finalList = []interface{}{} 38 | //if option.offset > len(option.originList) { 39 | // return 40 | //} 41 | 42 | v := reflect.ValueOf(option.originList) 43 | for v.Kind() == reflect.Ptr { 44 | v = v.Elem() 45 | } 46 | 47 | t:= v.Type() 48 | fmt.Println(t) 49 | //for i, v := range option.originList { 50 | // if i > option.offset-1 { 51 | // finalList = append(finalList.([]interface{}), v) 52 | // } 53 | //} 54 | } 55 | -------------------------------------------------------------------------------- /src/global/shared/dbutil.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/17 18:43 4 | * @Desc: 5 | */ 6 | package shared 7 | 8 | import ( 9 | "fmt" 10 | 11 | "workflow/src/model/request" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func ApplyPaging(db *gorm.DB, r *request.PagingRequest) *gorm.DB { 17 | // 如果等于0说明没传Limit参数,那么等于-1(不限制) 18 | limit := r.Limit 19 | if limit == 0 { 20 | limit = -1 21 | } 22 | 23 | order := "desc" 24 | if r.Order == "asc" { 25 | order = r.Order 26 | } 27 | 28 | sort := r.Sort 29 | if sort == "" { 30 | sort = "update_time" 31 | } 32 | 33 | return db.Offset(r.Offset).Limit(r.Limit).Order(fmt.Sprintf("%s %s", sort, order)) 34 | } 35 | 36 | func ApplyRawPaging(sql string, r *request.PagingRequest) string { 37 | order := "desc" 38 | if r.Order == "asc" { 39 | order = r.Order 40 | } 41 | sort := r.Sort 42 | if sort == "" { 43 | sort = "update_time" 44 | } 45 | 46 | sql += fmt.Sprintf(" order by %s %s ", sort, order) 47 | 48 | 49 | if r.Limit > 0 { 50 | sql += fmt.Sprintf(" limit %d ", r.Limit) 51 | } 52 | 53 | if r.Offset > 0 { 54 | sql += fmt.Sprintf(" offset %d ", r.Offset) 55 | } 56 | 57 | 58 | 59 | return sql 60 | } 61 | -------------------------------------------------------------------------------- /src/controller/historycontroller.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/31 13:28 4 | * @Desc: 流转历史 5 | */ 6 | package controller 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | 11 | "workflow/src/global/response" 12 | "workflow/src/model/request" 13 | "workflow/src/service" 14 | ) 15 | 16 | // @Tags process-instances 17 | // @Summary 获取流转历史列表 18 | // @Produce json 19 | // @param id path int true "实例id" 20 | // @param request query request.HistoryListRequest true "request" 21 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 22 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 23 | // @Success 200 {object} response.HttpResponse 24 | // @Router /api/wf/process-instances/{id}/history [GET] 25 | func ListHistory(c echo.Context) error { 26 | // 从queryString获取分页参数 27 | var r request.HistoryListRequest 28 | if err := c.Bind(&r); err != nil { 29 | return response.BadRequest(c) 30 | } 31 | 32 | if r.Sort == "" { 33 | r.Sort = "id" 34 | } 35 | 36 | if r.Order == "" { 37 | r.Order = "desc" 38 | } 39 | 40 | trainNodes, err := service.ListHistory(&r, c) 41 | if err != nil { 42 | return response.Failed(c, err) 43 | } 44 | 45 | return response.OkWithData(c, trainNodes) 46 | } 47 | -------------------------------------------------------------------------------- /src/util/converter.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/16 23:30 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "encoding/json" 10 | 11 | "gorm.io/datatypes" 12 | 13 | "workflow/src/model" 14 | ) 15 | 16 | func StringToMap(jsonStr string) (map[string]string, error) { 17 | m := make(map[string]string) 18 | err := json.Unmarshal([]byte(jsonStr), &m) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return m, nil 24 | } 25 | 26 | func MapToString(m map[string]interface{}) string { 27 | bytes, _ := json.Marshal(m) 28 | 29 | return string(bytes) 30 | } 31 | 32 | func MapToBytes(m map[string]interface{}) []byte { 33 | bytes, _ := json.Marshal(m) 34 | 35 | return bytes 36 | } 37 | 38 | func MarshalToBytes(m interface{}) []byte { 39 | bytes, _ := json.Marshal(m) 40 | 41 | return bytes 42 | } 43 | 44 | func MarshalToDbJson(m interface{}) datatypes.JSON { 45 | return datatypes.JSON(MarshalToBytes(m)) 46 | } 47 | 48 | func MarshalToString(m interface{}) string { 49 | return string(MarshalToBytes(m)) 50 | } 51 | 52 | func UnmarshalToInstanceVariables(m datatypes.JSON) []model.InstanceVariable { 53 | var variables []model.InstanceVariable 54 | _ = json.Unmarshal([]byte(m), &variables) 55 | 56 | return variables 57 | } 58 | -------------------------------------------------------------------------------- /src/model/request/pagingrequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/17 18:29 4 | * @Desc: 分页请求base model 5 | */ 6 | package request 7 | 8 | type PagingRequest struct { 9 | Sort string `json:"sort,omitempty" form:"sort,omitempty" query:"sort"` // 排序键的名字,在各查询实现中默认值与可用值都不同 10 | Order string `json:"order,omitempty" form:"order,omitempty" query:"order"` // asc或者是desc 11 | Offset int `json:"offset,omitempty" form:"offset,omitempty" query:"offset"` // 跳过的条数 12 | Limit int `json:"limit,omitempty" form:"limit,omitempty" query:"limit"` // 取的条数 13 | } 14 | 15 | type InstanceListRequest struct { 16 | PagingRequest 17 | Type int `json:"type,omitempty" form:"type" query:"type"` // 类别 1=我的待办 2=我创建的 3=和我相关的 4=所有 18 | Keyword string `json:"keyword,omitempty" form:"keyword,omitempty" query:"keyword"` // 关键词 19 | } 20 | 21 | type DefinitionListRequest struct { 22 | PagingRequest 23 | Keyword string `json:"keyword,omitempty" form:"keyword,omitempty" query:"keyword"` // 关键词 24 | Type int `json:"type,omitempty" form:"type" query:"type"` // 类别 1=我创建的 2=所有 25 | } 26 | 27 | type HistoryListRequest struct { 28 | PagingRequest 29 | Keyword string `json:"keyword,omitempty" form:"keyword,omitempty" query:"keyword"` // 关键词 30 | Type int `json:"type,omitempty" form:"type" query:"type"` // 类别 1=完整日志 2=简洁日志 31 | Id int `json:"id" path:"id" swaggerignore:"true"` // 工作流实例id 32 | } 33 | -------------------------------------------------------------------------------- /src/model/definition.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/14 22:13 4 | * @Desc: 流程的定义 5 | */ 6 | package model 7 | 8 | import ( 9 | "gorm.io/datatypes" 10 | 11 | "workflow/src/model/dto" 12 | ) 13 | 14 | // 流程定义表 15 | type ProcessDefinition struct { 16 | AuditableBase 17 | Name string `gorm:"column:name; type:varchar(128)" json:"name" form:"name"` // 流程名称 18 | FormId int `json:"formId" form:"formId"` // 对应的表单的id(表单不存在于当前系统中,仅对外部系统做一个标记) 19 | Structure dto.Structure `gorm:"column:structure; type:jsonb" json:"structure" form:"structure"` // 流程的具体结构 20 | ClassifyId int `gorm:"column:classify_id; type:integer" json:"classifyId" form:"classifyId"` // 分类ID 21 | Task datatypes.JSON `gorm:"column:task; type:jsonb" jsonb:"task" form:"task"` // 任务ID, array, 可执行多个任务,可以当成通知任务,每个节点都会去执行 22 | SubmitCount int `gorm:"column:submit_count; type:integer; default:0" json:"submitCount" form:"submitCount"` // 提交统计 23 | Notice datatypes.JSON `gorm:"column:notice; type:jsonb" json:"notice" form:"notice"` // 绑定通知 24 | TenantId int `gorm:"index" json:"tenantId" form:"tenantId"` // 租户id 25 | Remarks string `gorm:"column:remarks; type:text" json:"remarks" form:"remarks"` // 流程备注 26 | } 27 | -------------------------------------------------------------------------------- /src/model/dto/statearray.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/25 22:14 4 | * @Desc: 5 | */ 6 | package dto 7 | 8 | import ( 9 | "database/sql/driver" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | ) 14 | 15 | type StateArray []State 16 | 17 | type State struct { 18 | Id string `json:"id"` 19 | Label string `json:"label"` 20 | Processor []string `json:"processor"` // 完整的处理人列表 21 | CompletedProcessor []string `json:"completedProcessor"` // 已处理的人 22 | UnCompletedProcessor []string `json:"unCompletedProcessor"` //未处理的人 23 | ProcessMethod string `json:"processMethod"` // 处理方式(角色 用户等) 24 | AssignValue []string `json:"assignValue"` // 指定的处理者(用户的id或者角色的id) 25 | AvailableEdges []Edge `json:"availableEdges"` // 可走的线路 26 | IsCounterSign bool `json:"isCounterSign"` // 是否是会签 27 | } 28 | 29 | // 实现 sql.Scanner 接口,Scan 将 value 扫描至 StateArray 30 | func (j *StateArray) Scan(value interface{}) error { 31 | bytes, ok := value.([]byte) 32 | if !ok { 33 | return errors.New(fmt.Sprint("Failed to unmarshal dto.StateArray value:", value)) 34 | } 35 | 36 | var result StateArray 37 | err := json.Unmarshal(bytes, &result) 38 | *j = result 39 | 40 | return err 41 | } 42 | 43 | // 实现 driver.Valuer 接口,Value 返回 json value 44 | func (j StateArray) Value() (driver.Value, error) { 45 | if j == nil { 46 | return nil, nil 47 | } 48 | 49 | v, err := json.Marshal(j) 50 | return string(v), err 51 | } 52 | -------------------------------------------------------------------------------- /src/service/historyservice.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/31 13:44 4 | * @Desc: 流转历史 5 | */ 6 | package service 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | 11 | "workflow/src/global" 12 | "workflow/src/global/constant" 13 | "workflow/src/global/shared" 14 | "workflow/src/model" 15 | "workflow/src/model/request" 16 | "workflow/src/model/response" 17 | "workflow/src/util" 18 | ) 19 | 20 | func ListHistory(r *request.HistoryListRequest, c echo.Context) (*response.PagingResponse, error) { 21 | var ( 22 | histories []model.CirculationHistory 23 | tenantId, _ = util.GetWorkContext(c) 24 | ) 25 | 26 | db := global.BankDb. 27 | Model(&model.ProcessInstance{}). 28 | Where("tenant_id = ?", tenantId). 29 | Where("process_instance.id = ?", r.Id). 30 | Joins("inner join wf.circulation_history on circulation_history.process_instance_id = process_instance.id") 31 | 32 | // 根据type的不同有不同的逻辑 33 | switch r.Type { 34 | case constant.HistoryTypeFull: 35 | case constant.HistoryTypeSimple: 36 | db.Where("source_id not like 'exclusiveGateway%' and source_id not like 'parallelGateway%'") 37 | default: 38 | return nil, util.BadRequest.New("type不合法") 39 | } 40 | 41 | if r.Keyword != "" { 42 | db = db.Where("title ~ ?", r.Keyword) 43 | } 44 | 45 | var count int64 46 | db.Count(&count) 47 | 48 | db = shared.ApplyPaging(db, &r.PagingRequest) 49 | err := db.Select("circulation_history.*").Scan(&histories).Error 50 | 51 | return &response.PagingResponse{ 52 | TotalCount: count, 53 | CurrentCount: int64(len(histories)), 54 | Data: &histories, 55 | }, err 56 | } 57 | -------------------------------------------------------------------------------- /src/model/userrole.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 21:51 4 | * @Desc: 外部系统的角色用户表 5 | */ 6 | package model 7 | 8 | import ( 9 | "time" 10 | ) 11 | 12 | // 外部系统的用户表 13 | type User struct { 14 | EntityBase 15 | Identifier string `gorm:"index" json:"identifier"` // 外部系统用户id 16 | Name string `json:"name"` // 用户名称 17 | TenantId int `gorm:"index" json:"tenantId"` // 租户id 18 | CreateTime time.Time `gorm:"default:now();type:timestamp" json:"createTime"` 19 | } 20 | 21 | // 外部系统的角色表 22 | type Role struct { 23 | EntityBase 24 | Identifier string `gorm:"index" json:"identifier"` // 外部系统角色id 25 | Name string `json:"name"` // 角色名字 26 | TenantId int `gorm:"index" json:"tenantId"` // 租户id 27 | CreateTime time.Time `gorm:"default:now();type:timestamp" json:"createTime"` 28 | } 29 | 30 | // 用户角色关联表%v 31 | type UserRole struct { 32 | EntityBase 33 | UserIdentifier string `gorm:"index" json:"userIdentifier"` 34 | RoleIdentifier string `gorm:"index" json:"roleIdentifier"` 35 | } 36 | 37 | //// 角色用户: 一条数据是一个角色 38 | //type RoleUsers struct { 39 | // EntityBase 40 | // RoleId int `gorm:"index" json:"roleId" form:"roleId"` // 外部系统的角色Id 41 | // UserIds pq.Int64Array `gorm:"type:integer[]; default:array[]::integer[]" json:"userIds" form:"userIds"` // 外部系统的用户Id数组 42 | // TenantId int `gorm:"index" json:"tenantId" form:"tenantId"` // 租户id 43 | // CreateTime time.Time `gorm:"default:now();type:timestamp" json:"createTime" form:"createTime"` 44 | //} 45 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # tumbleweed(风滚草)工作流引擎 2 | 3 | [English](README.md) | 简体中文 4 | 5 | ## 简介 6 | 7 | 基于bpmn 2.0的工作流引擎, 将用户、表单等拆分出去,专注于流程流转本身 8 | 9 | 配合工作流设计器使用 10 | > https://github.com/lzw5399/tumbleweed-designer 11 | 12 | ## 项目定位 13 | 14 | 由于实际业务中要在主系统中集成一款可定制化的工作流组件,要求是专注于流程的设计和流转。于是就搞了一个工作流的独立微服务,提供RESTful服务(后续考虑改成grpc-gateway形式). 将【用户/角色】【表单】等依赖分离出去,支持多租户 15 | 16 | 针对【用户/角色】的处理 17 | - 接入工作流引擎的系统指定一个租户 18 | - 然后将本身系统中需要用到工作流的用户/角色 的id和name同步到工作流引擎的数据库中 19 | - 工作流引擎本身只关心用户/角色的唯一标识进行审批等的判定 20 | 21 | 针对【表单】的处理 22 | - 工作流引擎本身不保存任何表单的结构和数据 23 | - 如果流转中有一些网关的条件需要用到表单的数据,将表单中的判断字段赋值给variable,然后条件表达式中使用该变量来判断 24 | 25 | ## 支持的bpmn元素 26 | 27 | - 事件(Event) 28 | - 开始事件(StartEvent) 29 | - 结束事件(EndEvent) 30 | - 活动(Activity) 31 | - 用户任务(UserTask) 32 | - 脚本任务(ScriptTask) 33 | - 网关(Gateway) 34 | - 排他网关(ExclusiveGateway) 35 | - 并行网关(ParallelGateway) 36 | - 包容网关(InclusiveGateway) 37 | - 顺序流(SequenceFlow) 38 | 39 | ## 技术架构 40 | 41 | golang + echo + gorm + postgres 42 | 43 | ### 外部组件依赖 44 | 45 | 除了数据库(postgres)之外没有其他的依赖 46 | - mysql需要替换gorm的驱动,以及对一些sql做小幅改造 47 | 48 | ### 数据访问 49 | 50 | gorm + postgres 51 | 52 | 使用指定数据库的wf schema。可以单独使用一个数据库,也可以集成到已有业务的数据库中 53 | 54 | 支持自动迁移(可配置开启或关闭) 55 | 56 | ## 支持的功能 57 | 58 | - 上述bpmn元素应该支持的所有基础功能 59 | - 会签 60 | - 转交审批 61 | - 审批限时 自然日/工作日 62 | - 超时后果 (自动通过/拒绝 或者无操作) 63 | - WebHook 64 | - 多租户 65 | - 展示用的流程链路 66 | 67 | ## 质量保证 68 | 69 | - 集成测试 70 | 71 | ## 致谢 72 | 73 | 特别感谢[JetBrains](https://www.jetbrains.com?from=tumbleweed)提供的开源许可证授权 74 | 75 | 76 | Jetbrains 77 | -------------------------------------------------------------------------------- /src/model/request/roleusersrequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 22:03 4 | * @Desc: 5 | */ 6 | package request 7 | 8 | import ( 9 | "github.com/ahmetb/go-linq/v3" 10 | 11 | "workflow/src/model" 12 | ) 13 | 14 | // 同步 15 | type BatchSyncUserRoleRequest struct { 16 | Users []UserRequest `json:"users"` 17 | Roles []RoleRequest `json:"roles"` 18 | UserRoles []UserRoleRequest `json:"userRoles"` 19 | } 20 | 21 | type UserRequest struct { 22 | Identifier string `json:"identifier"` 23 | Name string `json:"name"` 24 | } 25 | 26 | type RoleRequest struct { 27 | Identifier string `json:"identifier"` 28 | Name string `json:"name"` 29 | } 30 | 31 | type UserRoleRequest struct { 32 | UserIdentifier string `json:"userIdentifier"` 33 | RoleIdentifier string `json:"roleIdentifier"` 34 | } 35 | 36 | func (s *BatchSyncUserRoleRequest) ToDbEntities(tenantId int) (users []model.User, roles []model.Role, userRole []model.UserRole) { 37 | if s.Users != nil { 38 | linq.From(s.Users).SelectT(func(i UserRequest) interface{} { 39 | return model.User{ 40 | Identifier: i.Identifier, 41 | Name: i.Name, 42 | TenantId: tenantId, 43 | } 44 | }).ToSlice(&users) 45 | } 46 | 47 | if s.Roles != nil { 48 | linq.From(s.Roles).SelectT(func(i RoleRequest) interface{} { 49 | return model.Role{ 50 | Identifier: i.Identifier, 51 | Name: i.Name, 52 | TenantId: tenantId, 53 | } 54 | }).ToSlice(&roles) 55 | } 56 | 57 | if s.UserRoles != nil { 58 | linq.From(s.UserRoles).SelectT(func(i UserRoleRequest) interface{} { 59 | return model.UserRole{ 60 | UserIdentifier: i.UserIdentifier, 61 | RoleIdentifier: i.RoleIdentifier, 62 | } 63 | }).ToSlice(&userRole) 64 | } 65 | 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /src/router/router.apis.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 15:52 4 | * @Desc: 流程定义路由 5 | */ 6 | package router 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | 11 | "workflow/src/controller" 12 | ) 13 | 14 | // 流程定义 15 | func RegisterProcessDefinition(r *echo.Group) { 16 | processGroup := r.Group("/process-definitions") 17 | { 18 | processGroup.POST("", controller.CreateProcessDefinition) // 新建 19 | processGroup.PUT("", controller.UpdateProcessDefinition) // 修改 20 | processGroup.DELETE("/:id", controller.DeleteProcessDefinition) // 删除 21 | processGroup.GET("/:id", controller.GetProcessDefinition) // 获取流程 22 | processGroup.GET("", controller.ListProcessDefinition) // 获取列表 23 | processGroup.POST("/_clone", controller.CloneProcessDefinition) // 克隆 24 | } 25 | } 26 | 27 | // 流程实例 28 | func RegisterProcessInstance(r *echo.Group) { 29 | instanceGroup := r.Group("/process-instances") 30 | { 31 | instanceGroup.POST("", controller.CreateProcessInstance) // 新建流程 32 | instanceGroup.GET("/:id", controller.GetProcessInstance) // 获取 33 | instanceGroup.GET("", controller.ListProcessInstances) // 获取列表 34 | instanceGroup.POST("/_handle", controller.HandleProcessInstance) // 流程审批 35 | instanceGroup.POST("/_deny", controller.DenyProcessInstance) // 流程否决 36 | instanceGroup.GET("/:id/history", controller.ListHistory) // 获取流程链路 37 | instanceGroup.GET("/:id/train-nodes", controller.GetProcessTrain) // 获取流程链路 38 | } 39 | } 40 | 41 | // 外部系统 角色和用户对应关系 42 | func RegisterRoleUsers(r *echo.Group) { 43 | instanceGroup := r.Group("/role-users") 44 | { 45 | instanceGroup.POST("/_batch", controller.BatchSyncRoleUsers) // 批量更新 46 | //instanceGroup.POST("", controller.SyncRoleUsers) // 单条更新 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/model/request/instancerequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/16 21:59 4 | * @Desc: 流程实例 process instance 5 | */ 6 | package request 7 | 8 | import ( 9 | "time" 10 | 11 | "workflow/src/model" 12 | "workflow/src/util" 13 | ) 14 | 15 | type ProcessInstanceRequest struct { 16 | Title string `json:"title" form:"title"` // 流程实例标题 17 | ProcessDefinitionId int `json:"processDefinitionId" form:"processDefinitionId"` // 流程ID 18 | Variables []model.InstanceVariable `json:"variables"` // 变量 19 | } 20 | 21 | func (i *ProcessInstanceRequest) ToProcessInstance(currentUserId string, tenantId int) model.ProcessInstance { 22 | if i.Variables == nil { 23 | i.Variables = []model.InstanceVariable{} 24 | } 25 | return model.ProcessInstance{ 26 | AuditableBase: model.AuditableBase{ 27 | CreateTime: time.Now().Local(), 28 | UpdateTime: time.Now().Local(), 29 | CreateBy: currentUserId, 30 | UpdateBy: currentUserId, 31 | }, 32 | Title: i.Title, 33 | ProcessDefinitionId: i.ProcessDefinitionId, 34 | TenantId: tenantId, 35 | Variables: util.MarshalToDbJson(i.Variables), 36 | } 37 | } 38 | 39 | type GetInstanceRequest struct { 40 | Id int `json:"id" form:"id" path:"id"` 41 | IncludeProcessTrain bool `json:"includeProcessTrain" body:"includeProcessTrain"` 42 | } 43 | 44 | type GetVariableRequest struct { 45 | InstanceId int `json:"instanceId,omitempty" form:"instanceId,omitempty"` 46 | VariableName string `json:"variableName,omitempty" form:"variableName,omitempty"` 47 | } 48 | 49 | type GetVariableListRequest struct { 50 | PagingRequest 51 | InstanceId int `json:"instanceId,omitempty" form:"instanceId,omitempty"` 52 | } 53 | -------------------------------------------------------------------------------- /src/model/instance.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/9 14:18 4 | * @Desc: 5 | */ 6 | package model 7 | 8 | import ( 9 | "github.com/lib/pq" 10 | "gorm.io/datatypes" 11 | 12 | "workflow/src/model/dto" 13 | ) 14 | 15 | type ProcessInstance struct { 16 | AuditableBase 17 | Title string `gorm:"type:text" json:"title" form:"title"` // 工单标题 18 | Priority int `gorm:"type:smallint" json:"priority" form:"priority"` // 工单优先级 1,正常 2,紧急 3,非常紧急 19 | ProcessDefinitionId int `gorm:"type:integer" json:"processDefinitionId" form:"processDefinitionId"` // 流程ID 20 | ClassifyId int `gorm:"type:integer" json:"classifyId" form:"classifyId"` // 分类ID 21 | IsEnd bool `gorm:"default:false" json:"isEnd" form:"isEnd"` // 是否结束 22 | IsDenied bool `gorm:"default:false" json:"isDenied" form:"isDenied"` // 是否被拒绝 23 | State dto.StateArray `gorm:"type:jsonb" json:"state" form:"state"` // 状态信息 24 | RelatedPerson pq.StringArray `gorm:"type:integer[]; default:array[]::integer[]" json:"relatedPerson" form:"relatedPerson"` // 工单所有处理人 25 | TenantId int `gorm:"index" json:"tenantId" form:"tenantId"` // 租户id 26 | Variables datatypes.JSON `gorm:"type:jsonb" json:"variables" form:"variables"` // 变量 27 | } 28 | 29 | type InstanceVariable struct { 30 | Name string `json:"name"` // 变量名 31 | Value interface{} `json:"value"` // 变量值 32 | } 33 | -------------------------------------------------------------------------------- /src/model/request/definitionrequest.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/5 11:05 4 | * @Desc: 5 | */ 6 | package request 7 | 8 | import ( 9 | "encoding/json" 10 | "time" 11 | 12 | "gorm.io/datatypes" 13 | 14 | "workflow/src/model" 15 | "workflow/src/model/dto" 16 | ) 17 | 18 | type ProcessDefinitionRequest struct { 19 | Id int `json:"id" form:"id"` 20 | Name string `json:"name" form:"name"` // 流程名称 21 | FormId int `json:"formId" form:"formId"` // 对应的表单的id(仅对外部系统做一个标记) 22 | Structure json.RawMessage `json:"structure" form:"structure" swaggertype:"string"` // 流程结构 23 | ClassifyId int `json:"classifyId" form:"classifyId"` // 分类ID 24 | Task json.RawMessage `json:"task" form:"task" swaggertype:"string"` // 任务ID, array, 可执行多个任务,可以当成通知任务,每个节点都会去执行 25 | Notice json.RawMessage `json:"notice" form:"notice" swaggertype:"string"` // 绑定通知 26 | Remarks string `json:"remarks" form:"remarks"` // 流程备注 27 | } 28 | 29 | func (p *ProcessDefinitionRequest) ProcessDefinition() model.ProcessDefinition { 30 | var structure dto.Structure 31 | _ = json.Unmarshal(p.Structure, &structure) 32 | 33 | return model.ProcessDefinition{ 34 | AuditableBase: model.AuditableBase{ 35 | EntityBase: model.EntityBase{ 36 | Id: p.Id, 37 | }, 38 | CreateTime: time.Now().Local(), 39 | UpdateTime: time.Now().Local(), 40 | }, 41 | Name: p.Name, 42 | Structure: structure, 43 | ClassifyId: p.ClassifyId, 44 | Task: datatypes.JSON(p.Task), 45 | Notice: datatypes.JSON(p.Notice), 46 | Remarks: p.Remarks, 47 | FormId: p.FormId, 48 | SubmitCount: 0, 49 | } 50 | } 51 | 52 | type CloneDefinitionRequest struct { 53 | Id int `json:"id" form:"id"` 54 | } 55 | -------------------------------------------------------------------------------- /src/initialize/db.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/12 15:12 4 | * @Desc: 初始化数据库连接 5 | */ 6 | package initialize 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | 12 | _ "github.com/jackc/pgx/v4" 13 | "gorm.io/driver/postgres" 14 | "gorm.io/gorm" 15 | "gorm.io/gorm/logger" 16 | "gorm.io/gorm/schema" 17 | 18 | "workflow/src/global" 19 | "workflow/src/model" 20 | ) 21 | 22 | func setupDbConn() { 23 | log.Println("-------开始初始化postgres数据库连接--------") 24 | 25 | // 获取数据库配置 26 | dbCfg := global.BankConfig.Db 27 | 28 | // 初始化数据库连接 29 | connStr := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=disable TimeZone=Asia/Shanghai", 30 | dbCfg.Host, dbCfg.Port, dbCfg.Username, dbCfg.Database, dbCfg.Password) 31 | 32 | db, err := gorm.Open(postgres.New(postgres.Config{ 33 | DSN: connStr, 34 | PreferSimpleProtocol: true, 35 | }), &gorm.Config{ 36 | Logger: logger.Default.LogMode(logger.Info), 37 | DisableForeignKeyConstraintWhenMigrating: true, // 忽略迁移添加外键 38 | NamingStrategy: schema.NamingStrategy{ 39 | TablePrefix: "wf.", // 指定schema 40 | SingularTable: true, // 表名不加s 41 | }, 42 | }) 43 | 44 | if err != nil { 45 | log.Fatalf("数据库连接初始化失败, 原因: %s", err.Error()) 46 | } 47 | 48 | global.BankDb = db 49 | log.Println("-------初始化postgres数据库连接成功--------") 50 | 51 | if dbCfg.AutoMigrate { 52 | log.Println("-------启动了数据库自动迁移, 开始迁移--------") 53 | doMigration() 54 | log.Println("-------表结构迁移完成--------") 55 | } 56 | } 57 | 58 | // 自动迁移 59 | func doMigration() { 60 | err := global.BankDb.Exec("create schema if not exists wf;").Error 61 | if err != nil { 62 | log.Fatalf("迁移创建schema错误,错误信息为:%s", err.Error()) 63 | } 64 | 65 | err = global.BankDb.AutoMigrate( 66 | &model.ProcessDefinition{}, &model.ProcessInstance{}, 67 | &model.Classify{}, &model.CirculationHistory{}, 68 | &model.Tenant{}, &model.User{}, 69 | &model.Role{}, &model.UserRole{}) 70 | if err != nil { 71 | log.Fatalf("迁移表结构发生错误,错误信息为:%s", err.Error()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/middleware/tenantmiddleware.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 14:57 4 | * @Desc: 多租户中间件 5 | */ 6 | package middleware 7 | 8 | import ( 9 | "encoding/json" 10 | "net/http" 11 | "time" 12 | 13 | . "github.com/ahmetb/go-linq/v3" 14 | "github.com/labstack/echo/v4" 15 | 16 | "workflow/src/global" 17 | "workflow/src/global/response" 18 | "workflow/src/model" 19 | "workflow/src/util" 20 | ) 21 | 22 | func MultiTenant(next echo.HandlerFunc) echo.HandlerFunc { 23 | return func(c echo.Context) error { 24 | tenantCode := c.Request().Header.Get("WF-TENANT-CODE") 25 | if tenantCode == "" { 26 | return response.FailWithMsg(c, http.StatusUnauthorized, "未指定当前租户") 27 | } 28 | 29 | // 从内存缓存中获取全部的tenant 30 | tenants := GetTenants() 31 | 32 | tenant := From(tenants).WhereT(func(i model.Tenant) bool { 33 | return i.Name == tenantCode 34 | }).First() 35 | 36 | // 不存在新增并更新全部租户缓存 37 | if tenant == nil { 38 | t := model.Tenant{ 39 | Name: tenantCode, 40 | CreateTime: time.Now().Local(), 41 | } 42 | err := global.BankDb.Model(&model.Tenant{}).Create(&t).Error 43 | if err != nil { 44 | return response.FailWithMsg(c, http.StatusUnauthorized, "指定租户失败") 45 | } 46 | tenant = t 47 | 48 | // 更新缓存 49 | go UpdateTenantCache() 50 | } 51 | 52 | // 当前租户放进缓存 53 | c.Set("currentTenant", tenant) 54 | 55 | return next(c) 56 | } 57 | } 58 | 59 | // 从缓存中获取所有的租户信息 60 | func GetTenants() []model.Tenant { 61 | var tenants []model.Tenant 62 | 63 | t, succeed := global.BankCache.Get("tenants") 64 | if !succeed { 65 | tenants = *(UpdateTenantCache()) 66 | } 67 | bytes := util.MarshalToBytes(t) 68 | _ = json.Unmarshal(bytes, &tenants) 69 | 70 | return tenants 71 | } 72 | 73 | // 更新新的租户信息到缓存中 74 | func UpdateTenantCache() *[]model.Tenant { 75 | var tenants []model.Tenant 76 | global.BankDb. 77 | Model(&model.Tenant{}). 78 | Find(&tenants) 79 | 80 | global.BankCache.SetDefault("tenants", tenants) 81 | global.BankLogger.Infoln("租户缓存更新成功") 82 | 83 | return &tenants 84 | } 85 | -------------------------------------------------------------------------------- /src/controller/roleuserscontroller.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 21:59 4 | * @Desc: 用户角色控制器(外部系统的) 5 | */ 6 | package controller 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | 11 | "workflow/src/global/response" 12 | "workflow/src/model/request" 13 | "workflow/src/service" 14 | "workflow/src/util" 15 | ) 16 | 17 | // @Tags role-users 18 | // @Summary 批量同步(创建或更新)角色用户映射关系 19 | // @Accept json 20 | // @Produce json 21 | // @param request body request.BatchSyncUserRoleRequest true "request" 22 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 23 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 24 | // @Success 200 {object} response.HttpResponse 25 | // @Router /api/wf/role-users/_batch [POST] 26 | func BatchSyncRoleUsers(c echo.Context) error { 27 | var ( 28 | r request.BatchSyncUserRoleRequest 29 | err error 30 | ) 31 | 32 | if err = c.Bind(&r); err != nil { 33 | return response.BadRequest(c) 34 | } 35 | 36 | tenantId := util.GetCurrentTenantId(c) 37 | err = service.BatchSyncRoleUsers(&r, tenantId) 38 | if err != nil { 39 | return response.Failed(c, err) 40 | } 41 | 42 | return response.OkWithMessage(c, "更新成功") 43 | } 44 | 45 | // @Tags role-users 46 | // @Summary 同步(创建或更新)角色用户映射关系 47 | // @Accept json 48 | // @Produce json 49 | // @param request body request.SyncRoleUsersRequest true "request" 50 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 51 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 52 | // @Success 200 {object} response.HttpResponse 53 | // @Router /api/wf/role-users [POST] 54 | //func SyncRoleUsers(c echo.Context) error { 55 | // var ( 56 | // r request.SyncRoleUsersRequest 57 | // err error 58 | // ) 59 | // 60 | // if err = c.Bind(&r); err != nil { 61 | // return response.BadRequest(c) 62 | // } 63 | // 64 | // tenantId := util.GetCurrentTenantId(c) 65 | // err = service.SyncRoleUsers(&r, tenantId) 66 | // if err != nil { 67 | // return response.Failed(c, err) 68 | // } 69 | // 70 | // return response.OkWithMessage(c, "更新成功") 71 | //} 72 | -------------------------------------------------------------------------------- /src/service/engine/processengine.history.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/22 11:27 4 | * @Desc: 流转历史CirculationHistory的相关方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | "time" 10 | 11 | "workflow/src/global/constant" 12 | "workflow/src/model" 13 | "workflow/src/util" 14 | ) 15 | 16 | // 创建流转历史记录 17 | func (engine *ProcessEngine) CreateHistory(remark string, isDenied bool) error { 18 | // 源节点不为【开始事件】的,获取上一条的流转历史的CreateTime来计算CostDuration 19 | duration := "0小时 0分钟" 20 | if engine.sourceNode.Clazz != constant.START { 21 | var lastCirculation model.CirculationHistory 22 | err := engine.tx. 23 | Where("process_instance_id = ?", engine.ProcessInstance.Id). 24 | Order("create_time desc"). 25 | Select("create_time"). 26 | First(&lastCirculation). 27 | Error 28 | if err != nil { 29 | return err 30 | } 31 | duration = util.FmtDuration(time.Since(lastCirculation.CreateTime)) 32 | } 33 | 34 | // 根据不同的类型取不同的值 35 | var ( 36 | sourceState = engine.sourceNode.Label 37 | sourceId = engine.sourceNode.Id 38 | targetId, circulation string 39 | ) 40 | switch { 41 | case engine.sourceNode.Clazz == constant.START && !isDenied: 42 | targetId = engine.targetNode.Id 43 | circulation = "开始" 44 | 45 | case engine.sourceNode.Clazz == constant.End && !isDenied: 46 | circulation = "结束" 47 | 48 | case isDenied: 49 | circulation = "否决" 50 | 51 | default: 52 | targetId = engine.targetNode.Id 53 | circulation = engine.linkEdge.Label 54 | } 55 | 56 | // 创建新的一条流转历史 57 | cirHistory := model.CirculationHistory{ 58 | AuditableBase: model.AuditableBase{ 59 | CreateBy: engine.userIdentifier, 60 | UpdateBy: engine.userIdentifier, 61 | }, 62 | Title: engine.ProcessInstance.Title, 63 | ProcessInstanceId: engine.ProcessInstance.Id, 64 | SourceState: sourceState, 65 | SourceId: sourceId, 66 | TargetId: targetId, 67 | Circulation: circulation, 68 | ProcessorId: engine.userIdentifier, 69 | CostDuration: duration, 70 | Remarks: remark, 71 | } 72 | 73 | err := engine.tx. 74 | Model(&model.CirculationHistory{}). 75 | Create(&cirHistory). 76 | Error 77 | 78 | return err 79 | } 80 | -------------------------------------------------------------------------------- /src/util/error.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/27 13:33 4 | * @Desc: 5 | */ 6 | package util 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ErrorType is the type of an error 15 | type ErrorType uint 16 | 17 | const ( 18 | NoType ErrorType = iota 19 | BadRequest // 400 20 | NotFound // 404 21 | Forbidden // 403 22 | InternalServerError // 500 23 | ) 24 | 25 | type customError struct { 26 | errorType ErrorType 27 | originalError error 28 | context errorContext 29 | } 30 | 31 | type errorContext struct { 32 | Field string 33 | Message string 34 | } 35 | 36 | // NewError creates a new customError 37 | func (errorType ErrorType) New(err interface{}) error { 38 | var finalError error 39 | switch err.(type) { 40 | case error: 41 | finalError = err.(error) 42 | case string: 43 | finalError = errors.New(err.(string)) 44 | } 45 | 46 | return customError{errorType: errorType, originalError: finalError} 47 | } 48 | 49 | // NewError creates a new customError with formatted message 50 | func (errorType ErrorType) Newf(msg string, args ...interface{}) error { 51 | return customError{errorType: errorType, originalError: fmt.Errorf(msg, args...)} 52 | } 53 | 54 | // Error returns the message of a customError 55 | func (error customError) Error() string { 56 | return error.originalError.Error() 57 | } 58 | 59 | // NewError creates a no type error 60 | func NewError(err interface{}) error { 61 | var finalError error 62 | switch err.(type) { 63 | case error: 64 | finalError = err.(error) 65 | case string: 66 | finalError = errors.New(err.(string)) 67 | } 68 | 69 | return customError{errorType: NoType, originalError: finalError} 70 | } 71 | 72 | // Newf creates a no type error with formatted message 73 | func NewErrorf(msg string, args ...interface{}) error { 74 | return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))} 75 | } 76 | 77 | // GetType returns the error type 78 | func GetType(err error) ErrorType { 79 | if customErr, ok := err.(customError); ok { 80 | return customErr.errorType 81 | } 82 | 83 | return NoType 84 | } 85 | -------------------------------------------------------------------------------- /src/util/envloader.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/22 15:18 4 | * @Desc: 获取环境变量 5 | */ 6 | package util 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "os" 12 | "reflect" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | func LoadEnv(val interface{}) { 18 | v := reflect.ValueOf(val) 19 | 20 | // 获取到指针指向的值 21 | v = reflect.Indirect(v) 22 | 23 | if v.Kind() != reflect.Struct { 24 | fmt.Println("请使用结构体指针") 25 | return 26 | } 27 | 28 | loadEnvToStruct(v, []string{}, 1) 29 | } 30 | 31 | func loadEnvToStruct(v reflect.Value, dependencies []string, deepLevel int) { 32 | num := v.NumField() 33 | for i := 0; i < num; i++ { 34 | if deepLevel == 1 { 35 | dependencies = []string{} 36 | } 37 | f := v.Field(i) 38 | fieldName := v.Type().Field(i).Name 39 | 40 | envKey := genDependenciesString(dependencies, fieldName) 41 | envValue := os.Getenv(envKey) 42 | 43 | if f.Kind() != reflect.Struct && (envValue == "" || !f.CanSet()) { 44 | log.Printf("【环境变量配置加载】当前envKey为: %s 的环境变量为空, 将跳过", envKey) 45 | continue 46 | } 47 | 48 | switch f.Kind() { 49 | case reflect.String: 50 | f.SetString(envValue) 51 | case reflect.Bool: 52 | if value, err := strconv.ParseBool(envValue); err == nil { 53 | log.Printf("【环境变量配置加载】当前envKey为: %s 的环境变量已成功替换", envKey) 54 | f.SetBool(value) 55 | } 56 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 57 | if value, err := strconv.Atoi(envValue); err == nil { 58 | log.Printf("【环境变量配置加载】当前envKey为: %s 的环境变量已替换", envKey) 59 | f.SetInt(int64(value)) 60 | } 61 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 62 | if value, err := strconv.Atoi(envValue); err == nil { 63 | log.Printf("【环境变量配置加载】当前envKey为: %s 的环境变量已替换", envKey) 64 | f.SetUint(uint64(value)) 65 | } 66 | case reflect.Struct: 67 | dependencies = append(dependencies, fieldName) 68 | loadEnvToStruct(f, dependencies, deepLevel+1) 69 | } 70 | } 71 | } 72 | 73 | func genDependenciesString(dependencies []string, fieldName string) string { 74 | prefix := strings.Join(dependencies, "__") 75 | if prefix == "" { 76 | return fieldName 77 | } 78 | 79 | return fmt.Sprintf("%s__%s", prefix, fieldName) 80 | } 81 | -------------------------------------------------------------------------------- /src/service/engine/processengine.validator.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/27 22:45 4 | * @Desc: 验证的相关方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | . "github.com/ahmetb/go-linq/v3" 10 | 11 | "workflow/src/model/dto" 12 | "workflow/src/model/request" 13 | "workflow/src/util" 14 | ) 15 | 16 | // 验证入参合法性 17 | func (engine *ProcessEngine) ValidateHandleRequest(r *request.HandleInstancesRequest) error { 18 | currentEdge, err := engine.GetEdge(r.EdgeId) 19 | if err != nil { 20 | return util.BadRequest.New(err) 21 | } 22 | 23 | state, err := engine.GetStateByEdgeId(currentEdge) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if currentEdge.Source != state.Id { 29 | return util.BadRequest.New("当前审批不合法, 请检查") 30 | } 31 | 32 | // 判断当前流程实例状态是否已结束或者被否决 33 | if engine.ProcessInstance.IsEnd { 34 | return util.BadRequest.New("当前流程已结束, 不能进行审批操作") 35 | } 36 | 37 | if engine.ProcessInstance.IsDenied { 38 | return util.BadRequest.New("当前流程已被否决, 不能进行审批操作") 39 | } 40 | 41 | // 判断当前用户是否有权限 42 | return engine.EnsurePermission(state) 43 | } 44 | 45 | // 验证否决请求的入参 46 | func (engine *ProcessEngine) ValidateDenyRequest(r *request.DenyInstanceRequest) error { 47 | state, err := engine.GetStateByNodeId(r.NodeId) 48 | if err != nil { 49 | return util.BadRequest.New(err) 50 | } 51 | 52 | // 判断当前流程实例状态是否已结束或者被否决 53 | if engine.ProcessInstance.IsEnd { 54 | return util.BadRequest.New("当前流程已结束, 不能进行审批操作") 55 | } 56 | 57 | if engine.ProcessInstance.IsDenied { 58 | return util.BadRequest.New("当前流程已被否决, 不能进行审批操作") 59 | } 60 | 61 | // 判断是否有权限 62 | return engine.EnsurePermission(state) 63 | } 64 | 65 | // 判断当前用户是否有权限 66 | func (engine *ProcessEngine) EnsurePermission(state dto.State) error { 67 | // 判断当前角色是否有权限 68 | hasPermission := false 69 | for _, processor := range state.Processor { 70 | if processor == engine.userIdentifier { 71 | hasPermission = true 72 | break 73 | } 74 | } 75 | 76 | if !hasPermission { 77 | return util.Forbidden.New("当前用户无权限进行当前操作") 78 | } 79 | 80 | alreadyCompleted := From(state.CompletedProcessor).AnyWith(func(it interface{}) bool { 81 | return it.(string) == engine.userIdentifier 82 | }) 83 | if alreadyCompleted { 84 | return util.Forbidden.New("当前用户针对目前节点已审核, 无法重复审核") 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tumbleweed workflow engine 2 | 3 | English | [简体中文](README-ZH.md) 4 | 5 | ## Introduction 6 | 7 | Workflow engine based on bpmn 2.0, split users, forms, etc., and focus on the flow of the process itself 8 | 9 | Use with workflow designer 10 | > https://github.com/lzw5399/tumbleweed-designer 11 | 12 | ## Project positioning 13 | 14 | `Tumbleweed` is a workflow independent microservice to provide RESTful services (subsequent to consider changing to grpc-gateway form). Separate dependencies such as [user/role] and [form] to support multi-tenancy 15 | 16 | Processing for [user/role] 17 | - Designate a tenant for the system connected to the workflow engine 18 | - Then synchronize the id and name of the user/role that needs to use the workflow in the system to the database of the workflow engine 19 | - The workflow engine itself only cares about the unique identification of the user/role for approval, etc. 20 | 21 | Processing for [Form] 22 | - The workflow engine itself does not save any form structure and data 23 | - If there are some gateway conditions in the circulation that need to use form data, assign the judgment field in the form to variable, and then use the variable in the condition expression to judge 24 | 25 | ## Supported bpmn elements 26 | 27 | - Event 28 | - StartEvent 29 | - EndEvent 30 | - Activity 31 | - UserTask 32 | - ScriptTask 33 | - Gateway 34 | - ExclusiveGateway 35 | - ParallelGateway 36 | - InclusiveGateway 37 | - SequenceFlow 38 | 39 | ## Technology Architecture 40 | 41 | golang + echo + gorm + postgres 42 | 43 | ### External component dependencies 44 | 45 | No other dependencies except the database (postgres) 46 | -mysql needs to replace the driver of gorm and make a small modification to some sql 47 | 48 | ### data access 49 | 50 | gorm + postgres 51 | 52 | Use the wf schema of the specified database. A database can be used alone or integrated into an existing business database 53 | 54 | Support automatic migration (configurable on or off) 55 | 56 | ## Supported features 57 | 58 | - All the basic functions that the above bpmn element should support 59 | - Countersign 60 | - Referral for approval 61 | - Approval time limit Natural day/working day 62 | - Timeout consequences (automatically pass/reject or no action) 63 | - WebHook 64 | - Multi-tenancy 65 | - Process link for display 66 | 67 | ## Quality assurance 68 | 69 | - Integration Testing 70 | 71 | ## Project supported by JetBrains 72 | 73 | Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. 74 | 75 | 76 | Jetbrains 77 | -------------------------------------------------------------------------------- /src/global/response/httpresponse.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2020/9/30 13:26 4 | * @Desc: format response 5 | */ 6 | package response 7 | 8 | import ( 9 | "log" 10 | "net/http" 11 | 12 | "github.com/labstack/echo/v4" 13 | 14 | "workflow/src/util" 15 | ) 16 | 17 | type HttpResponse struct { 18 | Success bool `json:"success"` 19 | Message interface{} `json:"message"` 20 | Data interface{} `json:"data"` 21 | } 22 | 23 | func Ok(c echo.Context) error { 24 | return result(c, nil, "success") 25 | } 26 | 27 | func OkWithMessage(c echo.Context, message string) error { 28 | return result(c, nil, message) 29 | } 30 | 31 | func OkWithData(c echo.Context, data interface{}) error { 32 | return result(c, data, "操作成功") 33 | } 34 | 35 | func OkWithDetailed(c echo.Context, data interface{}, message string) error { 36 | return result(c, data, message) 37 | } 38 | 39 | func BadRequest(c echo.Context) error { 40 | return FailWithMsg(c, http.StatusBadRequest, "错误的请求") 41 | } 42 | 43 | func BadRequestWithMessage(c echo.Context, message interface{}) error { 44 | return FailWithMsg(c, http.StatusBadRequest, message) 45 | } 46 | 47 | // 判断错误的类型 来返回对应的status code 48 | func Failed(c echo.Context, err error) error { 49 | var status int 50 | errorType := util.GetType(err) 51 | switch errorType { 52 | case util.BadRequest: 53 | status = http.StatusBadRequest 54 | case util.NotFound: 55 | status = http.StatusNotFound 56 | case util.Forbidden: 57 | status = http.StatusForbidden 58 | case util.NoType, util.InternalServerError: 59 | status = http.StatusInternalServerError 60 | } 61 | 62 | return FailWithMsg(c, status, err.Error()) 63 | } 64 | 65 | func FailWithMsg(c echo.Context, status int, err interface{}) error { 66 | var msg interface{} 67 | 68 | switch err.(type) { 69 | case error: 70 | msg = err.(error).Error() 71 | 72 | case string: 73 | msg = err.(string) 74 | 75 | default: 76 | msg = "" 77 | } 78 | 79 | switch status { 80 | // 400 81 | case http.StatusBadRequest: 82 | 83 | // 401 84 | case http.StatusUnauthorized: 85 | 86 | // 404 87 | case http.StatusNotFound: 88 | 89 | // 500 90 | case http.StatusInternalServerError: 91 | //msg = "server internal error, please contact the maintainer" 92 | log.Printf("err: %s", err) 93 | } 94 | 95 | return resultWithStatus(c, status, false, nil, msg) 96 | } 97 | 98 | func result(c echo.Context, data interface{}, msg string) error { 99 | return resultWithStatus(c, http.StatusOK, true, data, msg) 100 | } 101 | 102 | func resultWithStatus(c echo.Context, statusCode int, success bool, data interface{}, msg interface{}) error { 103 | return c.JSON(statusCode, HttpResponse{ 104 | success, 105 | msg, 106 | data, 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /src/service/engine/processengine.circulation.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/19 17:10 4 | * @Desc: 工单的流转相关方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | "time" 10 | 11 | "workflow/src/global/constant" 12 | "workflow/src/model/dto" 13 | "workflow/src/model/request" 14 | "workflow/src/util" 15 | ) 16 | 17 | // processInstance流转处理 18 | func (engine *ProcessEngine) Circulation(newStates dto.StateArray) error { 19 | toUpdate := map[string]interface{}{ 20 | "state": newStates, 21 | "related_person": engine.ProcessInstance.RelatedPerson, 22 | "is_end": false, 23 | "update_time": time.Now().Local(), 24 | "update_by": engine.userIdentifier, 25 | "variables": engine.ProcessInstance.Variables, 26 | } 27 | 28 | // 如果是跳转到结束节点,则需要修改节点状态 29 | if engine.targetNode.Clazz == constant.End { 30 | toUpdate["is_end"] = true 31 | } 32 | 33 | err := engine.tx. 34 | Model(&engine.ProcessInstance). 35 | Updates(toUpdate). 36 | Error 37 | 38 | return err 39 | } 40 | 41 | // 否决 42 | func (engine *ProcessEngine) Deny(r *request.DenyInstanceRequest) error { 43 | // 获取最新的相关者RelatedPerson 44 | engine.UpdateRelatedPerson() 45 | 46 | // 更新instance字段 47 | toUpdate := map[string]interface{}{ 48 | "related_person": engine.ProcessInstance.RelatedPerson, 49 | "is_denied": true, 50 | "update_time": time.Now().Local(), 51 | "update_by": engine.userIdentifier, 52 | "state": dto.StateArray{}, 53 | } 54 | 55 | err := engine.tx. 56 | Model(&engine.ProcessInstance). 57 | Updates(toUpdate). 58 | Error 59 | 60 | // 获取当前的node 61 | node, err := engine.GetNode(r.NodeId) 62 | if err != nil { 63 | return err 64 | } 65 | engine.SetCurrentNodeEdgeInfo(&node, nil, nil) 66 | 67 | // 创建历史记录 68 | err = engine.CreateHistory(r.Remarks, true) 69 | 70 | return err 71 | } 72 | 73 | // 更新relatedPerson 74 | func (engine *ProcessEngine) UpdateRelatedPerson() { 75 | // 获取最新的相关者RelatedPerson 76 | exist := false 77 | for _, person := range engine.ProcessInstance.RelatedPerson { 78 | if person == engine.userIdentifier { 79 | exist = true 80 | break 81 | } 82 | } 83 | if !exist { 84 | engine.ProcessInstance.RelatedPerson = append(engine.ProcessInstance.RelatedPerson, engine.userIdentifier) 85 | } 86 | } 87 | 88 | // 通过当前用户id获取当前审批的是哪个state的 89 | func (engine *ProcessEngine) GetStatesByCurrentUserId() dto.StateArray { 90 | states := dto.StateArray{} 91 | for _, state := range engine.ProcessInstance.State { 92 | // 审核者中有当前角色,但是审核完成中没有 93 | if util.SliceAnyString(state.Processor, engine.userIdentifier) && !util.SliceAnyString(state.CompletedProcessor, engine.userIdentifier) { 94 | states = append(states, state) 95 | } 96 | } 97 | 98 | return states 99 | } 100 | -------------------------------------------------------------------------------- /src/service/engine/processengine.exclusive.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/21 21:34 4 | * @Desc: 排他网关的相关方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "strings" 12 | 13 | "workflow/src/global" 14 | "workflow/src/global/constant" 15 | "workflow/src/model/dto" 16 | "workflow/src/model/request" 17 | "workflow/src/util" 18 | ) 19 | 20 | // 处理排他网关的跳转 21 | func (engine *ProcessEngine) ProcessingExclusiveGateway(gatewayNode dto.Node, r *request.HandleInstancesRequest) error { 22 | // 1. 找到所有source为当前网关节点的edges, 并按照sort排序 23 | edges := engine.GetEdges(gatewayNode.Id, "source") 24 | 25 | // 2. 遍历edges, 获取当前第一个符合条件的edge 26 | hitEdge := dto.Edge{} 27 | for _, edge := range edges { 28 | if edge.ConditionExpression == "" { 29 | return errors.New("处理失败, 排他网关的后续流程的条件表达式不能为空, 请检查") 30 | } 31 | 32 | // 进行条件判断 33 | condExprStatus, err := engine.ConditionJudgment(edge.ConditionExpression) 34 | if err != nil { 35 | return err 36 | } 37 | // 获取成功的节点 38 | if condExprStatus { 39 | hitEdge = edge 40 | break 41 | } 42 | } 43 | 44 | if hitEdge.Id == "" { 45 | return errors.New("没有符合条件的流向,请检查") 46 | } 47 | 48 | // 3. 获取必要的信息 49 | newTargetNode, err := engine.GetTargetNodeByEdgeId(hitEdge.Id) 50 | if err != nil { 51 | return errors.New("模板结构错误") 52 | } 53 | 54 | // 4. 合并获得最新的states 55 | var removeStateId string 56 | if engine.sourceNode.Clazz == constant.ExclusiveGateway || engine.sourceNode.Clazz == constant.ParallelGateway { 57 | removeStateId = engine.targetNode.Id 58 | } else { 59 | removeStateId = engine.sourceNode.Id 60 | } 61 | 62 | newStates, err := engine.MergeStates(removeStateId, []dto.Node{newTargetNode}) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // 5. 更新最新的node edge等信息 68 | engine.SetCurrentNodeEdgeInfo(&gatewayNode, &hitEdge, &newTargetNode) 69 | 70 | // 6. 根据edge进行跳转 71 | err = engine.Circulation(newStates) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // 条件表达式判断 80 | func (engine *ProcessEngine) ConditionJudgment(condExpr string) (bool, error) { 81 | // 先获取变量列表 82 | variables := util.UnmarshalToInstanceVariables(engine.ProcessInstance.Variables) 83 | 84 | envMap := make(map[string]interface{}, len(variables)) 85 | for _, variable := range variables { 86 | envMap[variable.Name] = variable.Value 87 | } 88 | 89 | // 替换变量表达式符 90 | condExpr = strings.Replace(condExpr, "{{", "", -1) 91 | condExpr = strings.Replace(condExpr, "}}", "", -1) 92 | condExpr = strings.Replace(condExpr, ">", ">", -1) 93 | condExpr = strings.Replace(condExpr, "<", "<", -1) 94 | 95 | result, err := util.CalculateExpression(condExpr, envMap) 96 | if err != nil { 97 | err = fmt.Errorf("计算表达式发生错误, 当前表达式:%s ,当前变量:%v, 错误原因:%s", condExpr, envMap, err.Error()) 98 | global.BankLogger.Error(err) 99 | return false, err 100 | } 101 | 102 | return result, nil 103 | } 104 | -------------------------------------------------------------------------------- /src/service/engine/processengine.countersign.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/27 21:59 4 | * @Desc: 会签相关方法 5 | */ 6 | package engine 7 | 8 | import ( 9 | "errors" 10 | "time" 11 | 12 | "workflow/src/util" 13 | ) 14 | 15 | // 判断是否是会签,如果是就更新相关状态 16 | func (engine *ProcessEngine) JudgeCounterSign() (isCounterSign bool, isLastProcessor bool, err error) { 17 | // 判断当前节点是否会签 18 | isCounterSign = engine.IsCounterSign() 19 | isLastProcessor = true 20 | 21 | // 不是会签 或者 流程为拒绝的情况下 直接退出 22 | if !isCounterSign || engine.linkEdge.FlowProperties == "0" { 23 | return 24 | } 25 | 26 | // 是最后一个人也退出 27 | isLastProcessor, err = engine.IsCounterSignLastProcessor() 28 | if err != nil || isLastProcessor { 29 | return 30 | } 31 | 32 | // 不是最后一个审批者则更新相关信息 33 | err = engine.UpdateInstanceForCounterSign() 34 | return 35 | } 36 | 37 | // 判断是否会签 38 | func (engine *ProcessEngine) IsCounterSign() bool { 39 | isCounterSign := false 40 | for _, state := range engine.ProcessInstance.State { 41 | if state.Id == engine.sourceNode.Id { 42 | isCounterSign = state.IsCounterSign 43 | break 44 | } 45 | } 46 | 47 | return isCounterSign 48 | } 49 | 50 | // 判断当前用户是否是最后一个未审批的 51 | // 并且更新CompletedProcessor字段 52 | func (engine *ProcessEngine) IsCounterSignLastProcessor() (bool, error) { 53 | isLastPerson := false 54 | matched := false 55 | for index, state := range engine.ProcessInstance.State { 56 | if state.Id == engine.sourceNode.Id { 57 | // 取差集获取未审批的人 58 | unCompletedProcessor := util.SliceDiff(state.Processor, state.CompletedProcessor) 59 | 60 | // 判断是否是最后一个未审批的人 61 | if len(unCompletedProcessor) == 1 && unCompletedProcessor[0] == engine.userIdentifier { 62 | isLastPerson = true 63 | } 64 | 65 | // 更新CompletedProcessor字段 66 | engine.ProcessInstance.State[index].CompletedProcessor = append(engine.ProcessInstance.State[index].CompletedProcessor, engine.userIdentifier) 67 | engine.ProcessInstance.State[index].UnCompletedProcessor = engine.RemoveCurrentFromUnCompleted(engine.ProcessInstance.State[index].UnCompletedProcessor) 68 | matched = true 69 | break 70 | } 71 | } 72 | 73 | if !matched { 74 | return isLastPerson, errors.New("未找到当前的state,请检查") 75 | } 76 | 77 | return isLastPerson, nil 78 | } 79 | 80 | // 更新会签的流程状态 81 | // 必须是当前会签的角色不为最后一个人,且走的流程不为拒绝流程 82 | func (engine *ProcessEngine) UpdateInstanceForCounterSign() error { 83 | toUpdate := map[string]interface{}{ 84 | "state": engine.ProcessInstance.State, 85 | "update_time": time.Now().Local(), 86 | "update_by": engine.userIdentifier, 87 | "related_person": engine.ProcessInstance.RelatedPerson, 88 | "variables": engine.ProcessInstance.Variables, 89 | } 90 | 91 | err := engine.tx.Model(&engine.ProcessInstance). 92 | Updates(toUpdate). 93 | Error 94 | 95 | return err 96 | } 97 | 98 | func (engine *ProcessEngine) RemoveCurrentFromUnCompleted(unCompletedProcessors []string) []string { 99 | newArr := make([]string, len(unCompletedProcessors)-1) 100 | for _, it := range unCompletedProcessors { 101 | if it == engine.userIdentifier { 102 | continue 103 | } 104 | newArr = append(newArr, it) 105 | } 106 | 107 | return newArr 108 | } 109 | -------------------------------------------------------------------------------- /src/service/engine/processengine.parallel.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/28 13:49 4 | * @Desc: 并行网关相关逻辑 5 | */ 6 | package engine 7 | 8 | import ( 9 | "errors" 10 | "time" 11 | 12 | "workflow/src/model/dto" 13 | ) 14 | 15 | // 处理并行网关 16 | func (engine *ProcessEngine) ProcessParallelGateway() ([]dto.RelationInfo, error) { 17 | gatewayNode := engine.targetNode 18 | 19 | // 获取所有source为当前 网关id 的edge 20 | nextEdges := engine.GetEdges(gatewayNode.Id, "source") 21 | 22 | // 获取所有target为当前 网关id 的edge 23 | sourceEdges := engine.GetEdges(gatewayNode.Id, "target") 24 | 25 | // 判断当前是fork还是join 26 | switch { 27 | // fork 28 | case len(sourceEdges) == 1 && len(nextEdges) >= 1: 29 | return engine.ProcessParallelFork(*gatewayNode, nextEdges) 30 | 31 | // join 32 | case len(sourceEdges) >= 1 && len(nextEdges) == 1: 33 | return engine.ProcessParallelJoin(*gatewayNode, sourceEdges, nextEdges[0]) 34 | 35 | default: 36 | return nil, errors.New("并行网关流程不正确") 37 | } 38 | } 39 | 40 | // 处理并行网关的fork 41 | func (engine *ProcessEngine) ProcessParallelFork(gatewayNode dto.Node, nextEdges []dto.Edge) ([]dto.RelationInfo, error) { 42 | infos := make([]dto.RelationInfo, 0, 1) 43 | 44 | // 先获取并行网关之后,接着的节点列表 45 | targetNodes := make([]dto.Node, 0, 1) 46 | for _, edge := range nextEdges { 47 | targetNode, err := engine.GetTargetNodeByEdgeId(edge.Id) 48 | if err != nil { 49 | return nil, err 50 | } 51 | targetNodes = append(targetNodes, targetNode) 52 | 53 | infos = append(infos, dto.RelationInfo{ 54 | SourceNode: gatewayNode, 55 | LinkedEdge: edge, 56 | TargetNode: targetNode, 57 | }) 58 | } 59 | 60 | // 根据节点获取state 61 | newStates, err := engine.MergeStates(engine.sourceNode.Id, targetNodes) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // 记录跳转 67 | err = engine.Circulation(newStates) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return infos, err 73 | } 74 | 75 | // 处理并行网关的join 76 | func (engine *ProcessEngine) ProcessParallelJoin(gatewayNode dto.Node, sourceEdges []dto.Edge, nextEdge dto.Edge) ([]dto.RelationInfo, error) { 77 | infos := make([]dto.RelationInfo, 0) 78 | 79 | // 获取当前ProcessInstance得state中,是gatewayNode前一个的个数 80 | gatewayPreviousNodes := engine.GetNodesByEdges(sourceEdges, "source") 81 | count := 0 82 | for _, state := range engine.ProcessInstance.State { 83 | for _, node := range gatewayPreviousNodes { 84 | if state.Id == node.Id { 85 | count++ 86 | } 87 | } 88 | } 89 | 90 | switch { 91 | // 大于1,说明还有其他线没有处理完, 不跳转, state数组中去掉当前state即可 92 | case count > 1: 93 | // 获取合并后的states 94 | mergedStates, err := engine.MergeStates(engine.sourceNode.Id, []dto.Node{}) 95 | if err != nil { 96 | return nil, err 97 | } 98 | // 更新到数据库 99 | err = engine.UpdateInstanceStateForParallel(mergedStates) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | // 等于1, 说明我其他的线都处理完了,可以跳到【当前并行网关】的【下一节点】 105 | case count == 1: 106 | // 获取最新的targetNode 107 | newTargetNode, err := engine.GetNode(nextEdge.Target) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | // 获取合并后的states 113 | mergedStates, err := engine.MergeStates(engine.sourceNode.Id, []dto.Node{newTargetNode}) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | // 更新跳转信息 119 | err = engine.Circulation(mergedStates) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | // 添加信息(跳到外层进行递归) 125 | infos = append(infos, dto.RelationInfo{ 126 | SourceNode: gatewayNode, 127 | LinkedEdge: nextEdge, 128 | TargetNode: newTargetNode, 129 | }) 130 | 131 | default: 132 | return nil, errors.New("当前流程结构不合法, 请检查") 133 | } 134 | 135 | return infos, nil 136 | } 137 | 138 | func (engine *ProcessEngine) UpdateInstanceStateForParallel(mergedStates dto.StateArray) error { 139 | toUpdate := map[string]interface{}{ 140 | "state": mergedStates, 141 | "update_time": time.Now().Local(), 142 | "update_by": engine.userIdentifier, 143 | "related_person": engine.ProcessInstance.RelatedPerson, 144 | "variables": engine.ProcessInstance.Variables, 145 | } 146 | 147 | err := engine.tx.Model(&engine.ProcessInstance). 148 | Updates(toUpdate). 149 | Error 150 | 151 | return err 152 | } 153 | -------------------------------------------------------------------------------- /src/service/roleusersservice.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/20 22:08 4 | * @Desc: 外部系统的角色用户对应关系 服务 5 | */ 6 | package service 7 | 8 | import ( 9 | . "github.com/ahmetb/go-linq/v3" 10 | 11 | "workflow/src/global" 12 | "workflow/src/model" 13 | "workflow/src/model/request" 14 | ) 15 | 16 | // 异步批量同步外部系统的角色用户对应关系 17 | func BatchSyncRoleUsers(r *request.BatchSyncUserRoleRequest, tenantId int) error { 18 | go BatchSyncRoleUsersAsync(r, tenantId) 19 | 20 | return nil 21 | } 22 | 23 | func BatchSyncRoleUsersAsync(r *request.BatchSyncUserRoleRequest, tenantId int) { 24 | users, roles, userRoles := r.ToDbEntities(tenantId) 25 | 26 | // 同步user 27 | err := DeleteOriginUsers(users, tenantId) 28 | if err != nil { 29 | global.BankLogger.Error("删除用户信息失败", err) 30 | } 31 | err = DeleteOriginRoles(roles, tenantId) 32 | if err != nil { 33 | global.BankLogger.Error("删除角色信息失败", err) 34 | } 35 | err = DeleteOriginUserRoles(userRoles, tenantId) 36 | if err != nil { 37 | global.BankLogger.Error("删除角色用户关联信息失败", err) 38 | } 39 | 40 | // 批量创建数据 41 | err = global.BankDb. 42 | Model(&model.User{}). 43 | Create(&users). 44 | Error 45 | if err != nil { 46 | global.BankLogger.Error("批量更新用户失败", err) 47 | } 48 | err = global.BankDb. 49 | Model(&model.Role{}). 50 | Create(&roles). 51 | Error 52 | if err != nil { 53 | global.BankLogger.Error("批量更新角色失败", err) 54 | } 55 | err = global.BankDb. 56 | Model(&model.UserRole{}). 57 | Create(&userRoles). 58 | Error 59 | if err != nil { 60 | global.BankLogger.Error("批量更新用户角色关联关系失败", err) 61 | } 62 | 63 | global.BankLogger.Infof("批量更新角色用户对应关系成功,更新用户条数:%d,更新角色条数:%d,更新关联关系条数:%d\n", len(users), len(roles), len(userRoles)) 64 | } 65 | 66 | func DeleteOriginUsers(users []model.User, tenantId int) error { 67 | var userIds []string 68 | From(users).Select(func(i interface{}) interface{} { 69 | return i.(model.User).Identifier 70 | }).ToSlice(&userIds) 71 | 72 | err := global.BankDb.Model(&model.User{}). 73 | Where("identifier in ?", userIds). 74 | Where("tenant_id = ?", tenantId). 75 | Delete(model.User{}). 76 | Error 77 | 78 | return err 79 | } 80 | 81 | func DeleteOriginRoles(roles []model.Role, tenantId int) error { 82 | var roleIds []string 83 | From(roles).Select(func(i interface{}) interface{} { 84 | return i.(model.Role).Identifier 85 | }).ToSlice(&roleIds) 86 | 87 | err := global.BankDb.Model(&model.Role{}). 88 | Where("identifier in ?", roleIds). 89 | Where("tenant_id = ?", tenantId). 90 | Delete(model.User{}). 91 | Error 92 | 93 | return err 94 | } 95 | 96 | func DeleteOriginUserRoles(userRoles []model.UserRole, tenantId int) error { 97 | var userIds []string 98 | var roleIds []string 99 | for _, userRole := range userRoles { 100 | userIds = append(userIds, userRole.UserIdentifier) 101 | roleIds = append(roleIds, userRole.RoleIdentifier) 102 | } 103 | 104 | err := global.BankDb.Model(&model.UserRole{}). 105 | Where("tenant_id = ?", tenantId). 106 | Where("user_identifier in ?", userIds). 107 | Or("role_identifier in ?", roleIds). 108 | Delete(model.UserRole{}). 109 | Error 110 | 111 | return err 112 | } 113 | 114 | // 115 | //// 批量同步外部系统的角色用户对应关系 116 | //func BatchSyncRoleUsersAsync(r *request.BatchSyncUserRoleRequest, tenantId int) { 117 | // var roleIds []int 118 | // From(r.RoleUsersList).Select(func(i interface{}) interface{} { 119 | // return i.(request.SyncRoleUsersRequest).RoleId 120 | // }).ToSlice(&roleIds) 121 | // 122 | // // 删除当前租户下当前请求的所有用户信息 123 | // err := global.BankDb. 124 | // Where("tenant_id = ?", tenantId). 125 | // Where("role_id in ?", roleIds). 126 | // Delete(model.RoleUsers{}). 127 | // Error 128 | // if err != nil { 129 | // log.Println(err.Error()) 130 | // return 131 | // } 132 | // 133 | // // 映射成数据库实体 134 | // var roleUsersList []model.RoleUsers 135 | // From(r.RoleUsersList).Select(func(i interface{}) interface{} { 136 | // re := i.(request.SyncRoleUsersRequest) 137 | // return re.ToRoleUsers(tenantId) 138 | // }).ToSlice(&roleUsersList) 139 | // 140 | // // 批量创建数据 141 | // err = global.BankDb. 142 | // Model(&model.RoleUsers{}). 143 | // Create(&roleUsersList). 144 | // Error 145 | // if err != nil { 146 | // global.BankLogger.Error("批量更新用户角色关系失败", err) 147 | // } 148 | // global.BankLogger.Infof("批量更新角色用户对应关系成功,更新条数:%d\n", len(roleUsersList)) 149 | //} 150 | 151 | // 152 | //// 同步外部系统的角色用户对应关系 153 | //func SyncRoleUsers(r *request.SyncRoleUsersRequest, tenantId int) error { 154 | // // 删除当前租户下当前请求的角色用户 155 | // err := global.BankDb. 156 | // Where("tenant_id = ?", tenantId). 157 | // Where("role_id = ?", r.RoleId). 158 | // Delete(model.RoleUsers{}). 159 | // Error 160 | // if err != nil { 161 | // global.BankLogger.Error("同步用户角色关系失败", err) 162 | // return err 163 | // } 164 | // 165 | // roleUsers := r.ToRoleUsers(tenantId) 166 | // err = global.BankDb. 167 | // Model(&model.RoleUsers{}). 168 | // Create(&roleUsers). 169 | // Error 170 | // if err != nil { 171 | // global.BankLogger.Error("同步用户角色关系失败", err) 172 | // return err 173 | // } 174 | // 175 | // global.BankLogger.Info("同步用户角色关系成功", r) 176 | // 177 | // return nil 178 | //} 179 | -------------------------------------------------------------------------------- /src/service/definitionservice.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/15 23:35 4 | * @Desc: 5 | */ 6 | package service 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "time" 13 | 14 | "github.com/labstack/echo/v4" 15 | "github.com/labstack/gommon/log" 16 | 17 | "workflow/src/global" 18 | "workflow/src/global/constant" 19 | "workflow/src/global/shared" 20 | "workflow/src/model" 21 | "workflow/src/model/request" 22 | "workflow/src/model/response" 23 | "workflow/src/util" 24 | ) 25 | 26 | func GetDefinition(id int, tenantId int) (*model.ProcessDefinition, error) { 27 | var definition model.ProcessDefinition 28 | 29 | err := global.BankDb. 30 | Where("id=?", id). 31 | Where("tenant_id=?", tenantId). 32 | First(&definition).Error 33 | if err != nil { 34 | global.BankLogger.Error(err) 35 | return nil, util.NewError("查询流程详情失败") 36 | } 37 | 38 | return &definition, nil 39 | } 40 | 41 | // 验证 42 | func ValidateDefinitionRequest(r *request.ProcessDefinitionRequest, excludeId int, tenantId int) error { 43 | // 验证名称是否已存在 44 | var c int64 45 | global.BankDb.Model(&model.ProcessDefinition{}). 46 | Where("name=?", r.Name). 47 | Where("id!=?", excludeId). 48 | Where("tenant_id=?", tenantId). 49 | Count(&c) 50 | if c != 0 { 51 | return util.BadRequest.Newf("当前名称为:\"%s\"的模板已存在", r.Name) 52 | } 53 | 54 | // 如果edge对象不存在id,则生成一个 55 | var definitionStructure map[string][]map[string]interface{} 56 | err := json.Unmarshal(r.Structure, &definitionStructure) 57 | if err != nil { 58 | return util.BadRequest.New("当前structure不合法,请检查") 59 | } 60 | 61 | for _, edge := range definitionStructure["edges"] { 62 | if edge["id"] == nil { 63 | edge["id"] = fmt.Sprintf("flow_%s", util.GenUUID()) 64 | } 65 | } 66 | r.Structure = util.MarshalToBytes(definitionStructure) 67 | 68 | // todo 校验structure的json 69 | 70 | return nil 71 | } 72 | 73 | // 创建新的process流程 74 | func CreateDefinition(r *request.ProcessDefinitionRequest, c echo.Context) (*model.ProcessDefinition, error) { 75 | var ( 76 | processDefinition = r.ProcessDefinition() 77 | tenantId, userIdentifier = util.GetWorkContext(c) 78 | ) 79 | processDefinition.CreateBy = userIdentifier 80 | processDefinition.UpdateBy = userIdentifier 81 | processDefinition.TenantId = tenantId 82 | 83 | err := global.BankDb.Create(&processDefinition).Error 84 | if err != nil { 85 | log.Error(err) 86 | return nil, util.NewError("创建失败") 87 | } 88 | 89 | return &processDefinition, nil 90 | } 91 | 92 | // 更新流程定义 93 | func UpdateDefinition(r *request.ProcessDefinitionRequest, c echo.Context) error { 94 | var ( 95 | processDefinition = r.ProcessDefinition() 96 | tenantId, userIdentifier = util.GetWorkContext(c) 97 | ) 98 | 99 | // 先查询 100 | var count int64 101 | err := global.BankDb.Model(&model.ProcessDefinition{}). 102 | Where("id=?", processDefinition.Id). 103 | Where("tenant_id=?", tenantId). 104 | Count(&count). 105 | Error 106 | if err != nil || count == 0 { 107 | return util.NotFound.New("记录不存在") 108 | } 109 | 110 | err = global.BankDb. 111 | Model(&processDefinition). 112 | Updates(map[string]interface{}{ 113 | "name": processDefinition.Name, 114 | "form_id": processDefinition.FormId, 115 | "structure": processDefinition.Structure, 116 | "classify_id": processDefinition.ClassifyId, 117 | "task": processDefinition.Task, 118 | "notice": processDefinition.Notice, 119 | "remarks": processDefinition.Remarks, 120 | "update_by": userIdentifier, 121 | "update_time": time.Now().Local(), 122 | }).Error 123 | 124 | return err 125 | } 126 | 127 | // 删除流程定义 128 | func DeleteDefinition(id int, tenantId int) error { 129 | // 先查询 130 | var count int64 131 | err := global.BankDb.Model(&model.ProcessDefinition{}). 132 | Where("id=?", id). 133 | Where("tenant_id=?", tenantId). 134 | Count(&count). 135 | Error 136 | if err != nil || count == 0 { 137 | return errors.New("记录不存在") 138 | } 139 | 140 | err = global.BankDb.Delete(model.ProcessDefinition{}, "id=?", id).Error 141 | 142 | if err != nil { 143 | return errors.New("流程不存在") 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func GetDefinitionList(r *request.DefinitionListRequest, c echo.Context) (interface{}, error) { 150 | var ( 151 | definitions []model.ProcessDefinition 152 | tenantId, userIdentifier = util.GetWorkContext(c) 153 | ) 154 | 155 | db := global.BankDb.Model(&model.ProcessDefinition{}).Where("tenant_id = ?", tenantId) 156 | 157 | // 根据type的不同有不同的逻辑 158 | switch r.Type { 159 | case constant.D_ICreated: 160 | db = db.Where("create_by=?", userIdentifier) 161 | break 162 | case constant.D_All: 163 | break 164 | default: 165 | return nil, util.BadRequest.New("type不合法") 166 | } 167 | 168 | if r.Keyword != "" { 169 | db = db.Where("name ~ ?", r.Keyword) 170 | } 171 | 172 | var count int64 173 | db.Count(&count) 174 | 175 | db = shared.ApplyPaging(db, &r.PagingRequest) 176 | err := db.Find(&definitions).Error 177 | 178 | return &response.PagingResponse{ 179 | TotalCount: count, 180 | CurrentCount: int64(len(definitions)), 181 | Data: &definitions, 182 | }, err 183 | } 184 | 185 | func CloneDefinition(r *request.CloneDefinitionRequest, c echo.Context) (*model.ProcessDefinition, error) { 186 | var ( 187 | tenantId, _ = util.GetWorkContext(c) 188 | ) 189 | definition, err := GetDefinition(r.Id, tenantId) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | newD := *definition 195 | fmt.Printf("xinde: %p,jiude: %p,san:%p", &newD, definition,&*definition) 196 | 197 | return nil, nil 198 | } -------------------------------------------------------------------------------- /src/controller/instancecontroller.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/16 21:43 4 | * @Desc: 流程实例 5 | */ 6 | package controller 7 | 8 | import ( 9 | "strconv" 10 | 11 | "github.com/labstack/echo/v4" 12 | 13 | "workflow/src/global/response" 14 | "workflow/src/model/request" 15 | "workflow/src/service" 16 | ) 17 | 18 | // @Tags process-instances 19 | // @Summary 创建新的流程实例 20 | // @Accept json 21 | // @Produce json 22 | // @param request body request.ProcessInstanceRequest true "request" 23 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 24 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 25 | // @Success 200 {object} response.HttpResponse 26 | // @Router /api/wf/process-instances [post] 27 | func CreateProcessInstance(c echo.Context) error { 28 | var r request.ProcessInstanceRequest 29 | if err := c.Bind(&r); err != nil { 30 | return response.BadRequest(c) 31 | } 32 | 33 | processInstance, err := service.CreateProcessInstance(&r, c) 34 | if err != nil { 35 | return response.Failed(c, err) 36 | } 37 | 38 | return response.OkWithData(c, processInstance) 39 | } 40 | 41 | // @Tags process-instances 42 | // @Summary 获取流程实例列表 43 | // @Accept json 44 | // @Produce json 45 | // @param request query request.InstanceListRequest true "request" 46 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 47 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 48 | // @Success 200 {object} response.HttpResponse 49 | // @Router /api/wf/process-instances [GET] 50 | func ListProcessInstances(c echo.Context) error { 51 | // 从queryString获取分页参数 52 | var r request.InstanceListRequest 53 | if err := c.Bind(&r); err != nil { 54 | return response.BadRequest(c) 55 | } 56 | 57 | instances, err := service.ListProcessInstance(&r, c) 58 | if err != nil { 59 | return response.Failed(c, err) 60 | } 61 | 62 | return response.OkWithData(c, instances) 63 | } 64 | 65 | // @Tags process-instances 66 | // @Summary 处理/审批一个流程 67 | // @Accept json 68 | // @Produce json 69 | // @param request body request.HandleInstancesRequest true "request" 70 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 71 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 72 | // @Success 200 {object} response.HttpResponse 73 | // @Router /api/wf/process-instances/_handle [POST] 74 | func HandleProcessInstance(c echo.Context) error { 75 | var r request.HandleInstancesRequest 76 | if err := c.Bind(&r); err != nil { 77 | return response.BadRequest(c) 78 | } 79 | 80 | instance, err := service.HandleProcessInstance(&r, c) 81 | if err != nil { 82 | return response.Failed(c, err) 83 | } 84 | 85 | return response.OkWithData(c, instance) 86 | } 87 | 88 | // @Tags process-instances 89 | // @Summary 否决流程流程 90 | // @Accept json 91 | // @Produce json 92 | // @param request body request.DenyInstanceRequest true "request" 93 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 94 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 95 | // @Success 200 {object} response.HttpResponse 96 | // @Router /api/wf/process-instances/_deny [POST] 97 | func DenyProcessInstance(c echo.Context) error { 98 | var r request.DenyInstanceRequest 99 | if err := c.Bind(&r); err != nil { 100 | return response.BadRequest(c) 101 | } 102 | 103 | instance, err := service.DenyProcessInstance(&r, c) 104 | if err != nil { 105 | return response.Failed(c, err) 106 | } 107 | 108 | return response.OkWithData(c, instance) 109 | } 110 | 111 | // @Tags process-instances 112 | // @Summary 获取一个流程实例 113 | // @Produce json 114 | // @param id path int true "request" 115 | // @param includeProcessTrain query bool false "request" 116 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 117 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 118 | // @Success 200 {object} response.HttpResponse 119 | // @Router /api/wf/process-instances/{id} [GET] 120 | func GetProcessInstance(c echo.Context) error { 121 | var r request.GetInstanceRequest 122 | if err := c.Bind(&r); err != nil { 123 | return response.BadRequest(c) 124 | } 125 | 126 | instance, err := service.GetProcessInstance(&r, c) 127 | if err != nil { 128 | return response.Failed(c, err) 129 | } 130 | 131 | return response.OkWithData(c, instance) 132 | } 133 | 134 | // @Tags process-instances 135 | // @Summary 获取流程链路 136 | // @Produce json 137 | // @param id path int true "request" 138 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 139 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 140 | // @Success 200 {object} response.HttpResponse 141 | // @Router /api/wf/process-instances/{id}/train-nodes [GET] 142 | func GetProcessTrain(c echo.Context) error { 143 | id, err := strconv.Atoi(c.Param("id")) 144 | if err != nil { 145 | return response.BadRequest(c) 146 | } 147 | 148 | trainNodes, err := service.GetProcessTrain(nil, id, c) 149 | if err != nil { 150 | return response.Failed(c, err) 151 | } 152 | 153 | return response.OkWithData(c, trainNodes) 154 | } 155 | 156 | // 获取流程实例中的变量 157 | //func GetInstanceVariable(c echo.Context) error { 158 | // var r request.GetVariableRequest 159 | // if err := c.Bind(&r); err != nil { 160 | // return response.FailedOblete(c, http.StatusBadRequest) 161 | // } 162 | // 163 | // resp, err := instanceService.GetVariable(&r) 164 | // if err != nil { 165 | // return response.FailWithMsg(c, http.StatusInternalServerError, err) 166 | // } 167 | // 168 | // return response.OkWithData(c, resp) 169 | //} 170 | // 171 | //func GetInstanceVariableList(c echo.Context) error { 172 | // var r request.GetVariableListRequest 173 | // if err := c.Bind(&r); err != nil { 174 | // return response.FailedOblete(c, http.StatusBadRequest) 175 | // } 176 | // variables, err := instanceService.ListVariables(&r) 177 | // if err != nil { 178 | // return response.FailWithMsg(c, http.StatusInternalServerError, err) 179 | // } 180 | // 181 | // return response.OkWithData(c, variables) 182 | //} 183 | -------------------------------------------------------------------------------- /src/controller/definitioncontroller.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/01/15 4 | * @Desc: process控制器 5 | */ 6 | package controller 7 | 8 | import ( 9 | "github.com/labstack/echo/v4" 10 | 11 | "workflow/src/global" 12 | "workflow/src/global/response" 13 | "workflow/src/model/request" 14 | "workflow/src/service" 15 | "workflow/src/util" 16 | ) 17 | 18 | // @Tags process-definitions 19 | // @Summary 创建流程模板 20 | // @Accept json 21 | // @Produce json 22 | // @param request body request.ProcessDefinitionRequest true "request" 23 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 24 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 25 | // @Success 200 {object} response.HttpResponse 26 | // @Router /api/wf/process-definitions [POST] 27 | func CreateProcessDefinition(c echo.Context) error { 28 | var ( 29 | r request.ProcessDefinitionRequest 30 | err error 31 | ) 32 | 33 | if err = c.Bind(&r); err != nil { 34 | return response.BadRequest(c) 35 | } 36 | 37 | // 验证 38 | tenantId := util.GetCurrentTenantId(c) 39 | err = service.ValidateDefinitionRequest(&r, 0, tenantId) 40 | if err != nil { 41 | return response.Failed(c, err) 42 | } 43 | 44 | // 创建 45 | processDefinition, err := service.CreateDefinition(&r, c) 46 | if err != nil { 47 | global.BankLogger.Error("CreateProcess错误", err) 48 | return response.Failed(c, err) 49 | } 50 | 51 | return response.OkWithData(c, processDefinition) 52 | } 53 | 54 | // @Tags process-definitions 55 | // @Summary 更新流程模板 56 | // @Accept json 57 | // @Produce json 58 | // @param request body request.ProcessDefinitionRequest true "request" 59 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 60 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 61 | // @Success 200 {object} response.HttpResponse 62 | // @Router /api/wf/process-definitions [PUT] 63 | func UpdateProcessDefinition(c echo.Context) error { 64 | var ( 65 | r request.ProcessDefinitionRequest 66 | err error 67 | ) 68 | 69 | if err = c.Bind(&r); err != nil { 70 | return response.BadRequest(c) 71 | } 72 | 73 | // 验证 74 | tenantId := util.GetCurrentTenantId(c) 75 | err = service.ValidateDefinitionRequest(&r, r.Id, tenantId) 76 | if err != nil { 77 | return response.Failed(c, err) 78 | } 79 | 80 | err = service.UpdateDefinition(&r, c) 81 | if err != nil { 82 | global.BankLogger.Error("UpdateProcessDefinition错误", err) 83 | return response.Failed(c, err) 84 | } 85 | 86 | return response.Ok(c) 87 | } 88 | 89 | // @Tags process-definitions 90 | // @Summary 删除流程模板 91 | // @Produce json 92 | // @param id path string true "request" 93 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 94 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 95 | // @Success 200 {object} response.HttpResponse 96 | // @Router /api/wf/process-definitions/{id} [DELETE] 97 | func DeleteProcessDefinition(c echo.Context) error { 98 | definitionId := c.Param("id") 99 | if definitionId == "" { 100 | return response.BadRequestWithMessage(c, "参数不正确,请确定参数processDefinitionId是否传递") 101 | } 102 | 103 | tenantId := util.GetCurrentTenantId(c) 104 | err := service.DeleteDefinition(util.StringToInt(definitionId), tenantId) 105 | if err != nil { 106 | return response.Failed(c, err) 107 | } 108 | 109 | return response.Ok(c) 110 | } 111 | 112 | // @Tags process-definitions 113 | // @Summary 获取流程模板详情 114 | // @Produce json 115 | // @param id path string true "request" 116 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 117 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 118 | // @Success 200 {object} response.HttpResponse 119 | // @Router /api/wf/process-definitions/{id} [GET] 120 | func GetProcessDefinition(c echo.Context) error { 121 | definitionId := c.Param("id") 122 | if definitionId == "" { 123 | return response.BadRequestWithMessage(c, "参数不正确,请确定参数processDefinitionId是否传递") 124 | } 125 | 126 | tenantId := util.GetCurrentTenantId(c) 127 | definition, err := service.GetDefinition(util.StringToInt(definitionId), tenantId) 128 | if err != nil { 129 | return response.Failed(c, err) 130 | } 131 | 132 | return response.OkWithData(c, definition) 133 | } 134 | 135 | // @Tags process-definitions 136 | // @Summary 获取流程定义列表 137 | // @Accept json 138 | // @Produce json 139 | // @param request query request.DefinitionListRequest true "request" 140 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 141 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 142 | // @Success 200 {object} response.HttpResponse 143 | // @Router /api/wf/process-definitions [GET] 144 | func ListProcessDefinition(c echo.Context) error { 145 | // 从queryString获取分页参数 146 | var r request.DefinitionListRequest 147 | if err := c.Bind(&r); err != nil { 148 | return response.BadRequest(c) 149 | } 150 | 151 | definitions, err := service.GetDefinitionList(&r, c) 152 | if err != nil { 153 | return response.Failed(c, err) 154 | } 155 | 156 | return response.OkWithData(c, definitions) 157 | } 158 | 159 | var ( 160 | DynamicPrefix = "/api" 161 | ) 162 | 163 | // @Tags process-definitions 164 | // @Summary 克隆流程定义列表 165 | // @Accept json 166 | // @Produce json 167 | // @param request body request.CloneDefinitionRequest true "request" 168 | // @param WF-TENANT-CODE header string true "WF-TENANT-CODE" 169 | // @param WF-CURRENT-USER header string true "WF-CURRENT-USER" 170 | // @Success 200 {object} response.HttpResponse 171 | // @Router /api/wf/process-definitions/_clone [POST] 172 | func CloneProcessDefinition(c echo.Context) error { 173 | // 从queryString获取分页参数 174 | var r request.CloneDefinitionRequest 175 | if err := c.Bind(&r); err != nil { 176 | return response.BadRequest(c) 177 | } 178 | 179 | denifition, err := service.CloneDefinition(&r, c) 180 | if err != nil { 181 | return response.Failed(c, err) 182 | } 183 | 184 | return response.OkWithData(c, denifition) 185 | } 186 | 187 | //// 分类流程列表 188 | //func ClassifyProcessList(c echo.Context) error { 189 | // var ( 190 | // err error 191 | // classifyIdList []int 192 | // classifyList []*struct { 193 | // process2.Classify 194 | // ProcessList []*process2.Info `json:"process_list"` 195 | // } 196 | // ) 197 | // 198 | // processName := c.DefaultQuery("name", "") 199 | // if processName == "" { 200 | // err = orm.Eloquent.Model(&process2.Classify{}).Find(&classifyList).Error 201 | // if err != nil { 202 | // app.Error(c, -1, err, fmt.Sprintf("获取分类列表失败,%v", err.Error())) 203 | // return 204 | // } 205 | // } else { 206 | // err = orm.Eloquent.Model(&process2.Info{}). 207 | // Where("name LIKE ?", fmt.Sprintf("%%%v%%", processName)). 208 | // Pluck("distinct classify", &classifyIdList).Error 209 | // if err != nil { 210 | // app.Error(c, -1, err, fmt.Sprintf("获取分类失败,%v", err.Error())) 211 | // return 212 | // } 213 | // 214 | // err = orm.Eloquent.Model(&process2.Classify{}). 215 | // Where("id in (?)", classifyIdList). 216 | // Find(&classifyList).Error 217 | // if err != nil { 218 | // app.Error(c, -1, err, fmt.Sprintf("获取分类失败,%v", err.Error())) 219 | // return 220 | // } 221 | // } 222 | // 223 | // for _, item := range classifyList { 224 | // err = orm.Eloquent.Model(&process2.Info{}). 225 | // Where("classify = ? and name LIKE ?", item.Id, fmt.Sprintf("%%%v%%", processName)). 226 | // Select("id, create_time, update_time, name, icon, remarks"). 227 | // Find(&item.ProcessList).Error 228 | // if err != nil { 229 | // app.Error(c, -1, err, fmt.Sprintf("获取流程失败,%v", err.Error())) 230 | // return 231 | // } 232 | // } 233 | // 234 | // app.OK(c, classifyList, "成功获取数据") 235 | //} 236 | -------------------------------------------------------------------------------- /src/service/instanceservice.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/1/16 22:58 4 | * @Desc: 流程实例服务 5 | */ 6 | package service 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | . "github.com/ahmetb/go-linq/v3" 13 | "github.com/labstack/echo/v4" 14 | "gorm.io/gorm" 15 | 16 | "workflow/src/global" 17 | "workflow/src/global/constant" 18 | "workflow/src/global/shared" 19 | "workflow/src/model" 20 | "workflow/src/model/dto" 21 | "workflow/src/model/request" 22 | "workflow/src/model/response" 23 | "workflow/src/service/engine" 24 | "workflow/src/util" 25 | ) 26 | 27 | // 创建实例 28 | func CreateProcessInstance(r *request.ProcessInstanceRequest, c echo.Context) (*model.ProcessInstance, error) { 29 | var ( 30 | processDefinition model.ProcessDefinition // 流程模板 31 | tx = global.BankDb.Begin() // 开启事务 32 | tenantId, userIdentifier = util.GetWorkContext(c) 33 | ) 34 | 35 | // 检查变量是否合法 36 | err := validateVariables(r.Variables) 37 | if err != nil { 38 | return nil, util.BadRequest.New(err) 39 | } 40 | 41 | // 查询对应的流程模板 42 | err = global.BankDb. 43 | Where("id = ?", r.ProcessDefinitionId). 44 | Where("tenant_id = ?", tenantId). 45 | First(&processDefinition). 46 | Error 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // 初始化流程引擎 52 | instanceEngine, err := engine.NewProcessEngine(processDefinition, r.ToProcessInstance(userIdentifier, tenantId), userIdentifier, tenantId, tx) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // 将初始状态赋值给当前的流程实例 58 | if currentInstanceState, err := instanceEngine.GetInstanceInitialState(); err != nil { 59 | return nil, err 60 | } else { 61 | instanceEngine.ProcessInstance.State = currentInstanceState 62 | } 63 | 64 | // TODO 这里判断下一步是排他网关等情况 65 | 66 | // 更新instance的关联人 67 | instanceEngine.UpdateRelatedPerson() 68 | 69 | // 创建 70 | err = instanceEngine.CreateProcessInstance() 71 | if err != nil { 72 | tx.Rollback() 73 | } else { 74 | tx.Commit() 75 | } 76 | 77 | return &instanceEngine.ProcessInstance, err 78 | } 79 | 80 | // 获取单个ProcessInstance 81 | func GetProcessInstance(r *request.GetInstanceRequest, c echo.Context) (*response.ProcessInstanceResponse, error) { 82 | var ( 83 | instance model.ProcessInstance 84 | tenantId, userIdentifier = util.GetWorkContext(c) 85 | ) 86 | err := global.BankDb. 87 | Where("id=?", r.Id). 88 | Where("tenant_id = ?", tenantId). 89 | First(&instance). 90 | Error 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // 必须是相关的才能看到 96 | exist := false 97 | for _, state := range instance.State { 98 | for _, processor := range state.Processor { 99 | if processor == userIdentifier { 100 | exist = true 101 | break 102 | } 103 | } 104 | } 105 | if !exist { 106 | exist = From(instance.RelatedPerson).AnyWith(func(i interface{}) bool { 107 | return i.(string) == userIdentifier 108 | }) 109 | 110 | if !exist { 111 | return nil, util.NotFound.New("记录不存在") 112 | } 113 | } 114 | 115 | resp := response.ProcessInstanceResponse{ 116 | ProcessInstance: instance, 117 | } 118 | 119 | // 包括流程链路 120 | if r.IncludeProcessTrain { 121 | trainNodes, err := GetProcessTrain(&instance, instance.Id, c) 122 | if err != nil { 123 | return nil, err 124 | } 125 | resp.ProcessChainNodes = trainNodes 126 | } 127 | 128 | return &resp, nil 129 | } 130 | 131 | // 获取ProcessInstance列表 132 | func ListProcessInstance(r *request.InstanceListRequest, c echo.Context) (*response.PagingResponse, error) { 133 | var ( 134 | instances []model.ProcessInstance 135 | db *gorm.DB 136 | tenantId, userIdentifier = util.GetWorkContext(c) 137 | ) 138 | 139 | switch r.Type { 140 | case constant.I_MyToDo: 141 | return getTodoInstances(r, userIdentifier, tenantId) 142 | default: 143 | db = global.BankDb.Model(&model.ProcessInstance{}). 144 | Where("tenant_id = ?", tenantId) 145 | } 146 | 147 | // 根据type的不同有不同的逻辑 148 | switch r.Type { 149 | case constant.I_MyToDo: 150 | //db = db.Joins("cross join jsonb_array_elements(state) as elem").Where(fmt.Sprintf("elem -> 'processor' @> '[%v]'", userIdentifier)) 151 | break 152 | case constant.I_ICreated: 153 | db = db.Where("create_by=?", userIdentifier) 154 | break 155 | case constant.I_IRelated: 156 | db = db.Joins("cross join jsonb_array_elements(state) as elem"). 157 | Where(fmt.Sprintf("related_person @> Array[%s] or elem -> 'processor' @> '[%s]'", userIdentifier, userIdentifier)) 158 | break 159 | case constant.I_All: 160 | break 161 | default: 162 | return nil, util.BadRequest.New("type不合法") 163 | } 164 | 165 | if r.Keyword != "" { 166 | db = db.Where("title ~ ?", r.Keyword) 167 | } 168 | 169 | var count int64 170 | db.Count(&count) 171 | 172 | db = shared.ApplyPaging(db, &r.PagingRequest) 173 | err := db.Find(&instances).Error 174 | 175 | return &response.PagingResponse{ 176 | TotalCount: count, 177 | CurrentCount: int64(len(instances)), 178 | Data: &instances, 179 | }, err 180 | } 181 | 182 | // 处理/审批ProcessInstance 183 | func HandleProcessInstance(r *request.HandleInstancesRequest, c echo.Context) (*model.ProcessInstance, error) { 184 | var ( 185 | tx = global.BankDb.Begin() // 开启事务 186 | tenantId, userIdentifier = util.GetWorkContext(c) 187 | ) 188 | 189 | // 验证变量是否符合要求 190 | err := validateVariables(r.Variables) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | // 流程实例引擎 196 | processEngine, err := engine.NewProcessEngineByInstanceId(r.ProcessInstanceId, userIdentifier, tenantId, tx) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | // 验证合法性(1.edgeId是否合法 2.当前用户是否有权限处理) 202 | err = processEngine.ValidateHandleRequest(r) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | // 合并最新的变量 208 | processEngine.MergeVariables(r.Variables) 209 | 210 | // 处理操作, 判断这里的原因是因为上面都不会进行数据库改动操作 211 | err = processEngine.Handle(r) 212 | if err != nil { 213 | tx.Rollback() 214 | } else { 215 | tx.Commit() 216 | } 217 | 218 | return &processEngine.ProcessInstance, err 219 | } 220 | 221 | // 否决流程 222 | func DenyProcessInstance(r *request.DenyInstanceRequest, c echo.Context) (*model.ProcessInstance, error) { 223 | var ( 224 | tx = global.BankDb.Begin() // 开启事务 225 | tenantId, userIdentifier = util.GetWorkContext(c) 226 | ) 227 | 228 | // 流程实例引擎 229 | instanceEngine, err := engine.NewProcessEngineByInstanceId(r.ProcessInstanceId, userIdentifier, tenantId, tx) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | // 验证当前用户是否有权限处理 235 | err = instanceEngine.ValidateDenyRequest(r) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | // 处理 241 | err = instanceEngine.Deny(r) 242 | if err != nil { 243 | tx.Rollback() 244 | } else { 245 | tx.Commit() 246 | } 247 | 248 | return &instanceEngine.ProcessInstance, err 249 | } 250 | 251 | // 获取流程链(用于展示) 252 | func GetProcessTrain(pi *model.ProcessInstance, instanceId int, c echo.Context) ([]response.ProcessChainNode, error) { 253 | var ( 254 | instance model.ProcessInstance 255 | tenantId, _ = util.GetWorkContext(c) 256 | ) 257 | 258 | // 1. 获取流程实例(如果为空) 259 | if pi == nil { 260 | err := global.BankDb. 261 | Where("id=?", instanceId). 262 | Where("tenant_id = ?", tenantId). 263 | First(&instance). 264 | Error 265 | if err != nil { 266 | } 267 | } else { 268 | instance = *pi 269 | } 270 | 271 | // 2. 获取流程模板 272 | var definition model.ProcessDefinition 273 | err := global.BankDb. 274 | Where("id=?", instance.ProcessDefinitionId). 275 | Where("tenant_id = ?", tenantId). 276 | First(&definition). 277 | Error 278 | if err != nil { 279 | return nil, errors.New("当前流程对应的模板为空") 280 | } 281 | 282 | // 3. 获取实例的当前nodeId列表 283 | currentNodeIds := make([]string, len(instance.State)) 284 | for i, state := range instance.State { 285 | currentNodeIds[i] = state.Id 286 | } 287 | 288 | // 4. 获取所有的显示节点 289 | shownNodes := make([]dto.Node, 0) 290 | currentNodeSortRange := make([]int, 0) // 当前节点的顺序区间, 在这个区间内的顺序都当作当前节点 291 | initialNodeId := "" 292 | for _, node := range definition.Structure.Nodes { 293 | // 隐藏节点就跳过 294 | if node.IsHideNode { 295 | continue 296 | } 297 | // 获取当前节点的顺序 298 | if util.SliceAnyString(currentNodeIds, node.Id) { 299 | currentNodeSortRange = append(currentNodeSortRange, util.StringToInt(node.Sort)) 300 | } 301 | // 找出开始节点的id 302 | if node.Clazz == constant.START { 303 | initialNodeId = node.Id 304 | } 305 | 306 | shownNodes = append(shownNodes, node) 307 | } 308 | 309 | // 5. 遍历出可能的流程链路 310 | possibleTrainNodesList := make([][]string, 0, util.Pow(len(definition.Structure.Nodes), 2)) 311 | getPossibleTrainNode(definition.Structure, initialNodeId, []string{}, &possibleTrainNodesList) 312 | 313 | // 6. 遍历获取当前显示的节点是否必须显示的 314 | // 具体实现方法是遍历possibleTrainNodesList中每一个变量,然后看当前变量的hitCount是否等于len(possibleTrainNodesList) 315 | // 等于的话,说明在数组每个元素里面都出现了, 那么肯定是必须的 316 | hitCount := make(map[string]int, len(definition.Structure.Nodes)) 317 | for _, possibleTrainNodes := range possibleTrainNodesList { 318 | for _, trainNode := range possibleTrainNodes { 319 | hitCount[trainNode] += 1 320 | } 321 | } 322 | 323 | // 7. 获取当前节点的排序 324 | // 由于当前节点可能有多个,排序也相应的有多个,多以会有一个当前节点排序的最大值和最小值 325 | // 这个范围内圈起来的都被当作当前节点 326 | currentNodeMinSort, currentNodeMaxSort := util.SliceMinMax(currentNodeSortRange) 327 | 328 | // 8. 最后将shownNodes映射成model返回 329 | var trainNodes []response.ProcessChainNode 330 | From(shownNodes).Select(func(i interface{}) interface{} { 331 | node := i.(dto.Node) 332 | currentNodeSort := util.StringToInt(node.Sort) 333 | 334 | var status constant.ChainNodeStatus 335 | switch { 336 | case currentNodeSort < currentNodeMinSort: 337 | status = 1 // 已处理 338 | case currentNodeSort > currentNodeMaxSort: 339 | status = 3 // 未处理的后续节点 340 | default: 341 | // 如果是结束节点,则不显示为当前节点,显示为已处理 342 | if node.Clazz == constant.End { 343 | status = 1 344 | } else { // 其他的等于情况显示为当前节点 345 | status = 2 // 当前节点 346 | } 347 | } 348 | 349 | var nodeType int 350 | switch node.Clazz { 351 | case constant.START: 352 | nodeType = 1 353 | case constant.UserTask: 354 | nodeType = 2 355 | case constant.ExclusiveGateway: 356 | nodeType = 3 357 | case constant.End: 358 | nodeType = 4 359 | } 360 | 361 | return response.ProcessChainNode{ 362 | Name: node.Label, 363 | Id: node.Id, 364 | Obligatory: hitCount[node.Id] == len(possibleTrainNodesList), 365 | Status: status, 366 | Sort: currentNodeSort, 367 | NodeType: nodeType, 368 | } 369 | }).OrderBy(func(i interface{}) interface{} { 370 | return i.(response.ProcessChainNode).Sort 371 | }).ToSlice(&trainNodes) 372 | 373 | return trainNodes, nil 374 | } 375 | 376 | // 检查变量是否合法 377 | func validateVariables(variables []model.InstanceVariable) error { 378 | checkedVariables := make(map[string]model.InstanceVariable, 0) 379 | for _, v := range variables { 380 | // 检查是否重名 381 | if _, present := checkedVariables[v.Name]; present { 382 | return fmt.Errorf("当前变量名:%s 重复, 请检查", v.Name) 383 | } 384 | checkedVariables[v.Name] = v 385 | } 386 | 387 | return nil 388 | } 389 | 390 | // 获取所有的可能的流程链路 391 | // definitionStructure: 流程模板的结构 392 | // currentNodes: 当前需要遍历的节点 393 | // dependencies: 依赖项 394 | // possibleTrainNodes: 最终返回的可能的流程链路 395 | func getPossibleTrainNode(definitionStructure dto.Structure, currentNodeId string, dependencies []string, possibleTrainNodes *[][]string) { 396 | targetNodeIds := make([]string, 0) 397 | // 当前节点添加到依赖中 398 | dependencies = append(dependencies, currentNodeId) 399 | for _, edge := range definitionStructure.Edges { 400 | // 找到edge的source是当前nodeId的edge 401 | if edge.Source == currentNodeId && edge.FlowProperties != "0" { 402 | targetNodeIds = append(targetNodeIds, edge.Target) 403 | } 404 | } 405 | 406 | // 已经是最终节点了 407 | if len(targetNodeIds) == 0 { 408 | *possibleTrainNodes = append(*possibleTrainNodes, dependencies) 409 | } else { 410 | // 不是最终节点,继续递归遍历 411 | for _, targetNodeId := range targetNodeIds { 412 | getPossibleTrainNode(definitionStructure, targetNodeId, dependencies, possibleTrainNodes) 413 | } 414 | } 415 | } 416 | 417 | func getTodoInstances(r *request.InstanceListRequest, userIdentifier string, tenantId int) (*response.PagingResponse, error) { 418 | countSql := fmt.Sprintf(`with base as ( 419 | select *, 420 | jsonb_array_elements(state) as singleState 421 | from wf.process_instance 422 | where tenant_id = %d 423 | AND is_end = false 424 | AND is_denied = false 425 | ) 426 | select count(1) 427 | from base 428 | where singleState -> 'processor' @> '["%s"]' 429 | and singleState -> 'unCompletedProcessor' @> '["%s"]'`, 430 | tenantId, userIdentifier, userIdentifier) 431 | if r.Keyword != "" { 432 | countSql += fmt.Sprintf(" AND title ~ '%s'", r.Keyword) 433 | } 434 | var c int64 435 | err := global.BankDb.Raw(countSql).Scan(&c).Error 436 | if err != nil { 437 | return nil, err 438 | } 439 | 440 | sql := fmt.Sprintf(`with base as ( 441 | select *, 442 | jsonb_array_elements(state) as singleState 443 | from wf.process_instance 444 | where tenant_id = %d 445 | AND is_end = false 446 | AND is_denied = false 447 | ) 448 | select id, create_time, update_time, create_by, update_by, title, priority, 449 | process_definition_id, classify_id, is_end, is_denied, state, related_person, tenant_id, variables 450 | from base 451 | where singleState -> 'processor' @> '["%s"]' 452 | and singleState -> 'unCompletedProcessor' @> '["%s"]' `, 453 | tenantId, userIdentifier, userIdentifier) 454 | if r.Keyword != "" { 455 | sql += fmt.Sprintf(" AND title ~ '%s'", r.Keyword) 456 | } 457 | sql = shared.ApplyRawPaging(sql, &r.PagingRequest) 458 | var instances []model.ProcessInstance 459 | err = global.BankDb.Raw(sql).Scan(&instances).Error 460 | 461 | return &response.PagingResponse{ 462 | TotalCount: c, 463 | CurrentCount: int64(len(instances)), 464 | Data: &instances, 465 | }, err 466 | } 467 | -------------------------------------------------------------------------------- /src/service/engine/processengine.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: lzw5399 3 | * @Date: 2021/3/17 23:15 4 | * @Desc: 5 | */ 6 | package engine 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | . "github.com/ahmetb/go-linq/v3" 13 | "gorm.io/gorm" 14 | 15 | "workflow/src/global" 16 | "workflow/src/global/constant" 17 | "workflow/src/model" 18 | "workflow/src/model/dto" 19 | "workflow/src/model/request" 20 | "workflow/src/util" 21 | ) 22 | 23 | type ProcessEngine struct { 24 | tx *gorm.DB // 数据库事务对象 25 | userIdentifier string // 当前用户外部系统的id 26 | tenantId int // 当前租户id 27 | sourceNode *dto.Node // 流转的源node 28 | targetNode *dto.Node // 流转的目标node 29 | linkEdge *dto.Edge // sourceNode和targetNode中间连接的edge 30 | ProcessInstance model.ProcessInstance // 流程实例 31 | ProcessDefinition model.ProcessDefinition // 流程定义 32 | DefinitionStructure dto.Structure // ProcessDefinition.Structure的快捷方式 33 | } 34 | 35 | // 初始化流程引擎 36 | func NewProcessEngine(p model.ProcessDefinition, instance model.ProcessInstance, userIdentifier string, tenantId int, tx *gorm.DB) (*ProcessEngine, error) { 37 | return &ProcessEngine{ 38 | ProcessDefinition: p, 39 | ProcessInstance: instance, 40 | DefinitionStructure: p.Structure, 41 | userIdentifier: userIdentifier, 42 | tenantId: tenantId, 43 | tx: tx, 44 | }, nil 45 | } 46 | 47 | // 初始化流程引擎(带process_instance) 48 | func NewProcessEngineByInstanceId(processInstanceId int, userIdentifier string, tenantId int, tx *gorm.DB) (*ProcessEngine, error) { 49 | var processInstance model.ProcessInstance 50 | var processDefinition model.ProcessDefinition 51 | 52 | err := global.BankDb. 53 | Model(model.ProcessInstance{}). 54 | Where("id = ?", processInstanceId). 55 | Where("tenant_id = ?", tenantId). 56 | First(&processInstance). 57 | Error 58 | if err != nil { 59 | return nil, fmt.Errorf("找不到当前processInstanceId为 %v 的记录", processInstanceId) 60 | } 61 | 62 | err = global.BankDb. 63 | Model(model.ProcessDefinition{}). 64 | Where("id = ?", processInstance.ProcessDefinitionId). 65 | Where("tenant_id = ?", tenantId). 66 | First(&processDefinition). 67 | Error 68 | if err != nil { 69 | return nil, fmt.Errorf("找不到当前processDefinitionId为 %v 的记录", processInstance.ProcessDefinitionId) 70 | } 71 | 72 | return &ProcessEngine{ 73 | ProcessInstance: processInstance, 74 | ProcessDefinition: processDefinition, 75 | DefinitionStructure: processDefinition.Structure, 76 | userIdentifier: userIdentifier, 77 | tenantId: tenantId, 78 | tx: tx, 79 | }, nil 80 | } 81 | 82 | // 获取instance的初始state 83 | func (engine *ProcessEngine) GetInstanceInitialState() (dto.StateArray, error) { 84 | var startNode dto.Node 85 | for _, node := range engine.DefinitionStructure.Nodes { 86 | if node.Clazz == constant.START { 87 | startNode = node 88 | break 89 | } 90 | } 91 | 92 | // 获取firstEdge 93 | firstEdge := dto.Edge{} 94 | for _, edge := range engine.DefinitionStructure.Edges { 95 | if edge.Source == startNode.Id { 96 | firstEdge = edge 97 | break 98 | } 99 | } 100 | 101 | if firstEdge.Id == "" { 102 | return nil, errors.New("流程模板结构不合法, 请检查初始流程节点和初始顺序流") 103 | } 104 | 105 | firstEdgeTargetId := firstEdge.Target 106 | nextNode := dto.Node{} 107 | // 获取接下来的节点nextNode 108 | for _, node := range engine.DefinitionStructure.Nodes { 109 | if node.Id == firstEdgeTargetId { 110 | nextNode = node 111 | break 112 | } 113 | } 114 | if nextNode.Id == "" { 115 | return nil, errors.New("流程模板结构不合法, 请检查初始流程节点和初始顺序流") 116 | } 117 | 118 | // 获取初始的states 119 | return engine.GenNewStates([]dto.Node{nextNode}) 120 | } 121 | 122 | // 流程处理 123 | func (engine *ProcessEngine) Handle(r *request.HandleInstancesRequest) error { 124 | // 获取edge 125 | edge, err := engine.GetEdge(r.EdgeId) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | // 获取两个node 131 | sourceNode, _ := engine.GetNode(edge.Source) 132 | targetNode, err := engine.GetTargetNodeByEdgeId(r.EdgeId) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | // 设置当前的节点和顺序流信息 138 | engine.SetCurrentNodeEdgeInfo(&sourceNode, &edge, &targetNode) 139 | engine.UpdateRelatedPerson() 140 | 141 | // handle内部(有递归操作,针对比如网关后还是网关等场景) 142 | return engine.handleInternal(r, 1) 143 | } 144 | 145 | // 流程处理内部(用于递归) 146 | func (engine *ProcessEngine) handleInternal(r *request.HandleInstancesRequest, deepLevel int) error { 147 | // 判断当前节点是否会签 148 | isCounterSign, isLastProcessor, err := engine.JudgeCounterSign() 149 | if err != nil { 150 | return err 151 | } 152 | 153 | // 添加流转历史记录 154 | err = engine.CreateHistory(r.Remarks, false) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | // 是会签并且不是最后一个人 160 | // 则不需要判下面目标节点相关的逻辑,直接退出 161 | if isCounterSign && !isLastProcessor { 162 | return nil 163 | } 164 | 165 | // 递归中, 当sourceNode为【结束事件】的情况下targetNode会为空 166 | if engine.targetNode == nil { 167 | return nil 168 | } 169 | 170 | // 判断目标节点的类型,有不同的处理方式 171 | switch engine.targetNode.Clazz { 172 | case constant.UserTask: 173 | // 只有第一次进来,针对userTask才需要跳转. 后续的直接退出 174 | if deepLevel > 1 { 175 | break 176 | } 177 | newStates, err := engine.MergeStates(engine.sourceNode.Id, []dto.Node{*engine.targetNode}) 178 | if err != nil { 179 | return err 180 | } 181 | err = engine.Circulation(newStates) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | case constant.End: 187 | // 只有第一次进来,才需要Circulation跳转 188 | // 非第一次的递归记一条结束的日志就退出 189 | if deepLevel == 1 { 190 | newStates, err := engine.MergeStates(engine.sourceNode.Id, []dto.Node{*engine.targetNode}) 191 | if err != nil { 192 | return err 193 | } 194 | err = engine.Circulation(newStates) 195 | if err != nil { 196 | return err 197 | } 198 | } 199 | engine.SetCurrentNodeEdgeInfo(engine.targetNode, nil, nil) 200 | return engine.handleInternal(r, deepLevel+1) 201 | 202 | case constant.ExclusiveGateway: 203 | err := engine.ProcessingExclusiveGateway(*engine.targetNode, r) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | // 递归处理 209 | return engine.handleInternal(r, deepLevel+1) 210 | 211 | case constant.ParallelGateway: 212 | relationInfos, err := engine.ProcessParallelGateway() 213 | if err != nil { 214 | return err 215 | } 216 | 217 | // 递归处理 218 | for _, info := range relationInfos { 219 | engine.SetCurrentNodeEdgeInfo(&info.SourceNode, &info.LinkedEdge, &info.TargetNode) 220 | err = engine.handleInternal(r, deepLevel+1) 221 | if err != nil { 222 | return err 223 | } 224 | } 225 | return nil 226 | 227 | default: 228 | return fmt.Errorf("目前的下一步节点类型:%v,暂不支持", engine.targetNode.Clazz) 229 | } 230 | 231 | return nil 232 | } 233 | 234 | func (engine *ProcessEngine) SetCurrentNodeEdgeInfo(sourceNode *dto.Node, edge *dto.Edge, targetNode *dto.Node) { 235 | engine.sourceNode = sourceNode 236 | engine.linkEdge = edge 237 | engine.targetNode = targetNode 238 | } 239 | 240 | // 获取edge 241 | func (engine *ProcessEngine) GetEdge(edgeId string) (dto.Edge, error) { 242 | if len(engine.DefinitionStructure.Edges) == 0 { 243 | return dto.Edge{}, errors.New("当前模板结构不合法, 缺少edges, 请检查") 244 | } 245 | 246 | for _, edge := range engine.DefinitionStructure.Edges { 247 | if edge.Id == edgeId { 248 | return edge, nil 249 | } 250 | } 251 | 252 | return dto.Edge{}, fmt.Errorf("当前edgeId为:%s的edge不存在", edgeId) 253 | } 254 | 255 | // i.GetEdges("userTask123", "source") 获取所有source为userTask123的edges 256 | // i.GetEdges("userTask123", "target") 获取所有target为userTask123的edges 257 | func (engine *ProcessEngine) GetEdges(nodeId string, nodeIdType string) []dto.Edge { 258 | edges := make([]dto.Edge, 0) 259 | for _, edge := range engine.DefinitionStructure.Edges { 260 | switch nodeIdType { 261 | case "source": 262 | if edge.Source == nodeId { 263 | edges = append(edges, edge) 264 | } 265 | case "target": 266 | if edge.Target == nodeId { 267 | edges = append(edges, edge) 268 | } 269 | } 270 | } 271 | 272 | // 根据sort排序 273 | From(edges).OrderByT(func(i dto.Edge) interface{} { 274 | return i.Sort 275 | }) 276 | 277 | return edges 278 | } 279 | 280 | // 获取node 281 | func (engine *ProcessEngine) GetNode(nodeId string) (dto.Node, error) { 282 | if len(engine.DefinitionStructure.Nodes) == 0 { 283 | return dto.Node{}, errors.New("当前模板结构不合法, 缺少nodes, 请检查") 284 | } 285 | 286 | for _, node := range engine.DefinitionStructure.Nodes { 287 | if node.Id == nodeId { 288 | return node, nil 289 | } 290 | } 291 | 292 | return dto.Node{}, fmt.Errorf("当前nodeId为:%s的node不存在", nodeId) 293 | } 294 | 295 | // GetNodes(edges, "source") 获取edges中source属性指向的node的集合 296 | // GetNodes(edges, "target") 获取edges中target属性指向的node的集合 297 | func (engine *ProcessEngine) GetNodesByEdges(edges []dto.Edge, edgeType string) []dto.Node { 298 | nodes := make([]dto.Node, 0) 299 | for _, node := range engine.DefinitionStructure.Nodes { 300 | for _, edge := range edges { 301 | switch edgeType { 302 | case "source": 303 | if edge.Source == node.Id { 304 | nodes = append(nodes, node) 305 | } 306 | case "target": 307 | if edge.Target == node.Id { 308 | nodes = append(nodes, node) 309 | } 310 | } 311 | } 312 | } 313 | 314 | return nodes 315 | } 316 | 317 | // 获取edge上的targetNode 318 | func (engine *ProcessEngine) GetTargetNodeByEdgeId(edgeId string) (dto.Node, error) { 319 | edge, err := engine.GetEdge(edgeId) 320 | if err != nil { 321 | return dto.Node{}, err 322 | } 323 | 324 | return engine.GetNode(edge.Target) 325 | } 326 | 327 | // 获取全新的 数据库process_instance表存储的state字段的对象 328 | func (engine *ProcessEngine) GenNewStates(nodes []dto.Node) (dto.StateArray, error) { 329 | states := dto.StateArray{} 330 | for _, node := range nodes { 331 | state := dto.State{ 332 | Id: node.Id, 333 | Label: node.Label, 334 | Processor: []string{}, 335 | CompletedProcessor: []string{}, 336 | ProcessMethod: node.AssignType, // 处理方式(角色 用户等) 337 | AssignValue: node.AssignValue, // 指定的处理者(用户的id或者角色的id) 338 | AvailableEdges: []dto.Edge{}, 339 | IsCounterSign: node.IsCounterSign, 340 | } 341 | 342 | // 审批者是role的需要在这里转成person 343 | if node.AssignType != "" && node.AssignValue != nil { 344 | switch node.AssignType { 345 | case "role": // 审批者是role, 需要转成person 346 | processors, err := engine.GetUserIdsByRoleIds(node.AssignValue) 347 | if err != nil { 348 | return nil, err 349 | } 350 | state.Processor = processors 351 | state.UnCompletedProcessor = processors 352 | break 353 | case "person": // 审批者是person的话直接用原值 354 | state.Processor = node.AssignValue 355 | state.UnCompletedProcessor = node.AssignValue 356 | break 357 | default: 358 | return nil, fmt.Errorf("不支持的处理人类型: %s", node.AssignType) 359 | } 360 | } 361 | 362 | // 获取可用的edge 363 | availableEdges := make([]dto.Edge, 0, 1) 364 | for _, edge := range engine.DefinitionStructure.Edges { 365 | if edge.Source == node.Id { 366 | availableEdges = append(availableEdges, edge) 367 | } 368 | } 369 | state.AvailableEdges = availableEdges 370 | 371 | states = append(states, state) 372 | } 373 | 374 | return states, nil 375 | } 376 | 377 | // 合并state字段,返回合并之后的 378 | func (engine *ProcessEngine) MergeStates(removeNodeId string, newNodes []dto.Node) (dto.StateArray, error) { 379 | finalMergedStates := make(dto.StateArray, 0) 380 | 381 | // 原来的state排除掉removeNodeId 382 | for _, state := range engine.ProcessInstance.State { 383 | if state.Id == removeNodeId { 384 | continue 385 | } 386 | finalMergedStates = append(finalMergedStates, state) 387 | } 388 | 389 | // 新的node生成state 390 | newStates, err := engine.GenNewStates(newNodes) 391 | if err != nil { 392 | return nil, err 393 | } 394 | 395 | // 合并 396 | finalMergedStates = append(finalMergedStates, newStates...) 397 | 398 | return finalMergedStates, nil 399 | } 400 | 401 | // 通过角色id获取用户id 402 | func (engine *ProcessEngine) GetUserIdsByRoleIds(roleIds []string) ([]string, error) { 403 | var userIds []string 404 | err := global.BankDb.Model(&model.Role{}). 405 | Joins("inner join wf.user_role on user_role.role_identifier = role.identifier"). 406 | Joins("inner join wf.user on user_role.user_identifier = \"user\".identifier"). 407 | Where("role.tenant_id = ? and role.identifier in ?", engine.tenantId, roleIds). 408 | Select("\"user\".identifier"). 409 | Scan(&userIds). 410 | Error 411 | 412 | return userIds, err 413 | } 414 | 415 | // 合并更新变量 416 | func (engine *ProcessEngine) MergeVariables(newVariables []model.InstanceVariable) { 417 | // 反序列化出来 418 | originVariables := util.UnmarshalToInstanceVariables(engine.ProcessInstance.Variables) 419 | 420 | // 查询优化先整理成map 421 | originVariableMap := make(map[string]model.InstanceVariable, len(originVariables)) 422 | for _, v := range originVariables { 423 | originVariableMap[v.Name] = v 424 | } 425 | 426 | for _, v := range newVariables { 427 | originVariableMap[v.Name] = v 428 | } 429 | 430 | finalVariables := make([]model.InstanceVariable, 0) 431 | for _, v := range originVariableMap { 432 | finalVariables = append(finalVariables, v) 433 | } 434 | 435 | engine.ProcessInstance.Variables = util.MarshalToDbJson(finalVariables) 436 | } 437 | 438 | // 获取初始节点 439 | func (engine *ProcessEngine) GetInitialNode() (dto.Node, error) { 440 | startNode := dto.Node{} 441 | for _, node := range engine.DefinitionStructure.Nodes { 442 | if node.Clazz == constant.START { 443 | startNode = node 444 | } 445 | } 446 | 447 | if startNode.Id == "" { 448 | return startNode, errors.New("当前结构不合法") 449 | } 450 | 451 | return startNode, nil 452 | } 453 | 454 | // 通过sourceNode获取TargetNodes列表 455 | func (engine *ProcessEngine) GetTargetNodes(sourceNode dto.Node) ([]dto.Node, error) { 456 | edges := engine.GetEdges(sourceNode.Id, "source") 457 | 458 | nextNodes := make([]dto.Node, 0, 1) 459 | 460 | for _, edge := range edges { 461 | node, err := engine.GetTargetNodeByEdgeId(edge.Id) 462 | if err != nil { 463 | return nil, err 464 | } 465 | 466 | nextNodes = append(nextNodes, node) 467 | } 468 | 469 | return nextNodes, nil 470 | } 471 | 472 | // 根据nodeId获取state 473 | func (engine *ProcessEngine) GetStateByNodeId(nodeId string) (dto.State, error) { 474 | state := dto.State{} 475 | for _, s := range engine.ProcessInstance.State { 476 | if s.Id == nodeId { 477 | state = s 478 | } 479 | } 480 | 481 | if state.Id == "" { 482 | return state, errors.New("当前流程状态错误,对应的nodeId错误") 483 | } 484 | 485 | return state, nil 486 | } 487 | 488 | // 通过edgeId获取state 489 | func (engine *ProcessEngine) GetStateByEdgeId(edge dto.Edge) (dto.State, error) { 490 | state := dto.State{} 491 | for _, s := range engine.ProcessInstance.State { 492 | if s.Id == edge.Source { 493 | state = s 494 | break 495 | } 496 | } 497 | 498 | if state.Id == "" { 499 | return state, util.BadRequest.New("当前审批不合法, 请检查") 500 | } 501 | 502 | return state, nil 503 | } 504 | -------------------------------------------------------------------------------- /src/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /okk 2 | definitions: 3 | model.InstanceVariable: 4 | properties: 5 | name: 6 | description: 变量名 7 | type: string 8 | value: 9 | description: 变量值 10 | type: object 11 | type: object 12 | request.BatchSyncUserRoleRequest: 13 | properties: 14 | roles: 15 | items: 16 | $ref: '#/definitions/request.RoleRequest' 17 | type: array 18 | userRoles: 19 | items: 20 | $ref: '#/definitions/request.UserRoleRequest' 21 | type: array 22 | users: 23 | items: 24 | $ref: '#/definitions/request.UserRequest' 25 | type: array 26 | type: object 27 | request.CloneDefinitionRequest: 28 | properties: 29 | id: 30 | type: integer 31 | type: object 32 | request.DenyInstanceRequest: 33 | properties: 34 | nodeId: 35 | description: 所在节点的id 36 | type: string 37 | processInstanceId: 38 | description: 流程实例的id 39 | type: integer 40 | remarks: 41 | description: 备注 42 | type: string 43 | type: object 44 | request.HandleInstancesRequest: 45 | properties: 46 | edgeId: 47 | description: 走的流程的id 48 | type: string 49 | processInstanceId: 50 | description: 流程实例的id 51 | type: integer 52 | remarks: 53 | description: 备注 54 | type: string 55 | variables: 56 | description: 变量 57 | items: 58 | $ref: '#/definitions/model.InstanceVariable' 59 | type: array 60 | type: object 61 | request.ProcessDefinitionRequest: 62 | properties: 63 | classifyId: 64 | description: 分类ID 65 | type: integer 66 | formId: 67 | description: 对应的表单的id(仅对外部系统做一个标记) 68 | type: integer 69 | id: 70 | type: integer 71 | name: 72 | description: 流程名称 73 | type: string 74 | notice: 75 | description: 绑定通知 76 | type: string 77 | remarks: 78 | description: 流程备注 79 | type: string 80 | structure: 81 | description: 流程结构 82 | type: string 83 | task: 84 | description: 任务ID, array, 可执行多个任务,可以当成通知任务,每个节点都会去执行 85 | type: string 86 | type: object 87 | request.ProcessInstanceRequest: 88 | properties: 89 | processDefinitionId: 90 | description: 流程ID 91 | type: integer 92 | title: 93 | description: 流程实例标题 94 | type: string 95 | variables: 96 | description: 变量 97 | items: 98 | $ref: '#/definitions/model.InstanceVariable' 99 | type: array 100 | type: object 101 | request.RoleRequest: 102 | properties: 103 | identifier: 104 | type: string 105 | name: 106 | type: string 107 | type: object 108 | request.UserRequest: 109 | properties: 110 | identifier: 111 | type: string 112 | name: 113 | type: string 114 | type: object 115 | request.UserRoleRequest: 116 | properties: 117 | roleIdentifier: 118 | type: string 119 | userIdentifier: 120 | type: string 121 | type: object 122 | response.HttpResponse: 123 | properties: 124 | data: 125 | type: object 126 | message: 127 | type: object 128 | success: 129 | type: boolean 130 | type: object 131 | info: 132 | contact: {} 133 | paths: 134 | /api/wf/process-definitions: 135 | get: 136 | consumes: 137 | - application/json 138 | parameters: 139 | - description: 关键词 140 | in: query 141 | name: keyword 142 | type: string 143 | - description: 取的条数 144 | in: query 145 | name: limit 146 | type: integer 147 | - description: 跳过的条数 148 | in: query 149 | name: offset 150 | type: integer 151 | - description: asc或者是desc 152 | in: query 153 | name: order 154 | type: string 155 | - description: 排序键的名字,在各查询实现中默认值与可用值都不同 156 | in: query 157 | name: sort 158 | type: string 159 | - description: 类别 1=我创建的 2=所有 160 | in: query 161 | name: type 162 | type: integer 163 | - description: WF-TENANT-CODE 164 | in: header 165 | name: WF-TENANT-CODE 166 | required: true 167 | type: string 168 | - description: WF-CURRENT-USER 169 | in: header 170 | name: WF-CURRENT-USER 171 | required: true 172 | type: string 173 | produces: 174 | - application/json 175 | responses: 176 | "200": 177 | description: OK 178 | schema: 179 | $ref: '#/definitions/response.HttpResponse' 180 | summary: 获取流程定义列表 181 | tags: 182 | - process-definitions 183 | post: 184 | consumes: 185 | - application/json 186 | parameters: 187 | - description: request 188 | in: body 189 | name: request 190 | required: true 191 | schema: 192 | $ref: '#/definitions/request.ProcessDefinitionRequest' 193 | - description: WF-TENANT-CODE 194 | in: header 195 | name: WF-TENANT-CODE 196 | required: true 197 | type: string 198 | - description: WF-CURRENT-USER 199 | in: header 200 | name: WF-CURRENT-USER 201 | required: true 202 | type: string 203 | produces: 204 | - application/json 205 | responses: 206 | "200": 207 | description: OK 208 | schema: 209 | $ref: '#/definitions/response.HttpResponse' 210 | summary: 创建流程模板 211 | tags: 212 | - process-definitions 213 | put: 214 | consumes: 215 | - application/json 216 | parameters: 217 | - description: request 218 | in: body 219 | name: request 220 | required: true 221 | schema: 222 | $ref: '#/definitions/request.ProcessDefinitionRequest' 223 | - description: WF-TENANT-CODE 224 | in: header 225 | name: WF-TENANT-CODE 226 | required: true 227 | type: string 228 | - description: WF-CURRENT-USER 229 | in: header 230 | name: WF-CURRENT-USER 231 | required: true 232 | type: string 233 | produces: 234 | - application/json 235 | responses: 236 | "200": 237 | description: OK 238 | schema: 239 | $ref: '#/definitions/response.HttpResponse' 240 | summary: 更新流程模板 241 | tags: 242 | - process-definitions 243 | /api/wf/process-definitions/_clone: 244 | post: 245 | consumes: 246 | - application/json 247 | parameters: 248 | - description: request 249 | in: body 250 | name: request 251 | required: true 252 | schema: 253 | $ref: '#/definitions/request.CloneDefinitionRequest' 254 | - description: WF-TENANT-CODE 255 | in: header 256 | name: WF-TENANT-CODE 257 | required: true 258 | type: string 259 | - description: WF-CURRENT-USER 260 | in: header 261 | name: WF-CURRENT-USER 262 | required: true 263 | type: string 264 | produces: 265 | - application/json 266 | responses: 267 | "200": 268 | description: OK 269 | schema: 270 | $ref: '#/definitions/response.HttpResponse' 271 | summary: 克隆流程定义列表 272 | tags: 273 | - process-definitions 274 | /api/wf/process-definitions/{id}: 275 | delete: 276 | parameters: 277 | - description: request 278 | in: path 279 | name: id 280 | required: true 281 | type: string 282 | - description: WF-TENANT-CODE 283 | in: header 284 | name: WF-TENANT-CODE 285 | required: true 286 | type: string 287 | - description: WF-CURRENT-USER 288 | in: header 289 | name: WF-CURRENT-USER 290 | required: true 291 | type: string 292 | produces: 293 | - application/json 294 | responses: 295 | "200": 296 | description: OK 297 | schema: 298 | $ref: '#/definitions/response.HttpResponse' 299 | summary: 删除流程模板 300 | tags: 301 | - process-definitions 302 | get: 303 | parameters: 304 | - description: request 305 | in: path 306 | name: id 307 | required: true 308 | type: string 309 | - description: WF-TENANT-CODE 310 | in: header 311 | name: WF-TENANT-CODE 312 | required: true 313 | type: string 314 | - description: WF-CURRENT-USER 315 | in: header 316 | name: WF-CURRENT-USER 317 | required: true 318 | type: string 319 | produces: 320 | - application/json 321 | responses: 322 | "200": 323 | description: OK 324 | schema: 325 | $ref: '#/definitions/response.HttpResponse' 326 | summary: 获取流程模板详情 327 | tags: 328 | - process-definitions 329 | /api/wf/process-instances: 330 | get: 331 | consumes: 332 | - application/json 333 | parameters: 334 | - description: 关键词 335 | in: query 336 | name: keyword 337 | type: string 338 | - description: 取的条数 339 | in: query 340 | name: limit 341 | type: integer 342 | - description: 跳过的条数 343 | in: query 344 | name: offset 345 | type: integer 346 | - description: asc或者是desc 347 | in: query 348 | name: order 349 | type: string 350 | - description: 排序键的名字,在各查询实现中默认值与可用值都不同 351 | in: query 352 | name: sort 353 | type: string 354 | - description: 类别 1=我的待办 2=我创建的 3=和我相关的 4=所有 355 | in: query 356 | name: type 357 | type: integer 358 | - description: WF-TENANT-CODE 359 | in: header 360 | name: WF-TENANT-CODE 361 | required: true 362 | type: string 363 | - description: WF-CURRENT-USER 364 | in: header 365 | name: WF-CURRENT-USER 366 | required: true 367 | type: string 368 | produces: 369 | - application/json 370 | responses: 371 | "200": 372 | description: OK 373 | schema: 374 | $ref: '#/definitions/response.HttpResponse' 375 | summary: 获取流程实例列表 376 | tags: 377 | - process-instances 378 | post: 379 | consumes: 380 | - application/json 381 | parameters: 382 | - description: request 383 | in: body 384 | name: request 385 | required: true 386 | schema: 387 | $ref: '#/definitions/request.ProcessInstanceRequest' 388 | - description: WF-TENANT-CODE 389 | in: header 390 | name: WF-TENANT-CODE 391 | required: true 392 | type: string 393 | - description: WF-CURRENT-USER 394 | in: header 395 | name: WF-CURRENT-USER 396 | required: true 397 | type: string 398 | produces: 399 | - application/json 400 | responses: 401 | "200": 402 | description: OK 403 | schema: 404 | $ref: '#/definitions/response.HttpResponse' 405 | summary: 创建新的流程实例 406 | tags: 407 | - process-instances 408 | /api/wf/process-instances/_deny: 409 | post: 410 | consumes: 411 | - application/json 412 | parameters: 413 | - description: request 414 | in: body 415 | name: request 416 | required: true 417 | schema: 418 | $ref: '#/definitions/request.DenyInstanceRequest' 419 | - description: WF-TENANT-CODE 420 | in: header 421 | name: WF-TENANT-CODE 422 | required: true 423 | type: string 424 | - description: WF-CURRENT-USER 425 | in: header 426 | name: WF-CURRENT-USER 427 | required: true 428 | type: string 429 | produces: 430 | - application/json 431 | responses: 432 | "200": 433 | description: OK 434 | schema: 435 | $ref: '#/definitions/response.HttpResponse' 436 | summary: 否决流程流程 437 | tags: 438 | - process-instances 439 | /api/wf/process-instances/_handle: 440 | post: 441 | consumes: 442 | - application/json 443 | parameters: 444 | - description: request 445 | in: body 446 | name: request 447 | required: true 448 | schema: 449 | $ref: '#/definitions/request.HandleInstancesRequest' 450 | - description: WF-TENANT-CODE 451 | in: header 452 | name: WF-TENANT-CODE 453 | required: true 454 | type: string 455 | - description: WF-CURRENT-USER 456 | in: header 457 | name: WF-CURRENT-USER 458 | required: true 459 | type: string 460 | produces: 461 | - application/json 462 | responses: 463 | "200": 464 | description: OK 465 | schema: 466 | $ref: '#/definitions/response.HttpResponse' 467 | summary: 处理/审批一个流程 468 | tags: 469 | - process-instances 470 | /api/wf/process-instances/{id}: 471 | get: 472 | parameters: 473 | - description: request 474 | in: path 475 | name: id 476 | required: true 477 | type: integer 478 | - description: request 479 | in: query 480 | name: includeProcessTrain 481 | type: boolean 482 | - description: WF-TENANT-CODE 483 | in: header 484 | name: WF-TENANT-CODE 485 | required: true 486 | type: string 487 | - description: WF-CURRENT-USER 488 | in: header 489 | name: WF-CURRENT-USER 490 | required: true 491 | type: string 492 | produces: 493 | - application/json 494 | responses: 495 | "200": 496 | description: OK 497 | schema: 498 | $ref: '#/definitions/response.HttpResponse' 499 | summary: 获取一个流程实例 500 | tags: 501 | - process-instances 502 | /api/wf/process-instances/{id}/history: 503 | get: 504 | parameters: 505 | - description: 实例id 506 | in: path 507 | name: id 508 | required: true 509 | type: integer 510 | - description: 关键词 511 | in: query 512 | name: keyword 513 | type: string 514 | - description: 取的条数 515 | in: query 516 | name: limit 517 | type: integer 518 | - description: 跳过的条数 519 | in: query 520 | name: offset 521 | type: integer 522 | - description: asc或者是desc 523 | in: query 524 | name: order 525 | type: string 526 | - description: 排序键的名字,在各查询实现中默认值与可用值都不同 527 | in: query 528 | name: sort 529 | type: string 530 | - description: 类别 1=完整日志 2=简洁日志 531 | in: query 532 | name: type 533 | type: integer 534 | - description: WF-TENANT-CODE 535 | in: header 536 | name: WF-TENANT-CODE 537 | required: true 538 | type: string 539 | - description: WF-CURRENT-USER 540 | in: header 541 | name: WF-CURRENT-USER 542 | required: true 543 | type: string 544 | produces: 545 | - application/json 546 | responses: 547 | "200": 548 | description: OK 549 | schema: 550 | $ref: '#/definitions/response.HttpResponse' 551 | summary: 获取流转历史列表 552 | tags: 553 | - process-instances 554 | /api/wf/process-instances/{id}/train-nodes: 555 | get: 556 | parameters: 557 | - description: request 558 | in: path 559 | name: id 560 | required: true 561 | type: integer 562 | - description: WF-TENANT-CODE 563 | in: header 564 | name: WF-TENANT-CODE 565 | required: true 566 | type: string 567 | - description: WF-CURRENT-USER 568 | in: header 569 | name: WF-CURRENT-USER 570 | required: true 571 | type: string 572 | produces: 573 | - application/json 574 | responses: 575 | "200": 576 | description: OK 577 | schema: 578 | $ref: '#/definitions/response.HttpResponse' 579 | summary: 获取流程链路 580 | tags: 581 | - process-instances 582 | /api/wf/role-users/_batch: 583 | post: 584 | consumes: 585 | - application/json 586 | parameters: 587 | - description: request 588 | in: body 589 | name: request 590 | required: true 591 | schema: 592 | $ref: '#/definitions/request.BatchSyncUserRoleRequest' 593 | - description: WF-TENANT-CODE 594 | in: header 595 | name: WF-TENANT-CODE 596 | required: true 597 | type: string 598 | - description: WF-CURRENT-USER 599 | in: header 600 | name: WF-CURRENT-USER 601 | required: true 602 | type: string 603 | produces: 604 | - application/json 605 | responses: 606 | "200": 607 | description: OK 608 | schema: 609 | $ref: '#/definitions/response.HttpResponse' 610 | summary: 批量同步(创建或更新)角色用户映射关系 611 | tags: 612 | - role-users 613 | /health/alive: 614 | get: 615 | consumes: 616 | - application/json 617 | produces: 618 | - application/json 619 | responses: 620 | "200": 621 | description: OK 622 | schema: 623 | $ref: '#/definitions/response.HttpResponse' 624 | tags: 625 | - health 626 | /health/ready: 627 | get: 628 | consumes: 629 | - application/json 630 | produces: 631 | - application/json 632 | responses: 633 | "200": 634 | description: OK 635 | schema: 636 | $ref: '#/definitions/response.HttpResponse' 637 | tags: 638 | - health 639 | swagger: "2.0" 640 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 4 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 5 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 6 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 7 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 8 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 9 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 10 | github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE= 11 | github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/kLZh/cj9U= 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 14 | github.com/antonmedv/expr v1.8.9 h1:O9stiHmHHww9b4ozhPx7T6BK7fXfOCHJ8ybxf0833zw= 15 | github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= 16 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 17 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 18 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 19 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 21 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg= 28 | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 29 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 30 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 31 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 32 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 33 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 34 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 35 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 36 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 37 | github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 38 | github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= 39 | github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 40 | github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= 41 | github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I= 42 | github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= 43 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 44 | github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= 45 | github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= 46 | github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= 47 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 48 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 49 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 50 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 51 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 52 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 53 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 54 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 55 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 56 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 57 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 58 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 59 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 60 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 61 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 62 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 63 | github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= 64 | github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 65 | github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 66 | github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA= 67 | github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= 68 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 69 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 70 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 71 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= 72 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 73 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 74 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 75 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 76 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 77 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 78 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 79 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 80 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 81 | github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 82 | github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 83 | github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= 84 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 85 | github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 86 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 87 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 88 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 89 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 90 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 91 | github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= 92 | github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= 93 | github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= 94 | github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= 95 | github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8= 96 | github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= 97 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 98 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 99 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 100 | github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= 101 | github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= 102 | github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= 103 | github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms= 104 | github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY= 105 | github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= 106 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 107 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 108 | github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 109 | github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 110 | github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 111 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 112 | github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= 113 | github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= 114 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 115 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 116 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 117 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 118 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 119 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 122 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 123 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 124 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 125 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 126 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 127 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 128 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 129 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 130 | github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= 131 | github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo= 132 | github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= 133 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 134 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 135 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 136 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 137 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 138 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 139 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 140 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 141 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 142 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 143 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 144 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 145 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 146 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 147 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 148 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 149 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 150 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 151 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 152 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 153 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 154 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 155 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 156 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 157 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 158 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 159 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 160 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 161 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 162 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 163 | github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= 164 | github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 165 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 166 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 167 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 168 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 169 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 170 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 171 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 172 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 173 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 174 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 175 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 176 | github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= 177 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 178 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 179 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 180 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 181 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 182 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 183 | github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= 184 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 185 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 186 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 187 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= 188 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 189 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 190 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 191 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 192 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 193 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 194 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 195 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 196 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 197 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 198 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 199 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 200 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 201 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 202 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 203 | github.com/swaggo/echo-swagger v1.1.0 h1:P46vSnGTjCo4PCDnztbyyiJ9csTt8/GvwL6UIhr4zEM= 204 | github.com/swaggo/echo-swagger v1.1.0/go.mod h1:JaipWDPqOBMwM40W6qz0o07lnPOxrhDkpjA2OaqfzL8= 205 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= 206 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= 207 | github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= 208 | github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= 209 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 210 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 211 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 212 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 213 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 214 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 215 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 216 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 217 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 218 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 219 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 220 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 221 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 222 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 223 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 224 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 225 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 226 | golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 227 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 228 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 229 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 230 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 231 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 232 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 233 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 234 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 235 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 236 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 237 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 238 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 239 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 240 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 241 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 242 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 243 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 244 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 247 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 248 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 249 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 250 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 251 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 252 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 253 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 254 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 256 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 257 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 258 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 263 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 264 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 265 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 266 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 270 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 272 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 273 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 274 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 275 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 276 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 277 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 278 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 279 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 280 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 281 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 282 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 283 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 284 | golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 285 | golang.org/x/tools v0.0.0-20201207182000-5679438983bd h1:aZYo+3GGTb9Pya0Di6t7G0JOwKGb782xQAJlZyVcwII= 286 | golang.org/x/tools v0.0.0-20201207182000-5679438983bd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 287 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 289 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 290 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 291 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 292 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 293 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 294 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 295 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 296 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 297 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 298 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 299 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 300 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 302 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 303 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 304 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 305 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 306 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 307 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 308 | gorm.io/datatypes v1.0.0 h1:5rDW3AnqXaacuQn6nB/ZNAIfTCIvmL5oKGa/TtCoBFA= 309 | gorm.io/datatypes v1.0.0/go.mod h1:aKpJ+RNhLXWeF5OAdxfzBwT1UPw1wseSchF0AY3/lSw= 310 | gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg= 311 | gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= 312 | gorm.io/driver/postgres v1.0.5/go.mod h1:qrD92UurYzNctBMVCJ8C3VQEjffEuphycXtxOudXNCA= 313 | gorm.io/driver/postgres v1.0.6 h1:9sqNcNC9PCkZ6tMzWF1cEE2PARlCONgSqRobszSTffw= 314 | gorm.io/driver/postgres v1.0.6/go.mod h1:r0nvX27yHDNbVeXMM9Y+9i5xSePcT18RfH8clP6wpwI= 315 | gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc= 316 | gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= 317 | gorm.io/driver/sqlserver v1.0.5 h1:n5knSvyaEwufxl0aROEW90pn+aLoV9h+vahYJk1x5l4= 318 | gorm.io/driver/sqlserver v1.0.5/go.mod h1:WI/bfZ+s9TigYXe3hb3XjNaUP0TqmTdXl11pECyLATs= 319 | gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 320 | gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 321 | gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 322 | gorm.io/gorm v1.20.5/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 323 | gorm.io/gorm v1.20.8/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 324 | gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc= 325 | gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 326 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 327 | -------------------------------------------------------------------------------- /src/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "basePath": "/okk", 7 | "paths": { 8 | "/api/wf/process-definitions": { 9 | "get": { 10 | "consumes": [ 11 | "application/json" 12 | ], 13 | "produces": [ 14 | "application/json" 15 | ], 16 | "tags": [ 17 | "process-definitions" 18 | ], 19 | "summary": "获取流程定义列表", 20 | "parameters": [ 21 | { 22 | "type": "string", 23 | "description": "关键词", 24 | "name": "keyword", 25 | "in": "query" 26 | }, 27 | { 28 | "type": "integer", 29 | "description": "取的条数", 30 | "name": "limit", 31 | "in": "query" 32 | }, 33 | { 34 | "type": "integer", 35 | "description": "跳过的条数", 36 | "name": "offset", 37 | "in": "query" 38 | }, 39 | { 40 | "type": "string", 41 | "description": "asc或者是desc", 42 | "name": "order", 43 | "in": "query" 44 | }, 45 | { 46 | "type": "string", 47 | "description": "排序键的名字,在各查询实现中默认值与可用值都不同", 48 | "name": "sort", 49 | "in": "query" 50 | }, 51 | { 52 | "type": "integer", 53 | "description": "类别 1=我创建的 2=所有", 54 | "name": "type", 55 | "in": "query" 56 | }, 57 | { 58 | "type": "string", 59 | "description": "WF-TENANT-CODE", 60 | "name": "WF-TENANT-CODE", 61 | "in": "header", 62 | "required": true 63 | }, 64 | { 65 | "type": "string", 66 | "description": "WF-CURRENT-USER", 67 | "name": "WF-CURRENT-USER", 68 | "in": "header", 69 | "required": true 70 | } 71 | ], 72 | "responses": { 73 | "200": { 74 | "description": "OK", 75 | "schema": { 76 | "$ref": "#/definitions/response.HttpResponse" 77 | } 78 | } 79 | } 80 | }, 81 | "put": { 82 | "consumes": [ 83 | "application/json" 84 | ], 85 | "produces": [ 86 | "application/json" 87 | ], 88 | "tags": [ 89 | "process-definitions" 90 | ], 91 | "summary": "更新流程模板", 92 | "parameters": [ 93 | { 94 | "description": "request", 95 | "name": "request", 96 | "in": "body", 97 | "required": true, 98 | "schema": { 99 | "$ref": "#/definitions/request.ProcessDefinitionRequest" 100 | } 101 | }, 102 | { 103 | "type": "string", 104 | "description": "WF-TENANT-CODE", 105 | "name": "WF-TENANT-CODE", 106 | "in": "header", 107 | "required": true 108 | }, 109 | { 110 | "type": "string", 111 | "description": "WF-CURRENT-USER", 112 | "name": "WF-CURRENT-USER", 113 | "in": "header", 114 | "required": true 115 | } 116 | ], 117 | "responses": { 118 | "200": { 119 | "description": "OK", 120 | "schema": { 121 | "$ref": "#/definitions/response.HttpResponse" 122 | } 123 | } 124 | } 125 | }, 126 | "post": { 127 | "consumes": [ 128 | "application/json" 129 | ], 130 | "produces": [ 131 | "application/json" 132 | ], 133 | "tags": [ 134 | "process-definitions" 135 | ], 136 | "summary": "创建流程模板", 137 | "parameters": [ 138 | { 139 | "description": "request", 140 | "name": "request", 141 | "in": "body", 142 | "required": true, 143 | "schema": { 144 | "$ref": "#/definitions/request.ProcessDefinitionRequest" 145 | } 146 | }, 147 | { 148 | "type": "string", 149 | "description": "WF-TENANT-CODE", 150 | "name": "WF-TENANT-CODE", 151 | "in": "header", 152 | "required": true 153 | }, 154 | { 155 | "type": "string", 156 | "description": "WF-CURRENT-USER", 157 | "name": "WF-CURRENT-USER", 158 | "in": "header", 159 | "required": true 160 | } 161 | ], 162 | "responses": { 163 | "200": { 164 | "description": "OK", 165 | "schema": { 166 | "$ref": "#/definitions/response.HttpResponse" 167 | } 168 | } 169 | } 170 | } 171 | }, 172 | "/api/wf/process-definitions/_clone": { 173 | "post": { 174 | "consumes": [ 175 | "application/json" 176 | ], 177 | "produces": [ 178 | "application/json" 179 | ], 180 | "tags": [ 181 | "process-definitions" 182 | ], 183 | "summary": "克隆流程定义列表", 184 | "parameters": [ 185 | { 186 | "description": "request", 187 | "name": "request", 188 | "in": "body", 189 | "required": true, 190 | "schema": { 191 | "$ref": "#/definitions/request.CloneDefinitionRequest" 192 | } 193 | }, 194 | { 195 | "type": "string", 196 | "description": "WF-TENANT-CODE", 197 | "name": "WF-TENANT-CODE", 198 | "in": "header", 199 | "required": true 200 | }, 201 | { 202 | "type": "string", 203 | "description": "WF-CURRENT-USER", 204 | "name": "WF-CURRENT-USER", 205 | "in": "header", 206 | "required": true 207 | } 208 | ], 209 | "responses": { 210 | "200": { 211 | "description": "OK", 212 | "schema": { 213 | "$ref": "#/definitions/response.HttpResponse" 214 | } 215 | } 216 | } 217 | } 218 | }, 219 | "/api/wf/process-definitions/{id}": { 220 | "get": { 221 | "produces": [ 222 | "application/json" 223 | ], 224 | "tags": [ 225 | "process-definitions" 226 | ], 227 | "summary": "获取流程模板详情", 228 | "parameters": [ 229 | { 230 | "type": "string", 231 | "description": "request", 232 | "name": "id", 233 | "in": "path", 234 | "required": true 235 | }, 236 | { 237 | "type": "string", 238 | "description": "WF-TENANT-CODE", 239 | "name": "WF-TENANT-CODE", 240 | "in": "header", 241 | "required": true 242 | }, 243 | { 244 | "type": "string", 245 | "description": "WF-CURRENT-USER", 246 | "name": "WF-CURRENT-USER", 247 | "in": "header", 248 | "required": true 249 | } 250 | ], 251 | "responses": { 252 | "200": { 253 | "description": "OK", 254 | "schema": { 255 | "$ref": "#/definitions/response.HttpResponse" 256 | } 257 | } 258 | } 259 | }, 260 | "delete": { 261 | "produces": [ 262 | "application/json" 263 | ], 264 | "tags": [ 265 | "process-definitions" 266 | ], 267 | "summary": "删除流程模板", 268 | "parameters": [ 269 | { 270 | "type": "string", 271 | "description": "request", 272 | "name": "id", 273 | "in": "path", 274 | "required": true 275 | }, 276 | { 277 | "type": "string", 278 | "description": "WF-TENANT-CODE", 279 | "name": "WF-TENANT-CODE", 280 | "in": "header", 281 | "required": true 282 | }, 283 | { 284 | "type": "string", 285 | "description": "WF-CURRENT-USER", 286 | "name": "WF-CURRENT-USER", 287 | "in": "header", 288 | "required": true 289 | } 290 | ], 291 | "responses": { 292 | "200": { 293 | "description": "OK", 294 | "schema": { 295 | "$ref": "#/definitions/response.HttpResponse" 296 | } 297 | } 298 | } 299 | } 300 | }, 301 | "/api/wf/process-instances": { 302 | "get": { 303 | "consumes": [ 304 | "application/json" 305 | ], 306 | "produces": [ 307 | "application/json" 308 | ], 309 | "tags": [ 310 | "process-instances" 311 | ], 312 | "summary": "获取流程实例列表", 313 | "parameters": [ 314 | { 315 | "type": "string", 316 | "description": "关键词", 317 | "name": "keyword", 318 | "in": "query" 319 | }, 320 | { 321 | "type": "integer", 322 | "description": "取的条数", 323 | "name": "limit", 324 | "in": "query" 325 | }, 326 | { 327 | "type": "integer", 328 | "description": "跳过的条数", 329 | "name": "offset", 330 | "in": "query" 331 | }, 332 | { 333 | "type": "string", 334 | "description": "asc或者是desc", 335 | "name": "order", 336 | "in": "query" 337 | }, 338 | { 339 | "type": "string", 340 | "description": "排序键的名字,在各查询实现中默认值与可用值都不同", 341 | "name": "sort", 342 | "in": "query" 343 | }, 344 | { 345 | "type": "integer", 346 | "description": "类别 1=我的待办 2=我创建的 3=和我相关的 4=所有", 347 | "name": "type", 348 | "in": "query" 349 | }, 350 | { 351 | "type": "string", 352 | "description": "WF-TENANT-CODE", 353 | "name": "WF-TENANT-CODE", 354 | "in": "header", 355 | "required": true 356 | }, 357 | { 358 | "type": "string", 359 | "description": "WF-CURRENT-USER", 360 | "name": "WF-CURRENT-USER", 361 | "in": "header", 362 | "required": true 363 | } 364 | ], 365 | "responses": { 366 | "200": { 367 | "description": "OK", 368 | "schema": { 369 | "$ref": "#/definitions/response.HttpResponse" 370 | } 371 | } 372 | } 373 | }, 374 | "post": { 375 | "consumes": [ 376 | "application/json" 377 | ], 378 | "produces": [ 379 | "application/json" 380 | ], 381 | "tags": [ 382 | "process-instances" 383 | ], 384 | "summary": "创建新的流程实例", 385 | "parameters": [ 386 | { 387 | "description": "request", 388 | "name": "request", 389 | "in": "body", 390 | "required": true, 391 | "schema": { 392 | "$ref": "#/definitions/request.ProcessInstanceRequest" 393 | } 394 | }, 395 | { 396 | "type": "string", 397 | "description": "WF-TENANT-CODE", 398 | "name": "WF-TENANT-CODE", 399 | "in": "header", 400 | "required": true 401 | }, 402 | { 403 | "type": "string", 404 | "description": "WF-CURRENT-USER", 405 | "name": "WF-CURRENT-USER", 406 | "in": "header", 407 | "required": true 408 | } 409 | ], 410 | "responses": { 411 | "200": { 412 | "description": "OK", 413 | "schema": { 414 | "$ref": "#/definitions/response.HttpResponse" 415 | } 416 | } 417 | } 418 | } 419 | }, 420 | "/api/wf/process-instances/_deny": { 421 | "post": { 422 | "consumes": [ 423 | "application/json" 424 | ], 425 | "produces": [ 426 | "application/json" 427 | ], 428 | "tags": [ 429 | "process-instances" 430 | ], 431 | "summary": "否决流程流程", 432 | "parameters": [ 433 | { 434 | "description": "request", 435 | "name": "request", 436 | "in": "body", 437 | "required": true, 438 | "schema": { 439 | "$ref": "#/definitions/request.DenyInstanceRequest" 440 | } 441 | }, 442 | { 443 | "type": "string", 444 | "description": "WF-TENANT-CODE", 445 | "name": "WF-TENANT-CODE", 446 | "in": "header", 447 | "required": true 448 | }, 449 | { 450 | "type": "string", 451 | "description": "WF-CURRENT-USER", 452 | "name": "WF-CURRENT-USER", 453 | "in": "header", 454 | "required": true 455 | } 456 | ], 457 | "responses": { 458 | "200": { 459 | "description": "OK", 460 | "schema": { 461 | "$ref": "#/definitions/response.HttpResponse" 462 | } 463 | } 464 | } 465 | } 466 | }, 467 | "/api/wf/process-instances/_handle": { 468 | "post": { 469 | "consumes": [ 470 | "application/json" 471 | ], 472 | "produces": [ 473 | "application/json" 474 | ], 475 | "tags": [ 476 | "process-instances" 477 | ], 478 | "summary": "处理/审批一个流程", 479 | "parameters": [ 480 | { 481 | "description": "request", 482 | "name": "request", 483 | "in": "body", 484 | "required": true, 485 | "schema": { 486 | "$ref": "#/definitions/request.HandleInstancesRequest" 487 | } 488 | }, 489 | { 490 | "type": "string", 491 | "description": "WF-TENANT-CODE", 492 | "name": "WF-TENANT-CODE", 493 | "in": "header", 494 | "required": true 495 | }, 496 | { 497 | "type": "string", 498 | "description": "WF-CURRENT-USER", 499 | "name": "WF-CURRENT-USER", 500 | "in": "header", 501 | "required": true 502 | } 503 | ], 504 | "responses": { 505 | "200": { 506 | "description": "OK", 507 | "schema": { 508 | "$ref": "#/definitions/response.HttpResponse" 509 | } 510 | } 511 | } 512 | } 513 | }, 514 | "/api/wf/process-instances/{id}": { 515 | "get": { 516 | "produces": [ 517 | "application/json" 518 | ], 519 | "tags": [ 520 | "process-instances" 521 | ], 522 | "summary": "获取一个流程实例", 523 | "parameters": [ 524 | { 525 | "type": "integer", 526 | "description": "request", 527 | "name": "id", 528 | "in": "path", 529 | "required": true 530 | }, 531 | { 532 | "type": "boolean", 533 | "description": "request", 534 | "name": "includeProcessTrain", 535 | "in": "query" 536 | }, 537 | { 538 | "type": "string", 539 | "description": "WF-TENANT-CODE", 540 | "name": "WF-TENANT-CODE", 541 | "in": "header", 542 | "required": true 543 | }, 544 | { 545 | "type": "string", 546 | "description": "WF-CURRENT-USER", 547 | "name": "WF-CURRENT-USER", 548 | "in": "header", 549 | "required": true 550 | } 551 | ], 552 | "responses": { 553 | "200": { 554 | "description": "OK", 555 | "schema": { 556 | "$ref": "#/definitions/response.HttpResponse" 557 | } 558 | } 559 | } 560 | } 561 | }, 562 | "/api/wf/process-instances/{id}/history": { 563 | "get": { 564 | "produces": [ 565 | "application/json" 566 | ], 567 | "tags": [ 568 | "process-instances" 569 | ], 570 | "summary": "获取流转历史列表", 571 | "parameters": [ 572 | { 573 | "type": "integer", 574 | "description": "实例id", 575 | "name": "id", 576 | "in": "path", 577 | "required": true 578 | }, 579 | { 580 | "type": "string", 581 | "description": "关键词", 582 | "name": "keyword", 583 | "in": "query" 584 | }, 585 | { 586 | "type": "integer", 587 | "description": "取的条数", 588 | "name": "limit", 589 | "in": "query" 590 | }, 591 | { 592 | "type": "integer", 593 | "description": "跳过的条数", 594 | "name": "offset", 595 | "in": "query" 596 | }, 597 | { 598 | "type": "string", 599 | "description": "asc或者是desc", 600 | "name": "order", 601 | "in": "query" 602 | }, 603 | { 604 | "type": "string", 605 | "description": "排序键的名字,在各查询实现中默认值与可用值都不同", 606 | "name": "sort", 607 | "in": "query" 608 | }, 609 | { 610 | "type": "integer", 611 | "description": "类别 1=完整日志 2=简洁日志", 612 | "name": "type", 613 | "in": "query" 614 | }, 615 | { 616 | "type": "string", 617 | "description": "WF-TENANT-CODE", 618 | "name": "WF-TENANT-CODE", 619 | "in": "header", 620 | "required": true 621 | }, 622 | { 623 | "type": "string", 624 | "description": "WF-CURRENT-USER", 625 | "name": "WF-CURRENT-USER", 626 | "in": "header", 627 | "required": true 628 | } 629 | ], 630 | "responses": { 631 | "200": { 632 | "description": "OK", 633 | "schema": { 634 | "$ref": "#/definitions/response.HttpResponse" 635 | } 636 | } 637 | } 638 | } 639 | }, 640 | "/api/wf/process-instances/{id}/train-nodes": { 641 | "get": { 642 | "produces": [ 643 | "application/json" 644 | ], 645 | "tags": [ 646 | "process-instances" 647 | ], 648 | "summary": "获取流程链路", 649 | "parameters": [ 650 | { 651 | "type": "integer", 652 | "description": "request", 653 | "name": "id", 654 | "in": "path", 655 | "required": true 656 | }, 657 | { 658 | "type": "string", 659 | "description": "WF-TENANT-CODE", 660 | "name": "WF-TENANT-CODE", 661 | "in": "header", 662 | "required": true 663 | }, 664 | { 665 | "type": "string", 666 | "description": "WF-CURRENT-USER", 667 | "name": "WF-CURRENT-USER", 668 | "in": "header", 669 | "required": true 670 | } 671 | ], 672 | "responses": { 673 | "200": { 674 | "description": "OK", 675 | "schema": { 676 | "$ref": "#/definitions/response.HttpResponse" 677 | } 678 | } 679 | } 680 | } 681 | }, 682 | "/api/wf/role-users/_batch": { 683 | "post": { 684 | "consumes": [ 685 | "application/json" 686 | ], 687 | "produces": [ 688 | "application/json" 689 | ], 690 | "tags": [ 691 | "role-users" 692 | ], 693 | "summary": "批量同步(创建或更新)角色用户映射关系", 694 | "parameters": [ 695 | { 696 | "description": "request", 697 | "name": "request", 698 | "in": "body", 699 | "required": true, 700 | "schema": { 701 | "$ref": "#/definitions/request.BatchSyncUserRoleRequest" 702 | } 703 | }, 704 | { 705 | "type": "string", 706 | "description": "WF-TENANT-CODE", 707 | "name": "WF-TENANT-CODE", 708 | "in": "header", 709 | "required": true 710 | }, 711 | { 712 | "type": "string", 713 | "description": "WF-CURRENT-USER", 714 | "name": "WF-CURRENT-USER", 715 | "in": "header", 716 | "required": true 717 | } 718 | ], 719 | "responses": { 720 | "200": { 721 | "description": "OK", 722 | "schema": { 723 | "$ref": "#/definitions/response.HttpResponse" 724 | } 725 | } 726 | } 727 | } 728 | }, 729 | "/health/alive": { 730 | "get": { 731 | "consumes": [ 732 | "application/json" 733 | ], 734 | "produces": [ 735 | "application/json" 736 | ], 737 | "tags": [ 738 | "health" 739 | ], 740 | "responses": { 741 | "200": { 742 | "description": "OK", 743 | "schema": { 744 | "$ref": "#/definitions/response.HttpResponse" 745 | } 746 | } 747 | } 748 | } 749 | }, 750 | "/health/ready": { 751 | "get": { 752 | "consumes": [ 753 | "application/json" 754 | ], 755 | "produces": [ 756 | "application/json" 757 | ], 758 | "tags": [ 759 | "health" 760 | ], 761 | "responses": { 762 | "200": { 763 | "description": "OK", 764 | "schema": { 765 | "$ref": "#/definitions/response.HttpResponse" 766 | } 767 | } 768 | } 769 | } 770 | } 771 | }, 772 | "definitions": { 773 | "model.InstanceVariable": { 774 | "type": "object", 775 | "properties": { 776 | "name": { 777 | "description": "变量名", 778 | "type": "string" 779 | }, 780 | "value": { 781 | "description": "变量值", 782 | "type": "object" 783 | } 784 | } 785 | }, 786 | "request.BatchSyncUserRoleRequest": { 787 | "type": "object", 788 | "properties": { 789 | "roles": { 790 | "type": "array", 791 | "items": { 792 | "$ref": "#/definitions/request.RoleRequest" 793 | } 794 | }, 795 | "userRoles": { 796 | "type": "array", 797 | "items": { 798 | "$ref": "#/definitions/request.UserRoleRequest" 799 | } 800 | }, 801 | "users": { 802 | "type": "array", 803 | "items": { 804 | "$ref": "#/definitions/request.UserRequest" 805 | } 806 | } 807 | } 808 | }, 809 | "request.CloneDefinitionRequest": { 810 | "type": "object", 811 | "properties": { 812 | "id": { 813 | "type": "integer" 814 | } 815 | } 816 | }, 817 | "request.DenyInstanceRequest": { 818 | "type": "object", 819 | "properties": { 820 | "nodeId": { 821 | "description": "所在节点的id", 822 | "type": "string" 823 | }, 824 | "processInstanceId": { 825 | "description": "流程实例的id", 826 | "type": "integer" 827 | }, 828 | "remarks": { 829 | "description": "备注", 830 | "type": "string" 831 | } 832 | } 833 | }, 834 | "request.HandleInstancesRequest": { 835 | "type": "object", 836 | "properties": { 837 | "edgeId": { 838 | "description": "走的流程的id", 839 | "type": "string" 840 | }, 841 | "processInstanceId": { 842 | "description": "流程实例的id", 843 | "type": "integer" 844 | }, 845 | "remarks": { 846 | "description": "备注", 847 | "type": "string" 848 | }, 849 | "variables": { 850 | "description": "变量", 851 | "type": "array", 852 | "items": { 853 | "$ref": "#/definitions/model.InstanceVariable" 854 | } 855 | } 856 | } 857 | }, 858 | "request.ProcessDefinitionRequest": { 859 | "type": "object", 860 | "properties": { 861 | "classifyId": { 862 | "description": "分类ID", 863 | "type": "integer" 864 | }, 865 | "formId": { 866 | "description": "对应的表单的id(仅对外部系统做一个标记)", 867 | "type": "integer" 868 | }, 869 | "id": { 870 | "type": "integer" 871 | }, 872 | "name": { 873 | "description": "流程名称", 874 | "type": "string" 875 | }, 876 | "notice": { 877 | "description": "绑定通知", 878 | "type": "string" 879 | }, 880 | "remarks": { 881 | "description": "流程备注", 882 | "type": "string" 883 | }, 884 | "structure": { 885 | "description": "流程结构", 886 | "type": "string" 887 | }, 888 | "task": { 889 | "description": "任务ID, array, 可执行多个任务,可以当成通知任务,每个节点都会去执行", 890 | "type": "string" 891 | } 892 | } 893 | }, 894 | "request.ProcessInstanceRequest": { 895 | "type": "object", 896 | "properties": { 897 | "processDefinitionId": { 898 | "description": "流程ID", 899 | "type": "integer" 900 | }, 901 | "title": { 902 | "description": "流程实例标题", 903 | "type": "string" 904 | }, 905 | "variables": { 906 | "description": "变量", 907 | "type": "array", 908 | "items": { 909 | "$ref": "#/definitions/model.InstanceVariable" 910 | } 911 | } 912 | } 913 | }, 914 | "request.RoleRequest": { 915 | "type": "object", 916 | "properties": { 917 | "identifier": { 918 | "type": "string" 919 | }, 920 | "name": { 921 | "type": "string" 922 | } 923 | } 924 | }, 925 | "request.UserRequest": { 926 | "type": "object", 927 | "properties": { 928 | "identifier": { 929 | "type": "string" 930 | }, 931 | "name": { 932 | "type": "string" 933 | } 934 | } 935 | }, 936 | "request.UserRoleRequest": { 937 | "type": "object", 938 | "properties": { 939 | "roleIdentifier": { 940 | "type": "string" 941 | }, 942 | "userIdentifier": { 943 | "type": "string" 944 | } 945 | } 946 | }, 947 | "response.HttpResponse": { 948 | "type": "object", 949 | "properties": { 950 | "data": { 951 | "type": "object" 952 | }, 953 | "message": { 954 | "type": "object" 955 | }, 956 | "success": { 957 | "type": "boolean" 958 | } 959 | } 960 | } 961 | } 962 | } --------------------------------------------------------------------------------