最近在研究工單系統(tǒng)的時(shí)候,被我找到一個(gè)非常流弊的工單系統(tǒng),我們都知道工單系統(tǒng)最麻煩的就是流程和模版的維護(hù),并且,在工單處理過(guò)程中很可能會(huì)添加一些操作,這些操作被稱(chēng)之為鉤子。就按我目前調(diào)研的結(jié)果來(lái)說(shuō),目前其實(shí)沒(méi)有啥工單系統(tǒng)能實(shí)現(xiàn)的這么好的。
這個(gè)工單系統(tǒng)就把流程設(shè)計(jì),模版設(shè)計(jì)等等做的非常不錯(cuò),而且對(duì)權(quán)限的把控非常詳細(xì),包括API接口、菜單、頁(yè)面按鈕權(quán)限,都可以靈活的控制,非常的不錯(cuò)。
Demo:http://fdevops.com:8001/#/dashboard
Github:https://github.com/lanyulei/ferry
如果覺(jué)得不錯(cuò)就給作者一個(gè)star,你的star沒(méi)準(zhǔn)就是作者繼續(xù)維護(hù)下去的動(dòng)力呢。
功能介紹
系統(tǒng)管理
- 用戶(hù)管理不僅僅包括了用戶(hù),還有角色、職位、部門(mén)的管理,方便后面的工單處理人擴(kuò)展。
- 菜單管理,對(duì)菜單,頁(yè)面按鈕,甚至是API接口的管理。
- 登陸日志,對(duì)用戶(hù)登陸和退出的日志記錄。
流程中心
- 流程申請(qǐng),對(duì)流程進(jìn)行分類(lèi)管理,方便維護(hù)與可視化。
- 工單列表,拆分了4個(gè)分類(lèi),包括:我待辦的工單,我創(chuàng)建的工單,我相關(guān)的工單,還有所有工單。
- 轉(zhuǎn)交工單,如果你當(dāng)前有別的事情在處理就可以把工單轉(zhuǎn)交給別人去處理。
- 結(jié)束工單,如果一個(gè)工單發(fā)展申請(qǐng)不對(duì),權(quán)限足夠的話,是可以直接結(jié)束工單的。
- 工單處理人的多樣化,不僅可以個(gè)人處理,還可以是部門(mén)、角色、變量。
- 處理人變量,根據(jù)用戶(hù)數(shù)據(jù)來(lái)自動(dòng)獲得是該誰(shuí)處理,比如:創(chuàng)建人,創(chuàng)建人leader,HRBP等等。
- 會(huì)簽,如果是多個(gè)選擇人的話,并且勾選了會(huì)簽功能,那么就需要這些負(fù)責(zé)人都處理完成后才會(huì)通過(guò)。
- 任務(wù)管理,可以給任何階段綁定任務(wù),相當(dāng)于流程中的鉤子操作,實(shí)現(xiàn)的效果就是,工單完成,任務(wù)也就執(zhí)行完成了,減少很多的人力成本。
- 通知方式的靈活性,可以通過(guò)任務(wù)給每個(gè)階段綁定通知方式,也可以給流程綁定全局通知。
- 網(wǎng)關(guān),支持排他網(wǎng)關(guān)和并行網(wǎng)關(guān),排他網(wǎng)關(guān)即通過(guò)條件判斷,只要有一個(gè)條件通過(guò),則可進(jìn)入下一個(gè)階段;并行網(wǎng)關(guān),即必須所有的階段都完成處理,才可以進(jìn)行下一個(gè)階段
- 后面還會(huì)有很多的功能擴(kuò)展,包括:加簽,催辦,子流程等等。
等等還有很多功能待研究。
數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
對(duì)于一個(gè)完整的工作流系統(tǒng)來(lái)說(shuō),我們需要有流程、模版、分組、用戶(hù)、任務(wù)等等,并且這些東西都是可以靈活定制的,因?yàn)槿绻荒莒`活定制的話,對(duì)于普通的使用這來(lái)說(shuō)是非常不方便的,所以對(duì)于一個(gè)好的工作流系統(tǒng),是必須要實(shí)現(xiàn)靈活性的。
下面直接來(lái)展示一下,數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)圖。

流程分類(lèi)
type Classify struct {
base.Model
Name string `gorm:"column:name; type: varchar(128)" json:"name" form:"name"` // 分類(lèi)名稱(chēng)
Creator int `gorm:"column:creator; type: int(11)" json:"creator" form:"creator"` // 創(chuàng)建者
}
func (Classify) TableName() string {
return "process_classify"
}
流程
type Info struct {
base.Model
Name string `gorm:"column:name; type:varchar(128)" json:"name" form:"name"` // 流程名稱(chēng)
Structure json.RawMessage `gorm:"column:structure; type:json" json:"structure" form:"structure"` // 流程結(jié)構(gòu)
Classify int `gorm:"column:classify; type:int(11)" json:"classify" form:"classify"` // 分類(lèi)ID
Tpls json.RawMessage `gorm:"column:tpls; type:json" json:"tpls" form:"tpls"` // 模版
Task json.RawMessage `gorm:"column:task; type:json" json:"task" form:"task"` // 任務(wù)ID, array, 可執(zhí)行多個(gè)任務(wù),可以當(dāng)成通知任務(wù),每個(gè)節(jié)點(diǎn)都會(huì)去執(zhí)行
Creator int `gorm:"column:creator; type:int(11)" json:"creator" form:"creator"` // 創(chuàng)建者
}
func (Info) TableName() string {
return "process_info"
}
模版
type Info struct {
base.Model
Name string `gorm:"column:name; type: varchar(128)" json:"name" form:"name" binding:"required"` // 模板名稱(chēng)
FormStructure json.RawMessage `gorm:"column:form_structure; type: json" json:"form_structure" form:"form_structure" binding:"required"` // 表單結(jié)構(gòu)
Creator int `gorm:"column:creator; type: int(11)" json:"creator" form:"creator"` // 創(chuàng)建者
Remarks string `gorm:"column:remarks; type: longtext" json:"remarks" form:"remarks"` // 備注
}
func (Info) TableName() string {
return "tpl_info"
}
工單
type Info struct {
base.Model
Title string `gorm:"column:title; type:varchar(128)" json:"title" form:"title"` // 工單標(biāo)題
Process int `gorm:"column:process; type:int(11)" json:"process" form:"process"` // 流程ID
Classify int `gorm:"column:classify; type:int(11)" json:"classify" form:"classify"` // 分類(lèi)ID
IsEnd int `gorm:"column:is_end; type:int(11); default:0" json:"is_end" form:"is_end"` // 是否結(jié)束, 0 未結(jié)束,1 已結(jié)束
State json.RawMessage `gorm:"column:state; type:json" json:"state" form:"state"` // 狀態(tài)信息
RelatedPerson json.RawMessage `gorm:"column:related_person; type:json" json:"related_person" form:"related_person"` // 工單所有處理人
Creator int `gorm:"column:creator; type:int(11)" json:"creator" form:"creator"` // 創(chuàng)建人
}
func (Info) TableName() string {
return "work_order_info"
}
工單綁定模版
type TplData struct {
base.Model
WorkOrder int `gorm:"column:work_order; type: int(11)" json:"work_order" form:"work_order"` // 工單ID
FormStructure json.RawMessage `gorm:"column:form_structure; type: json" json:"form_structure" form:"form_structure"` // 表單結(jié)構(gòu)
FormData json.RawMessage `gorm:"column:form_data; type: json" json:"form_data" form:"form_data"` // 表單數(shù)據(jù)
}
func (TplData) TableName() string {
return "work_order_tpl_data"
}
工單流轉(zhuǎn)歷史
type CirculationHistory struct {
base.Model
Title string `gorm:"column:title; type: varchar(128)" json:"title" form:"title"` // 工單標(biāo)題
WorkOrder int `gorm:"column:work_order; type: int(11)" json:"work_order" form:"work_order"` // 工單ID
State string `gorm:"column:state; type: varchar(128)" json:"state" form:"state"` // 工單狀態(tài)
Source string `gorm:"column:source; type: varchar(128)" json:"source" form:"source"` // 源節(jié)點(diǎn)ID
Target string `gorm:"column:target; type: varchar(128)" json:"target" form:"target"` // 目標(biāo)節(jié)點(diǎn)ID
Circulation string `gorm:"column:circulation; type: varchar(128)" json:"circulation" form:"circulation"` // 流轉(zhuǎn)ID
Processor string `gorm:"column:processor; type: varchar(45)" json:"processor" form:"processor"` // 處理人
ProcessorId int `gorm:"column:processor_id; type: int(11)" json:"processor_id" form:"processor_id"` // 處理人ID
CostDuration string `gorm:"column:cost_duration; type: varchar(128)" json:"cost_duration" form:"cost_duration"` // 處理時(shí)長(zhǎng)
Remarks string `gorm:"column:remarks; type: longtext" json:"remarks" form:"remarks"` // 備注
}
func (CirculationHistory) TableName() string {
return "work_order_circulation_history"
}
任務(wù)
type Info struct {
base.Model
Name string `gorm:"column:name; type: varchar(256)" json:"name" form:"name"` // 任務(wù)名稱(chēng)
TaskType string `gorm:"column:task_type; type: varchar(45)" json:"task_type" form:"task_type"` // 任務(wù)類(lèi)型
Content string `gorm:"column:content; type: longtext" json:"content" form:"content"` // 任務(wù)內(nèi)容
Creator int `gorm:"column:creator; type: int(11)" json:"creator" form:"creator"` // 創(chuàng)建者
Remarks string `gorm:"column:remarks; type: longtext" json:"remarks" form:"remarks"` // 備注
}
func (Info) TableName() string {
return "task_info"
}
任務(wù)執(zhí)行歷史
type History struct {
base.Model
Task int `gorm:"column:task; type: int(11)" json:"task" form:"task"` // 任務(wù)ID
Name string `gorm:"column:name; type: varchar(256)" json:"name" form:"name"` // 任務(wù)名稱(chēng)
TaskType int `gorm:"column:task_type; type: int(11)" json:"task_type" form:"task_type"` // 任務(wù)類(lèi)型, python, shell
ExecutionTime string `gorm:"column:execution_time; type: varchar(128)" json:"execution_time" form:"execution_time"` // 執(zhí)行時(shí)間
Result string `gorm:"column:result; type: longtext" json:"result" form:"result"` // 任務(wù)返回
}
func (History) TableName() string {
return "task_history"
}
安裝部署
go >= 1.14
vue >= 2.6
npm >= 6.14
二次開(kāi)發(fā)
后端
# 1. 獲取代碼
git https://github.com/lanyulei/ferry.git
# 2. 進(jìn)入工作路徑
cd ./ferry
# 3. 修改配置 ferry/config/settings.dev.yml
vi ferry/config/settings.dev.yml
# 配置信息注意事項(xiàng):
1. 程序的啟動(dòng)參數(shù)
2. 數(shù)據(jù)庫(kù)的相關(guān)信息
3. 日志的路徑
# 4. 初始化數(shù)據(jù)庫(kù)
go run main.go init -c=config/settings.dev.yml
# 5. 啟動(dòng)程序
go run main.go server -c=config/settings.dev.yml
前端
# 1. 獲取代碼
git https://github.com/lanyulei/ferry_web.git
# 2. 進(jìn)入工作路徑
cd ./ferry_web
# 3. 安裝依賴(lài)
npm install
# 4. 啟動(dòng)程序
npm run dev
上線部署
后端
# 1. 進(jìn)入到項(xiàng)目路徑下進(jìn)行交叉編譯(centos)
env GOOS=linux GOARCH=amd64 go build
更多交叉編譯內(nèi)容,請(qǐng)?jiān)L問(wèn) https://www.fdevops.com/2020/03/08/go-locale-configuration
# 2. config目錄上傳到項(xiàng)目根路徑下,并確認(rèn)配置信息是否正確
vi ferry/config/settings.yml
# 配置信息注意事項(xiàng):
1. 程序的啟動(dòng)參數(shù)
2. 數(shù)據(jù)庫(kù)的相關(guān)信息
3. 日志的路徑
# 3. 創(chuàng)建日志路徑及靜態(tài)文件經(jīng)歷
mkdir -p log static/uploadfile
# 4. 初始化數(shù)據(jù)
./ferry init -c=config/settings.yml
# 5. 啟動(dòng)程序,推薦通過(guò)"進(jìn)程管理工具"進(jìn)行啟動(dòng)維護(hù)
nohup ./ferry server -c=config/settings.yml > /dev/null 2>&1 &
前端
# 1. 編譯
npm run build:prod
# 2. 將dist目錄上傳至項(xiàng)目路徑下即可。
mv dist web
# 3. nginx配置,根據(jù)業(yè)務(wù)自行調(diào)整即可
server {
listen 8001; # 監(jiān)聽(tīng)端口
server_name localhost; # 域名可以有多個(gè),用空格隔開(kāi)
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /data/ferry/web;
index index.html index.htm; #目錄內(nèi)的默認(rèn)打開(kāi)文件,如果沒(méi)有匹配到index.html,則搜索index.htm,依次類(lèi)推
}
#ssl配置省略
location /api {
# rewrite ^.+api/?(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8002; #node api server 即需要代理的IP地址
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 登陸
location /login {
proxy_pass http://127.0.0.1:8002; #node api server 即需要代理的IP地址
}
# 刷新token
location /refresh_token {
proxy_pass http://127.0.0.1:8002; #node api server 即需要代理的IP地址
}
# 接口地址
location /swagger {
proxy_pass http://127.0.0.1:8002; #node api server 即需要代理的IP地址
}
# 后端靜態(tài)文件路徑
location /static/uploadfile {
proxy_pass http://127.0.0.1:8002; #node api server 即需要代理的IP地址
}
#error_page 404 /404.html; #對(duì)錯(cuò)誤頁(yè)面404.html 做了定向配置
# redirect server error pages to the static page /50x.html
#將服務(wù)器錯(cuò)誤頁(yè)面重定向到靜態(tài)頁(yè)面/50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}