微服务项目实战笔记

273 阅读10分钟

业务代码

创建User表

package model

import (
	"time"

	"gorm.io/gorm"
)

type BaseModel struct {
	ID        int32     `gorm:"primarykey"`
	CreatedAt time.Time `gorm:"column:add_time"`
	UpdatedAt time.Time `gorm:"column:update_time"`
	DeletedAt gorm.DeletedAt
	IsDelete  bool
}

type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:'手机号'"`
	Password string     `gorm:"type:varchar(100);not null;comment:'密码'"`
	NickName string     `gorm:"type:varchar(20);comment:'昵称'"`
	Birthday *time.Time `gorm:"type:datetime;comment:'生日'"`
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6);comment:'性别'"`
	Role     int        `gorm:"default:1;comment:'用户角色 1 是普通用户,2是管理员'"`
}

迁移User表

package main

import (
	"log"
	"os"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"

	"mxshop_srv/user_srv/model"
)

func main() {
	dsn := "root:1230123@tcp(192.168.0.101:3306)/mxshop_srv?charset=utf8mb4&parseTime=True&loc=Local"
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  false,       // 禁用彩色打印
		},
	)

	// 全局模式
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{

		// 取消表名复数
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},

		// 开启数据库迁移控制台日志
		Logger: newLogger,
	})

	if err != nil {
		panic(err)
	}

	_ = db.AutoMigrate(&model.User{})

}

proto文件

syntax = "proto3";

option go_package=".;proto";

service User {
    rpc GetUserList(PageInfo)returns(UserListResponse);
    rpc GetUserByMobile(MobileRequest)returns(UserInfoResponse);
    rpc GetUserById(IdRequest)returns(UserInfoResponse);
    rpc CreateUser(CreateUserInfo)returns(UserInfoResponse); 
    rpc UpdateUser(UpdateUserInfo)returns(UpdateUserInfoResponse);
    rpc ConfirmPassword(PasswordInfo)returns(PasswordResponse);
}

message PageInfo {
    int32 pn = 1;
    int32 pSize = 2;
}

message PasswordInfo{
    string password = 1;
    string hashPassword = 2;
}

message PasswordResponse{
    bool success = 1;
}

message IdRequest{
    uint32 id = 1;
}
message UpdateUserInfo {
    uint32 id = 1;
    string nickName = 2;
    string gender = 3;
    string birthday = 4;
}

message UpdateUserInfoResponse{
    uint32 id = 1;
}

message MobileRequest{
    string mobile =1;
}

message CreateUserInfo{
    string nickname = 1;
    string password = 2;
    string mobile = 3;
}

message UserInfoResponse{
    uint32 id = 1;
    string password = 2;
    string mobile = 3;
    string nickName = 4;
    uint32 birthday = 5;
    string gender = 6;
    int32  role = 7; 
}

message UserListResponse{
    int32 total = 1;
    repeated UserInfoResponse data = 2;
}

protoc -I . user.proto --go_out=plugins=grpc:.

global

package global

import (
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "gorm.io/gorm/schema"
   "log"
   "os"
   "time"
)

var (
   DB *gorm.DB
)

func init() {
   dsn := "root:1230123@tcp(192.168.0.101:3306)/mxshop_srv?charset=utf8mb4&parseTime=True&loc=Local"
   newLogger := logger.New(
      log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
      logger.Config{
         SlowThreshold:             time.Second, // 慢 SQL 阈值
         LogLevel:                  logger.Info, // 日志级别
         IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
         Colorful:                  false,       // 禁用彩色打印
      },
   )

   // 全局模式
   var err error
   DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{

      // 取消表名复数
      NamingStrategy: schema.NamingStrategy{
         SingularTable: true,
      },

      // 开启数据库迁移控制台日志
      Logger: newLogger,
   })

   if err != nil {
      panic(err)
   }
}

user业务代码

package handler

import (
   "context"
   "gorm.io/gorm"
   "mxshop_srv/user_srv/global"
   "mxshop_srv/user_srv/model"
   "mxshop_srv/user_srv/proto"
)

type UserService struct {
}

func (u *UserService) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {
   var users []model.User
   data := global.DB.Find(&users)
   if data.Error != nil {
      return nil, data.Error
   }
   rsp := &proto.UserListResponse{}
   rsp.Total = int32(data.RowsAffected)
   global.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)

   for _, user := range users {
      userInfoRes := modelToResponse(user)
      rsp.Data = append(rsp.Data, &userInfoRes)
   }
   return rsp, nil
}

// 分页
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
   return func(db *gorm.DB) *gorm.DB {

      if page == 0 {
         page = 1
      }

      switch {
      case pageSize > 100:
         pageSize = 100
      case pageSize <= 0:
         pageSize = 10
      }

      offset := (page - 1) * pageSize
      return db.Offset(offset).Limit(pageSize)
   }
}

// 将每个User信息转化成grpc的 user信息
func modelToResponse(user model.User) proto.UserInfoResponse {
   userInfoRsp := proto.UserInfoResponse{
      Id:       uint32(user.ID),
      Mobile:   user.Mobile,
      NickName: user.NickName,
      Gender:   user.Gender,
      Role:     int32(user.Role),
   }

   // birthday在grpc中没有设置默认值,数据库有可能取出的数据是空值,这样会将一个nil赋值给grpc的birthday
   // 在grpc中将一个字段设置为nil可能会抛出异常,所以此处将没有默认值的birthday进行特殊处理
   if user.Birthday != nil {
      userInfoRsp.Birthday = uint32(user.Birthday.Unix())
   }

   return userInfoRsp
}

根据用户ID和或者手机号查找用户


// 根据手机号查找用户

func (u *UserService) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {
   var user model.User
   result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
   if result.RowsAffected == 0 {
      return nil, status.Error(codes.NotFound, "用户不存在")
   }
   if result.Error != nil {
      return nil, result.Error
   }
   rsp := modelToResponse(user)
   return &rsp, nil
}

//根据ID查找用户

func (u *UserService) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {
   var user model.User
   result := global.DB.First(&user, req.Id)
   if result.RowsAffected == 0 {
      return nil, status.Error(codes.NotFound, "用户不存在")
   }
   if result.Error != nil {
      return nil, result.Error
   }
   rsp := modelToResponse(user)
   return &rsp, nil
}

创建用户

// 新建用户

func (u *UserService) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
   var user model.User
   data := global.DB.First(&user, req.Mobile)
   if data.RowsAffected == 1 {
      return nil, status.Error(codes.AlreadyExists, "用户已存在")
   }

   user.Mobile = req.Mobile
   user.NickName = req.Nickname

   passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
   if err != nil {
      return nil, status.Error(codes.Internal, err.Error())
   }
   user.Password = string(passwordHash)
   rsp := modelToResponse(user)
   return &rsp, nil
}

启动User服务

package main

import (
   "flag"
   "fmt"
   "google.golang.org/grpc"
   "mxshop_srv/user_srv/handler"
   "mxshop_srv/user_srv/proto"
   "net"
)

func main() {
   IP := flag.String("ip", "0.0.0.0", "ip地址")
   Port := flag.Int("port", 8080, "端口号")
   flag.Parse()
   fmt.Println("ip:", *IP)
   fmt.Println("Port", *Port)

   server := grpc.NewServer()
   proto.RegisterUserServer(server, &handler.UserService{})
   listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
   if err != nil {
      panic("User服务监听失败:" + err.Error())
   }

   err = server.Serve(listener)
   if err != nil {
      panic("User服务启动失败:" + err.Error())
   }
}

启动User客户端

package api

import (
   "context"
   "github.com/gin-gonic/gin"
   "go.uber.org/zap"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials/insecure"
   "mxshop_api/proto"
   "mxshop_api/response"
   "mxshop_api/tools"
   "net/http"
   "time"
)

func GetUserList(ctx *gin.Context) {
   zap.L().Debug("用户列表")
   clientConn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
   if err != nil {
      zap.S().Error("连接User客户端失败", err.Error())
   }
   userClient := proto.NewUserClient(clientConn)

   rsp, err := userClient.GetUserList(context.Background(), &proto.PageInfo{
      Pn:    0,
      PSize: 0,
   })

   if err != nil {
      zap.S().Error("查询用户列表失败")
      tools.GrpcErrorToHttp(err, ctx)
      return
   }

   result := make([]interface{}, 0)

   for _, value := range rsp.Data {
      user := response.UserResponse{
         Id:       value.Id,
         Mobile:   value.Mobile,
         NickName: value.NickName,
         Birthday: tools.JsonTime(time.Unix(int64(value.Birthday), 0)),
         Gender:   value.Gender,
         Role:     value.Role,
      }

      result = append(result, user)
   }

   ctx.JSON(http.StatusOK, result)
}

模块

分页

func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
   return func(db *gorm.DB) *gorm.DB {

      if page == 0 {
         page = 1
      }

      switch {
      case pageSize > 100:
         pageSize = 100
      case pageSize <= 0:
         pageSize = 10
      }

      offset := (page - 1) * pageSize
      return db.Offset(offset).Limit(pageSize)
   }
}

grpc状态码

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"


if result.RowsAffected == 0 {
   return nil, status.Error(codes.NotFound, "用户不存在")
}

获取用户输入参数启动服务

IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 8080, "端口号")
flag.Parse()
fmt.Println("ip:", *IP)
fmt.Println("Port", *Port)

zap日志库

go get -u go.uber.org/zap

  • Zap提供了两种类型的日志记录器—Sugared Logger 和 Logger 。

  • 在性能很好但不是很关键的上下文中,使用 SugaredLogger 。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。

  • 在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。

Logger

  • 可以通过调用 zap.NewProduction() / zap.NewDevelopment() 来创建又给Logger

image.png

  • 两种方式都可以,区别就是打印出来的格式不一样

NewDevelopment 是以 空格分开 的形式展示
NewProduction 使用的是 json格式 ,键值对的形式 展示出来

  • 注意:默认情况下日志都会打印到应用程序的console界面。

SugaredLogger

  • 这个就直接使用logger.Sugar()即可,啥使用都管用
  • 他们基本上相同,唯一的不同的就是SugaredLogger可以用printf格式记录语句

例如:

sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)

自定义Logger

那么不想打印在终端怎么办呢,那我们只有自定义配置了

  • 那我们只能使用 zap.New(…) 方法来手动传递所有配置

image.png

  • 我们可以看到需要一个zapcore.Core的参数,所以我们再进去看看

image.png

  • 我们可以看到这个是一个接口,里面有个newCore的方法可以创建一个core。
  • 源码里面可以明显的看到哈,只要我们给三个参数,就可以得到一个logger了,那么这三个参数分别表示上面呢?

Encoder : 编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()。

// core 三个参数之 编码 func getEncoder() zapcore.Encoder { return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) }

WriteSyncer : 指定日志将写到哪里去。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数来进行一个转换。

// core 三个参数之  路径
func getLogWriter() zapcore.WriteSyncer {
	file,_ := os.Create("E:/test.log")
	return zapcore.AddSync(file)
}

WriteSyncer : 指定日志将写到哪里去。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数来进行一个转换。

// core 三个参数之  路径
func getLogWriter() zapcore.WriteSyncer {
	file,_ := os.Create("E:/test.log")
	return zapcore.AddSync(file)
}

LevelEnabler: 这个就是我们所需要打印的日志等级设置了,通过它来动态的保存日志,比如上线后我们error以下的日志就不打印了!

  • 我们通过 *zapcore.**Level 来设置,里面都是封装好的日志等级
  • 可以看下zapcore的源码哦

image.png

  • 非常的ok!然后我们就可以创建一个logger了
var logger *zap.Logger
var sugarLogger *zap.SugaredLogger

func InitLogger() {
	encoder := getEncoder()
	writerSyncer := getLogWriter()
	core := zapcore.NewCore(encoder,writerSyncer,zapcore.DebugLevel)
	logger = zap.New(core)
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func getLogWriter() zapcore.WriteSyncer {
	file,_ := os.Create("E:/test.log")
	return zapcore.AddSync(file)
}

  • 我们来跑一个例子:

image.png

  • 好用!!!只有存在文件里面才方便我们往后的查看呢!!!

将JSON Encoder更改为普通的Log Encoder

  • 我们采用编码格式的时候,采用的json格式满,可以有的人习惯看空格呀,怎么办,那就换一个呗,
  • 人家zap也是提供了的
// core 三个参数之  编码
func getEncoder() zapcore.Encoder {
	return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}

image.png

  • 非常的ok哈!!!
  • 但是这个时间,,,还是有点不敬人意哈,所以我们好需要调整以下

编码配置优化

  • 那么我们就需要对 encoderConfig 进行一个自定义配置了
func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	return zapcore.NewConsoleEncoder(encoderConfig)
}

  • 修改时间编码器
  • 在日志文件中使用大写字母记录日志级别

image.png

  • 是不是感觉又好很多了!
  • 那么我们怎么来获取 调用的文件,函数名称,行号呢?
  • 也很简单哈,我们再new一个zap 的时候加个 zap.AddCaller() 即可!
logger := zap.New(core, zap.AddCaller())

  • 来看看效果吧!

image.png

  • 样子,我们基本上就可以进行一个很好的使用体验了哈!!!
  • 你以为没了? 不,还有最重要的一点,文件的切割,但是很可惜,zap没有这玩意,所以我们只有采用第三方库来实现拉!

使用Lumberjack进行日志切割归档

注意:Zap本身不支持切割归档日志文件

  • 为了实现切割功能呢,我们采用第三方库 Lumberjack

Lumberjack的安装

  • 老规矩哈,要是有依赖漏了就 go mod tidy 一下哈!!!
go get -u github.com/natefinch/lumberjack

zap logger中加入Lumberjack

  • 要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码。我们将按照下面的代码修改getLogWriter()函数:
func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   false,
	}
	return zapcore.AddSync(lumberJackLogger)
}

  • 分别表示上面意思呢? Lumberjack Logger采用以下属性作为输入:
属性含义
Filename日志文件的位置,也就是路径
MaxSize在进行切割之前,日志文件的最大大小(以MB为单位)
MaxBackups保留旧文件的最大个数
MaxAges保留旧文件的最大天数
Compress是否压缩/归档旧文件

到这里我们的代码就完成了!!!
来看看总代码:

package main

import (
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net/http"
)

var logger *zap.Logger
var sugarLogger *zap.SugaredLogger



func InitLogger() {
	encoder := getEncoder()
	writerSyncer := getLogWriter()
	core := zapcore.NewCore(encoder,writerSyncer,zapcore.DebugLevel)
	logger = zap.New(core,zap.AddCaller())
	sugarLogger = logger.Sugar()
}

// core 三个参数之  编码
func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	return zapcore.NewConsoleEncoder(encoderConfig)
}

// core 三大核心之  路径
func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "E:/test.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   false,
	}
	return zapcore.AddSync(lumberJackLogger)
}

func main() {
	InitLogger()
	defer logger.Sync()
	simpleHttpGet("www.baidu.com")
	simpleHttpGet("http://www.baidu.com")
}
func simpleHttpGet(url string) {
	resp, err := http.Get(url)
	if err != nil {
		logger.Error(
			"Error fetching url..",
			zap.String("url", url),
			zap.Error(err))
	} else {
		logger.Info("Success..",
			zap.String("statusCode", resp.Status),
			zap.String("url", url))
		resp.Body.Close()
	}
}

这里我们设置的是 日志文件每 5MB 会切割并且在当前目录下最多保存 5 个备份,并且会将旧文档保存30天。 到这里,我们就完成了zap日志程序集成到项目中了,还是很方便简单的哈! 至于测试数据,大家可以跑几个 goroutine来试试,把MaxSize调小一点,即可看到切分的效果哦!!

grpc状态码转换成http状态码


func GrpcErrorToHttp(err error, ctx *gin.Context) {
   if err != nil {
      if e, ok := status.FromError(err); ok {
         switch e.Code() {
         case codes.NotFound:
            ctx.JSON(http.StatusNotFound, gin.H{
               "msg": e.Message(),
            })
         case codes.Internal:
            ctx.JSON(http.StatusInternalServerError, gin.H{
               "msg": "内部错误",
            })
         case codes.InvalidArgument:
            ctx.JSON(http.StatusBadRequest, gin.H{
               "msg": "参数错误",
            })
         default:
            ctx.JSON(http.StatusInternalServerError, gin.H{
               "msg": "未知错误",
            })
         }
      }
      return
   }
}


if err != nil {
   zap.S().Error("查询用户列表失败")
   tools.GrpcErrorToHttp(err, ctx)
   return
}

time.Time类型处理


type JsonTime time.Time

func (j JsonTime) MarshalJSON() ([]byte, error) {
   var stmp = fmt.Sprintf(""%s"", time.Time(j).Format("2006-01-02"))
   return []byte(stmp), nil
}


for _, value := range rsp.Data {
   user := response.UserResponse{
      Id:       value.Id,
      Mobile:   value.Mobile,
      NickName: value.NickName,
      Birthday: tools.JsonTime(time.Unix(int64(value.Birthday), 0)),  // 标注
      Gender:   value.Gender,
      Role:     value.Role,
   }

   result = append(result, user)
}


type UserResponse struct {
   Id       uint32         `json:"id"`
   Mobile   string         `json:"mobile"`
   NickName string         `json:"nickname"`
   Birthday tools.JsonTime `json:"birthday"`   // 标注
   Gender   string         `json:"gender"`
   Role     int32          `json:"role"`
}

viper配置管理


name:'user-web'
user_srv:
  host:'127.0.0.1'
  port: 8080

package initialize

import (
   "fmt"
   "github.com/fsnotify/fsnotify"
   "github.com/spf13/viper"
   "go.uber.org/zap"
   "mxshop_api/global"
)

func GetEnvInfo(env string) bool {
   viper.AutomaticEnv()
   return viper.GetBool(env)
}

func InitConfig() {
   v := viper.New()
   debug := GetEnvInfo("TEST_CONFIG")
   v.AddConfigPath("../")
   configFilePrefix := "config"
   configFileName := fmt.Sprintf("%s-pro.yaml", configFilePrefix)
   if debug {
      configFileName = fmt.Sprintf("mxshop_api/%s-dev.yaml", configFilePrefix)
   }
   v.SetConfigFile(configFileName)

   if err := v.ReadInConfig(); err != nil {
      zap.S().Panic(err.Error())
   }

   if err := v.Unmarshal(&global.ServerConfig); err != nil {
      zap.S().Panic(err.Error())
   }

   fmt.Println(global.ServerConfig)
   fmt.Println("%V", v.Get("name"))

   v.WatchConfig()

   v.OnConfigChange(func(e fsnotify.Event) {
      fmt.Println("config file change: ", e.Name)
      _ = v.ReadInConfig()
      _ = v.Unmarshal(&global.ServerConfig)
      fmt.Println(global.ServerConfig)
   })
}

gin多语言验证错误提示

package initialize

import (
   "fmt"
   "github.com/gin-gonic/gin/binding"
   "github.com/go-playground/locales/en"
   "github.com/go-playground/locales/zh"
   ut "github.com/go-playground/universal-translator"
   "mxshop_api/global"

   "github.com/go-playground/validator/v10"
   en_translation "github.com/go-playground/validator/v10/translations/en"
   zh_translation "github.com/go-playground/validator/v10/translations/zh"
   "go.uber.org/zap"
   "reflect"
   "strings"
)

func InitTrans(locale string) (err error) {
   if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
      //注册一个获取json的tag的方法
      v.RegisterTagNameFunc(func(fld reflect.StructField) string {
         name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
         if name == "-" {
            return ""
         }
         return name
      })

      zhT := zh.New() //中文翻译器
      enT := en.New() //英文翻译器
      //第一个参数为备用语言环境,剩下了两个为必须支持的语言环境
      uni := ut.New(enT, zhT, enT)
      global.Trans, ok = uni.GetTranslator(locale)
      if !ok {
         return fmt.Errorf("uni.GetTranslator(%s)", locale)
      }
      switch locale {
      case "en":
         err = en_translation.RegisterDefaultTranslations(v, global.Trans)
      case "zh":
         err = zh_translation.RegisterDefaultTranslations(v, global.Trans)
      default:
         err = en_translation.RegisterDefaultTranslations(v, global.Trans)
      }
      if err != nil {
         zap.S().Panic(err.Error())
      }
   }
   return
}

func removeTopStruct(fields map[string]string) map[string]string {
   rsp := map[string]string{}
   for field, err := range fields {
      rsp[field[strings.Index(field, ".")+1:]] = err
   }
   return rsp
}

func ValidatorError(ctx *gin.Context, err error) {
   errs, ok := err.(validator.ValidationErrors)
   if !ok {
      ctx.JSON(http.StatusOK, gin.H{
         "message": err.Error(),
      })
   }
   ctx.JSON(http.StatusBadRequest, gin.H{
      "error": removeTopStruct(errs.Translate(global.Trans)),
   })
   return

}
// 加载翻译器
if err := initialize.InitTrans("zh"); err != nil {
   zap.S().Panic(err.Error())
}

gin自定义验证器

package initialize

import (
   "github.com/gin-gonic/gin/binding"
   ut "github.com/go-playground/universal-translator"
   "github.com/go-playground/validator/v10"
   "go.uber.org/zap"
   "mxshop_api/global"
   "regexp"
)

func ValidateMobile(fl validator.FieldLevel) bool {
   mobile := fl.Field().String()
   //使用正则表达式判断是否合法
   ok, _ := regexp.MatchString(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`, mobile)
   if !ok {
      return false
   }
   return true
}

//注册验证器
func InitValidator() {
   //注册验证器
   if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
      err := v.RegisterValidation("mobile", ValidateMobile)
      err = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
         return ut.Add("mobile", "{0} 非法的手机号码!", true) // see universal-translator for details
      }, func(ut ut.Translator, fe validator.FieldError) string {
         t, _ := ut.T("mobile", fe.Field())
         return t
      })
      if err != nil {
         zap.S().Panic(err.Error())
      }
   }

}

JWT

包:

github.com/dgrijalva/jwt-go

声明结构体

type MyStandardClaims struct {
  Username string `json:"username"`
  jwt.StandardClaims
}


设置密钥

myKey := []byte(“qwertyuiop”)

创建结构体

ms := MyStandardClaims{
  Username: "hzyy",
  StandardClaims: jwt.StandardClaims{
     ExpiresAt: time.Now().Unix() + 10,
     Issuer:    "233",
  },
}

StandardClaims: jwt.StandardClaims{} 可以添加其他属性

创建token

token := jwt.NewWithClaims(jwt.SigningMethodHS256, &ms)

jwt.SigningMethodHS256 加密方式

加密token

signedString, err := token.SignedString(myKey)

signedString 就是需要的jwt字符串了

解析token


claims, err := jwt.ParseWithClaims(signedString, &MyStandardClaims{}, func(token *jwt.Token) (interface{}, error) {
  return myKey, nil
})
if err != nil {
  fmt.Println(err)
}
fmt.Println(claims.Claims.(*MyStandardClaims).Username)


singnedString 为jwt字符串

&MyStandardClaims{} 为解析后的结构体

func(token *jwt.Token) 该函数的返回值为密钥

claims.Claims.(*MyStandardClaims).Username 断言为对应结构体,并获取对应属性的值

实例


package tools
 
import (
    "github.com/dgrijalva/jwt-go"
    "time"
)
 
const SECRET = "taoshihan"
 
type UserClaims struct {
    Id         uint      `json:"id"`
    Pid        uint      `json:"pid"`
    Username   string    `json:"username"`
    RoleId     uint      `json:"role_id"`
    CreateTime time.Time `json:"create_time"`
    jwt.StandardClaims
}
 
 
func MakeCliamsToken(obj UserClaims) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, obj)
    tokenString, err := token.SignedString([]byte(SECRET))
    return tokenString, err
}

func ParseCliamsToken(token string) (*UserClaims, error) {
    tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(SECRET), nil
    })
    if tokenClaims != nil {
        if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
            return claims, nil
        }
    }
    return nil, err
}

跨域


func Cors() gin.HandlerFunc {
	return func(context *gin.Context) {
		method := context.Request.Method
		context.Header("Access-Control-Allow-Origin", "*")
		context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,x-token")
		context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
		context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
		context.Header("Access-Control-Allow-Credentials", "true")
		if method == "OPTIONS" {
			context.AbortWithStatus(http.StatusNoContent)
		}
		context.Next()
	}
}

验证码

go get -u github.com/mojocn/base64Captcha

创建图像验证码

import "github.com/mojocn/base64Captcha"
func demoCodeCaptchaCreate() {
  //config struct for digits
  //数字验证码配置
  var configD = base64Captcha.ConfigDigit{
    Height:   80,
    Width:   240,
    MaxSkew:  0.7,
    DotCount:  80,
    CaptchaLen: 5,
  }
  //config struct for audio
  //声音验证码配置
  var configA = base64Captcha.ConfigAudio{
    CaptchaLen: 6,
    Language:  "zh",
  }
  //config struct for Character
  //字符,公式,验证码配置
  var configC = base64Captcha.ConfigCharacter{
    Height:       60,
    Width:       240,
    //const CaptchaModeNumber:数字,CaptchaModeAlphabet:字母,CaptchaModeArithmetic:算术,CaptchaModeNumberAlphabet:数字字母混合.
    Mode:        base64Captcha.CaptchaModeNumber,
    ComplexOfNoiseText: base64Captcha.CaptchaComplexLower,
    ComplexOfNoiseDot: base64Captcha.CaptchaComplexLower,
    IsShowHollowLine:  false,
    IsShowNoiseDot:   false,
    IsShowNoiseText:  false,
    IsShowSlimeLine:  false,
    IsShowSineLine:   false,
    CaptchaLen:     6,
  }
  //create a audio captcha.
  idKeyA, capA := base64Captcha.GenerateCaptcha("", configA)
  //以base64编码
  base64stringA := base64Captcha.CaptchaWriteToBase64Encoding(capA)
  //create a characters captcha.
  idKeyC, capC := base64Captcha.GenerateCaptcha("", configC)
  //以base64编码
  base64stringC := base64Captcha.CaptchaWriteToBase64Encoding(capC)
  //create a digits captcha.
  idKeyD, capD := base64Captcha.GenerateCaptcha("", configD)
  //以base64编码
  base64stringD := base64Captcha.CaptchaWriteToBase64Encoding(capD)
   
  fmt.Println(idKeyA, base64stringA, "\n")
  fmt.Println(idKeyC, base64stringC, "\n")
  fmt.Println(idKeyD, base64stringD, "\n")
}

校验验证码

import "github.com/mojocn/base64Captcha"
func verfiyCaptcha(idkey,verifyValue string){
  verifyResult := base64Captcha.VerifyCaptcha(idkey, verifyValue)
  if verifyResult {
    //success
  } else {
    //fail
  }
}

获取可用端口

// 获取可用端口
func GetAvailablePort() (int, error) {
	address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
	if err != nil {
		return 0, err
	}

	listener, err := net.ListenTCP("tcp", address)
	if err != nil {
		return 0, err
	}

	defer listener.Close()
	return listener.Addr().(*net.TCPAddr).Port, nil

}

// 判断端口是否可以(未被占用)
func IsPortAvailable(port int) bool {
	address := fmt.Sprintf("%s:%d", "0.0.0.0", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		log.Infof("port %s is taken: %s", address, err)
		return false
	}

	defer listener.Close()
	return true
}