Installation
Backend
Install Golang
You can download and install it by this link(suggested version >= 1.14).
Clone Gin Web
Clone Gin Web:
git clone https://github.com/piupuer/gin-web
You can clone the backend repo by git clone.
Enable GOPROXY
For current session:
export GOPROXY=https://goproxy.cn,direct
For current session
For system variable(Unix example):
# append one line
echo ~/.bashrc >> 'export GOPROXY=https://goproxy.cn,direct'
# for current session
source .bashrc
# reboot for every session
reboot
For system variable
Change default config
Change default config:
cd gin-web/conf
# change config file
vim config.dev.yml
Select a yml based on your run mode:
development: config.dev.yml
staging: config.stage.yml
production: config.prod.yml
Run in Terminal
Run in Terminal:
cd gin-web
# project use go modules, run will auto download libraries
go run main.go
project use go modules, go run
will auto download libraries.
Run in Idea
1. Set GOROOT
2. Enable GOPROXY
3. Run
Success example:
Swagger
Comments
package v1
// FindLeave
// @Security Bearer
// @Accept json
// @Produce json
// @Success 201 {object} resp.Resp "success"
// @Tags Leave
// @Description FindLeave
// @Param params query request.Leave true "params"
// @Router /leave/list [GET]
func FindLeave(c *gin.Context) {
// ...
}
You can add swagger comments in /api/v1/xxx function.
Update
# install swag
go get -u github.com/swaggo/swag/cmd/swag
cd gin-web
# update comments to swagger docs
swag init -o docs/swagger --pd --parseInternal
After run swag init
gin-web/docs/swagger/** will be changed.
View
You can view api list in 127.0.0.1/swagger/index.html.
More: swag
Frontend
Install Node
You can download and install it by this link(suggested version >= 14).
Clone Gin Web Vue
Clone Gin Web Vue:
git clone https://github.com/piupuer/gin-web-vue
You can clone the frontend repo by git clone.
Install libraries
Install libraries:
# change registry if you need
# npm config set registry https://registry.npm.taobao.org
npm install
You should run command to download libraries before run.
Change default config
Change default config:
vim gin-web-vue/.env.development
# backend api:
VUE_APP_BASE_API = 'http://127.0.0.1:10000/api/v1'
# backend websocket:
VUE_APP_BASE_WS = 'ws://127.0.0.1:10000/api/v1'
If you not use nginx, you should change the api url prefix.
Run in Terminal
Run in Terminal:
npm run serve
Success example:
Iconfont
Search favorite icons
Add to project
Generate css
Replace css
Copy generated CSS to gin-web-vue/public/index.html:
Nginx
Install
Install(Ubuntu example):
apt-get install nginx
nginx -v
# my version
# nginx version: nginx/1.14.0 (Ubuntu)
You can also download and install it by this link.
Run
Start service:
systemctl start nginx
Find nginx.conf:
nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
Add virtual host in
http
:
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
Add virtual host
Add gin-web.conf:
vim /etc/nginx/conf.d/gin-web.conf
# write this:
# api
upstream gin-web {
server 127.0.0.1:10000;
keepalive 64;
}
# ui
upstream gin-web-vue {
server 127.0.0.1:10001;
keepalive 64;
}
# pprof
upstream gin-pprof {
server 127.0.0.1:10005;
keepalive 64;
}
server {
listen 80;
# enable https
#listen 443 ssl;
# cert
#ssl_certificate cert/domain.com.pem;
#ssl_certificate_key cert/domain.com.key;
# http redirect https
#if ( $ssl_protocol = "") {
# rewrite ^ https://$host$request_uri? permanent;
#}
# your domain
# server_name domain.com;
server_name 127.0.0.1;
location / {
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Connection "";
proxy_http_version 1.1;
# end with / (useful for children path)
proxy_pass http://gin-web-vue/;
}
location ^~ /api {
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_http_version 1.1;
proxy_pass http://gin-web/api;
}
location ^~ /swagger {
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_http_version 1.1;
proxy_pass http://gin-web/swagger;
}
location ^~ /debug/pprof/ {
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_pass http://gin-pprof/debug/pprof/;
}
}
Restart
Restart service:
systemctl restart nginx
Visit
Visit it in your browser 127.0.0.1, port is hidden.
Finite state machine
Do you often encounter finite state machines or approval workflow in your development process?
Gin Web provides a simple FSM implementation example: Leave request.
Config machine
You can add a new machine easily:
Show results
Approval refused
Approval approved or ended
Approval cancelled
How to use
You can customize your workflow is like leave request similar to the following functions:
Transition
Leave Transition:
package service
func (my MysqlService) LeaveTransition(logs ...resp.FsmApprovalLog) (err error) {
m := make(map[uint][]string)
for _, log := range logs {
if log.Category == global.FsmCategoryLeave {
if log.Resubmit == constant.One {
arr := make([]string, 0)
if item, ok := m[models.LevelStatusWaitingConfirm]; ok {
arr = item
}
m[models.LevelStatusRefused] = append(arr, log.Uuid)
} else if log.Cancel == constant.One {
arr := make([]string, 0)
if item, ok := m[models.LevelStatusCancelled]; ok {
arr = item
}
m[models.LevelStatusCancelled] = append(arr, log.Uuid)
} else if log.Confirm == constant.One {
arr := make([]string, 0)
if item, ok := m[models.LevelStatusWaitingConfirm]; ok {
arr = item
}
m[models.LevelStatusWaitingConfirm] = append(arr, log.Uuid)
} else if log.End == constant.One {
arr := make([]string, 0)
if item, ok := m[models.LevelStatusApproved]; ok {
arr = item
}
m[models.LevelStatusApproved] = append(arr, log.Uuid)
} else {
arr := make([]string, 0)
if item, ok := m[models.LevelStatusApproving]; ok {
arr = item
}
m[models.LevelStatusApproving] = append(arr, log.Uuid)
}
}
}
for status, uuids := range m {
err = my.Q.Tx.
Model(&models.Leave{}).
Where("fsm_uuid IN (?)", uuids).
Update("status", status).Error
if err != nil {
return errors.WithStack(err)
}
}
return nil
}
Get Detail
Get Leave Fsm Detail:
package service
func (my MysqlService) GetLeaveFsmDetail(detail req.FsmSubmitterDetail) []resp.FsmSubmitterDetail {
arr := make([]resp.FsmSubmitterDetail, 0)
switch uint(detail.Category) {
case global.FsmCategoryLeave:
var leave models.Leave
my.Q.Tx.
Model(&models.Leave{}).
Where("fsm_uuid = ?", detail.Uuid).
First(&leave)
if leave.Id > 0 {
arr = append(arr, resp.FsmSubmitterDetail{
Name: "leave desc",
Key: "desc",
Val: leave.Desc,
})
if !leave.StartTime.IsZero() {
arr = append(arr, resp.FsmSubmitterDetail{
Name: "leave start time",
Key: "startTime",
Val: leave.StartTime.String(),
})
}
if !leave.EndTime.IsZero() {
arr = append(arr, resp.FsmSubmitterDetail{
Name: "leave end time",
Key: "endTime",
Val: leave.EndTime.String(),
})
}
}
}
return arr
}
Update Detail
Update Leave Fsm Detail:
package service
func (my MysqlService) UpdateLeaveFsmDetail(detail req.UpdateFsmSubmitterDetail) (err error) {
switch uint(detail.Category) {
case global.FsmCategoryLeave:
detail.Parse()
m := make(map[string]interface{})
for i, key := range detail.Keys {
m[utils.SnakeCase(key)] = detail.Vals[i]
}
var leave models.Leave
q := my.Q.Tx.
Model(&models.Leave{}).
Where("fsm_uuid = ?", detail.Uuid)
q.First(&leave)
if leave.Id > 0 {
err = q.Updates(&m).Error
return errors.WithStack(err)
}
}
return nil
}
Model
Leave Model:
package models
const (
LevelStatusWaiting uint = iota // waiting approval
LevelStatusApproved // approved
LevelStatusRefused // approval rejection
LevelStatusCancelled // approval cancelled
LevelStatusApproving // pending approval but not end
LevelStatusWaitingConfirm // approval waiting confirm
)
type Leave struct {
ms.M
UserId uint `gorm:"comment:'user id(SysUser.Id)'" json:"userId"`
User SysUser `gorm:"foreignKey:UserId" json:"user"`
FsmUuid string `gorm:"size:100;comment:'finite state machine uuid'" json:"fsmUuid"`
Status uint `gorm:"default:0;comment:'status(0:submitted 1:approved 2:refused 3:cancel 4:approving 5:waiting confirm)'" json:"status"`
ApprovalOpinion string `gorm:"comment:'approval opinion or remark'" json:"approvalOpinion"`
Desc string `gorm:"comment:'submitter description'" json:"desc"`
StartTime carbon.ToDateTimeString `gorm:"comment:'start time'" json:"startTime"`
EndTime carbon.ToDateTimeString `gorm:"comment:'end time'" json:"endTime"`
}
Common Api
Fsm Common Api:
package router
func (rt Router) Fsm() {
router1 := rt.Casbin("/fsm")
router2 := rt.CasbinAndIdempotence("/fsm")
router1.GET("/approving/list", v1.FindFsmApprovingLog(rt.ops.v1Ops...))
router1.GET("/log/track", v1.FindFsmLogTrack(rt.ops.v1Ops...))
router1.GET("/submitter/detail", v1.GetFsmSubmitterDetail(rt.ops.v1Ops...))
router1.PATCH("/submitter/detail", v1.UpdateFsmSubmitterDetail(rt.ops.v1Ops...))
router1.PATCH("/approve", v1.FsmApproveLog(rt.ops.v1Ops...))
router1.PATCH("/cancel", v1.FsmCancelLogByUuids(rt.ops.v1Ops...))
}
Gin Web Docker
git clone https://github.com/piupuer/gin-web-docker
cd gin-web-docker
chmod +x control.sh
Redis sentinel
Change Master/Slave IP
Single machine multiple instance example
export REDIS_MASTER_IP=10.13.2.252
export LOCAL_IP=10.13.2.252
# start port
# export REDIS_PORT=6379
Initialization
./control.sh sentinel 3
Waiting:
Status
View container
docker ps | grep redis
Master
docker exec -it redis-master redis-cli -a 123456
keys *
# show REPLICATION info
info REPLICATION
# success example:
# connected_slaves:2
Slave
docker exec -it redis-slave2 redis-cli -p 6381 -a 123456
keys *
# show REPLICATION info
info REPLICATION
Loki
Initialization
./control.sh loki
Waiting:
Grafana
Login
Default username/password: admin/admin
Skip
Add source
Select Loki
Gateway
URL: loki-gateway:3100
Logs
Distribute Cron Task
Redis
I assume you have redis installed, or click this to initialization.
Go Helper
go get -u github.com/piupuer/go-helper
Download Go Helper by go get
.
Example
package main
import (
"context"
"fmt"
"github.com/piupuer/go-helper/pkg/constant"
"github.com/piupuer/go-helper/pkg/job"
)
func main() {
// parse redis URI
client, _ := job.ParseRedisURI("redis://127.0.0.1:6379/0")
j, err := job.New(
job.Config{
RedisClient: client,
},
job.WithAutoRequestId(true),
)
if err != nil {
panic(err)
}
// add task and start
j.AddTask(job.GoodTask{
Name: "task",
Expr: "@every 5s",
Func: task,
}).Start()
ch := make(chan int)
<-ch
}
func task(ctx context.Context) error {
requestId := ctx.Value(constant.MiddlewareRequestIdCtxKey)
fmt.Println(requestId, "task running...")
return nil
}
Run result:
New
AutoRequestId
If set it true, request id is automatically generated each time the task is executed.
AddTask
Name
Task name must be unique.
Expr
Task expression is support:
- 0 30 * * * * (Every hour on the half hour)
- @hourly (Every hour)
- @every 1h30m (Every hour thirty)
More: robfig/cron