├── 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 |
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 |
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 | }
--------------------------------------------------------------------------------