NAV
shell go

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

Home

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

Save:

Logs

Explore:

Select labels:

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:

More: robfig/cron