> 文章列表 > Golang微服务一把嗦 用户微服务集成主流最新go技术栈

Golang微服务一把嗦 用户微服务集成主流最新go技术栈

Golang微服务一把嗦 用户微服务集成主流最新go技术栈

声明:此文章为博主个人学习记录,仅供学习和交流,如有侵权请联系博主。

前言

前段时间,因为本地k8s环境一直出问题,线上云环境也用不起,(后面搞定了再慢慢学习)所以就暂时搁置了k8s学习,然后就想着封装一下go微服务框架,看了一些大佬的项目跟着写了一个自己的用户微服务,重构一下微服务框架体系。
项目源码:github地址,觉得有用的话,点个star,谢谢~

该项目涉及的技术栈:1. 配置viper集成2. 日志zap集成3. orm框架gorm集成4. redis集成5. protobuf 6. gin框架7. redis分布式锁实现8. 服务优雅退出9. jwt鉴权 token黑名单和续签机制10. 腾讯云sms sdk11. 全局错误和响应封装12. gin中间件 cors跨域13. 自定义校验器实现14. grpc15. 图片验证码
篇幅较长,关键代码会有注释,可以作为微服务练手级项目,加深自己对微服务体系的理解。

用户微服务

用户服务 srv

目录结构

Golang微服务一把嗦 用户微服务集成主流最新go技术栈

model

定义用户模型

model user.go

// 自定义公有字段 gorm自带 gorm.Model
type BaseModel struct {ID        int32     `gorm:"primarykey"`CreatedAt time.Time `gorm:"column:add_time"`UpdatedAt time.Time `gorm:"column:update_time"`DeletedAt gorm.DeletedAtIsDeleted bool
}/*
User1. 对称加密2. 非对称加密3. md5 信息摘要算法密码如果不可以反解,用户找回密码
*/
type User struct {BaseModelMobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null"` // index 索引Password string     `gorm:"type:varchar(100);not null"`NickName string     `gorm:"type:varchar(20)"`Birthday *time.Time `gorm:"type:datetime"`Gender   string     `gorm:"column:gender;default:male;type:varchar(6) comment 'female表示女, male表示男'"` // 注意commentRole     int        `gorm:"column:role;default:1;type:int comment '1表示普通用户, 2表示管理员'"`
}

同步表结构

mymicro/srvs/user_srv/model/main/main.go

func main() {dsn := "root:root@tcp(192.168.60.120:3306)/mymicro_user_srv?charset=utf8mb4&parseTime=True&loc=Local"newLogger := logger.New(log.New(os.Stdout, "\\r\\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold: time.Second, // 慢 SQL 阈值LogLevel:      logger.Info, // Log levelColorful:      true,        // 禁用彩色打印},)// 全局模式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{})

加密工具

md5

mymicro/srvs/user_srv/utils/cryptx/md5.go

扩展 加盐彩虹表 二次加密

import ("crypto/md5""encoding/hex""io"
)func GenMD5(code string) string {Md5 := md5.New()_, _ = io.WriteString(Md5, code)return hex.EncodeToString(Md5.Sum(nil))
}

encode

mymicro/srvs/user_srv/utils/cryptx/encode.go

github.com/anaskhan96/go-password-encoder 第三方加密库

import ("crypto/sha512""fmt""github.com/anaskhan96/go-password-encoder""strings"
)// github.com/anaskhan96/go-password-encoder 加密
var options = &password.Options{16, 100, 32, sha512.New}func EncodePwd(code string) string {salt, encodedPwd := password.Encode(code, options)newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)return newPassword
}func VerifyPwd(code, pwd string) bool {passwordInfo := strings.Split(pwd, "$")check := password.Verify(code, passwordInfo[2], passwordInfo[3], options)return check
}

proto生成

命令

protoc -I . user.proto --go_out=. --go-grpc_out=.

proto文件

mymicro/srvs/user_srv/proto/user.proto

pb.go 里面的接口 UserServer

syntax = "proto3";
import "google/protobuf/empty.proto";  // 空结构 message Empty {}
option go_package="./proto";
service User{rpc GetUserList(PageInfo) returns (UserListResponse); // 用户列表rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse); //通过mobile查询用户rpc GetUserById(IdRequest) returns (UserInfoResponse); //通过id查询用户rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 添加用户rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); // 更新用户rpc CheckPassWord(PasswordCheckInfo) returns (CheckResponse); //检查密码
}message PasswordCheckInfo {string password = 1;string encryptedPassword = 2;
}message CheckResponse{bool success = 1;
}message PageInfo {uint32 pn = 1;uint32 pSize = 2;
}message MobileRequest{string mobile = 1;
}message IdRequest {int32 id = 1;
}message CreateUserInfo {string nickName = 1;string passWord = 2;string mobile = 3;
}message UpdateUserInfo {int32 id = 1;string nickName = 2;string gender = 3;uint64 birthDay = 4; // 本质是数字
}message UserInfoResponse {int32 id = 1;string passWord = 2;string mobile = 3;string nickName = 4;uint64 birthDay = 5;string gender = 6;int32 role = 7;
}message UserListResponse {int32 total = 1;repeated UserInfoResponse data = 2; // repeated 数组 
}

初始化

DB

初始化gorm-mysql数据库

  • 连接mysql
  • 注意dsn换为自己本地的
import ("fmt""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/schema""log""os""time"
)func InitialDB() *gorm.DB {dsn := "root:root@tcp(192.168.60.120:3306)/mymicro_user_srv?charset=utf8mb4&parseTime=True&loc=Local"newLogger := logger.New(log.New(os.Stdout, "\\r\\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold: time.Second, // 慢 SQL 阈值LogLevel:      logger.Info, // Log levelColorful:      true,        // 禁用彩色打印},)// 全局模式db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true,},Logger: newLogger,})if err != nil {panic(err)}fmt.Println("==========connect database success=============")//_ = db.AutoMigrate(&model.User{})return db
}

server

开启服务

  • 实例grpc服务 grpc.NewServer()
  • 注册grpc
  • 监听tcp连接
  • 开启协程保持grpc连接
  • 监听信号,优雅退出服务
import ("context""flag""fmt""google.golang.org/grpc""mymicro/srvs/user_srv/handler""mymicro/srvs/user_srv/proto/proto""net""os""os/signal""syscall""time"
)func RunServer() {// flag 解析addr := flag.String("ip", "localhost", "ip地址")port := flag.Int("port", 8060, "端口")flag.Parse()// 注册server := grpc.NewServer()proto.RegisterUserServer(server, &handler.UserServer{})// 开启监听lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *addr, *port))if err != nil {panic("server listen fail:" + err.Error())}go func() {err = server.Serve(lis)if err != nil {panic("failed to start grpc:" + err.Error())}}()fmt.Printf("==============listen %s %d...==============\\n", *addr, *port)// 优雅退出服务quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitctx, cancel := context.WithTimeout(context.Background(), time.Second*5)defer cancel()fmt.Println("=================quiting server...================")go func(ctx context.Context) {server.GracefulStop()}(ctx)fmt.Println("=================quit success================")
}

全局

定义全局App变量

  • db
import "gorm.io/gorm"type app struct {DB *gorm.DB
}var App = new(app)

用户服务接口

mymicro/srvs/user_srv/handler/user.go

抽离公有方法

用户返回信息 ModelToResponse

func ModelToResponse(user model.User) proto.UserInfoResponse {userInfoRsp := proto.UserInfoResponse{Id:       user.ID,PassWord: user.Password,NickName: user.NickName,Gender:   user.Gender,Role:     int32(user.Role),Mobile:   user.Mobile,}if user.Birthday != nil {userInfoRsp.BirthDay = uint64(user.Birthday.Unix())}return userInfoRsp
}

分页方法 Paginate

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 = 100case pageSize <= 0:pageSize = 10}offset := (page - 1) * pageSizereturn db.Offset(offset).Limit(pageSize) // 从offset+1行开始返回pagesize行}
}

用户列表接口

逻辑:

  1. 定义用户列表切片
  2. find查询所有用户,scope查出用户数
  3. 对用户分页处理
  4. 返回用户列表信息
func (s *UserServer) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {var users []model.User// 返回所有 listres := global.App.DB.Find(&users)if res.Error != nil {return nil, res.Error}rsp := &proto.UserListResponse{}// 返回字段数 countrsp.Total = int32(res.RowsAffected)// 分页global.App.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)for _, user := range users {userInfo := ModelToResponse(user)rsp.Data = append(rsp.Data, &userInfo)}return rsp, nil
}

通过手机号查询用户

逻辑:

  1. 定义用户模型
  2. where根据手机号查询用户,判断
  3. 返回用户信息
func (s *UserServer) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {var user model.Userres := global.App.DB.Where(&model.User{Mobile: req.Mobile,}).First(&user)if res.RowsAffected == 0 {return nil, status.Error(codes.NotFound, "该用户不存在")}if res.Error != nil {return nil, res.Error}userInfoRsp := ModelToResponse(user)return &userInfoRsp, nil
}

通过id查询用户

逻辑:

  1. 定义用户模型
  2. 根据id查询用户,判断
  3. 返回用户信息
func (s *UserServer) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {var user model.Userres := global.App.DB.First(&user, req.Id)if res.RowsAffected == 0 {return nil, status.Error(codes.NotFound, "该用户不存在")}if res.Error != nil {return nil, res.Error}userInfoRsp := ModelToResponse(user)return &userInfoRsp, nil
}

创建用户

逻辑:

  1. 定义用户模型
  2. 根据手机号查询,判断是否存在该用户
  3. 根据传入参数,create用户,记得用户加密,判断
  4. 返回用户信息
func (s *UserServer) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {var user model.User// 判断用户是否存在res := global.App.DB.Where(&model.User{Mobile: req.Mobile,}).First(&user)if res.RowsAffected == 1 {return nil, status.Errorf(codes.AlreadyExists, "该用户已存在")}user.Mobile = req.Mobileuser.NickName = req.NickNamepwd := cryptx.EncodePwd(req.PassWord)fmt.Println(pwd)user.Password = pwdres = global.App.DB.Create(&user)if res.Error != nil {return nil, res.Error}userInfoRsp := ModelToResponse(user)return &userInfoRsp, nil
}

更新用户信息

逻辑:

  1. 定义用户模型
  2. 通过id查询,是否存在该用户
  3. 根据传入参数,Save用户,注意时间类型处理,判断
  4. 返回响应信息
func (s *UserServer) UpdateUser(ctx context.Context, req *proto.UpdateUserInfo) (*emptypb.Empty, error) {var user model.User// 验证res := global.App.DB.First(&user, req.Id)if res.RowsAffected == 0 {return nil, status.Errorf(codes.NotFound, "该用户不存在")}if res.Error != nil {return nil, res.Error}birthday := time.Unix(int64(req.BirthDay), 0)user.Birthday = &birthdayuser.NickName = req.NickNameuser.Gender = req.Genderres = global.App.DB.Save(&user)if res.Error != nil {return nil, res.Error}return &emptypb.Empty{}, nil
}

校验密码

逻辑:

  1. 根据传入参数调用校验方法
  2. 返回
func (s *UserServer) CheckPassWord(ctx context.Context, req *proto.PasswordCheckInfo) (*proto.CheckResponse, error) {check := cryptx.VerifyPwd(req.Password, req.EncryptedPassword)return &proto.CheckResponse{Success: check}, nil
}

main 启动

srvs/user_srv/main.go

func main() {// 初始化配置// 初始化日志// 初始化数据库global.App.DB = initialize.InitialDB()// 启动服务initialize.RunServer()
}

用户服务 web

选用gin框架

注意:proto文件没有写说明,直接将srv层的proto复制到web就可以了

目录结构

Golang微服务一把嗦 用户微服务集成主流最新go技术栈

utils工具集

目录判断 directory | redis锁标识 lockstr |加密 md5

directory apis/user_web/utils/directory.go

// PathExists 目录
func PathExists(path string) (bool, error) {fi, err := os.Stat(path)if err == nil {if fi.IsDir() {return true, nil}return false, errors.New("存在同名文件")}if os.IsNotExist(err) {return false, nil}return false, err
}

lockstr apis/user_web/utils/lockstr.go

// RandString 生成锁标识
func RandString(len int) string {r := rand.New(rand.NewSource(time.Now().UnixNano()))bytes := make([]byte, len)for i := 0; i < len; i++ {b := r.Intn(26) + 65bytes[i] = byte(b)}return string(bytes)
}

md5 apis/user_web/utils/md5.go

func GenMD5(code string) string {Md5 := md5.New()_, _ = io.WriteString(Md5, code)return hex.EncodeToString(Md5.Sum(nil))
}

配置集成

配置文件

开发和生产config

开发 mymicro/apis/user_web/config-debug.yaml

app:name: user_webenv: debugport: 8070url: http://localhost# viper# zap
log:level: info # 开发日志级别prefix: '[mycro/user_web]' # 日志前缀format: console # 输出director: log # 日志存放的文件encode_level: LowercaseColorLevelEncoder # 编码级别stacktrace_key: stacktrace # 栈名max_age: 0 # 日志留存时间show_line: true # 显示行log_in_console: true # 输出控制台# jwt
jwt:secret: ;d/WOIx0:C}@qGHcpl'XuK2a+]FKS#SfS6$vr%g:Rm/XR6$*>:]`<{B^}v/T`Ow0 # 随机字符jwt_ttl: 43200  # token 有效期(秒)jwt_blacklist_grace_period: 10 # 黑名单宽限时间(秒)refresh_grace_period: 1800# redis
redis: host: 127.0.0.1port: 6379db: 0password:

生产 mymicro/apis/user_web/config-prod.yaml

app:name: user_webenv: production

配置结构体

配置相关 config | app |日志 log |鉴权jwt |redis

mymicro/apis/user_web/config/app.go

package configtype App struct {Name string `mapstructure:"name" json:"name" yaml:"name"`Env  string `mapstructure:"env" json:"env" yaml:"env"`Port string `mapstructure:"port" json:"port" yaml:"port"`Url  string `mapstructure:"url" json:"url" yaml:"url"`
}

mymicro/apis/user_web/config/log.go

package configimport ("go.uber.org/zap/zapcore""strings"
)type Log struct {Level         string `mapstructure:"level" json:"level" yaml:"level"`                            // 级别Prefix        string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`                         // 日志前缀Format        string `mapstructure:"format" json:"format" yaml:"format"`                         // 输出Director      string `mapstructure:"director" json:"director"  yaml:"director"`                  // 日志文件夹EncodeLevel   string `mapstructure:"encode_level" json:"encode_level" yaml:"encode_level"`       // 编码级StacktraceKey string `mapstructure:"stacktrace_key" json:"stacktrace_key" yaml:"stacktrace_key"` // 栈名MaxAge        int    `mapstructure:"max_age" json:"max_age" yaml:"max_age"`                      // 日志留存时间ShowLine      bool   `mapstructure:"show_line" json:"show_line" yaml:"show_line"`                // 显示行LogInConsole  bool   `mapstructure:"log_in_console" json:"log_in_console" yaml:"log_in_console"` // 输出控制台
}// ZapEncodeLevel 根据 EncodeLevel 返回 zapcore.LevelEncoder
func (l *Log) ZapEncodeLevel() zapcore.LevelEncoder {switch {case l.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)return zapcore.LowercaseLevelEncodercase l.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色return zapcore.LowercaseColorLevelEncodercase l.EncodeLevel == "CapitalLevelEncoder": // 大写编码器return zapcore.CapitalLevelEncodercase l.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色return zapcore.CapitalColorLevelEncoderdefault:return zapcore.LowercaseLevelEncoder}
}// TransportLevel 根据字符串转化为 zapcore.Level
func (l *Log) TransportLevel() zapcore.Level {l.Level = strings.ToLower(l.Level)switch l.Level {case "debug":return zapcore.DebugLevelcase "info":return zapcore.InfoLevelcase "warn":return zapcore.WarnLevelcase "error":return zapcore.WarnLevelcase "dpanic":return zapcore.DPanicLevelcase "panic":return zapcore.PanicLevelcase "fatal":return zapcore.FatalLeveldefault:return zapcore.DebugLevel}
}

jwt apis/user_web/config/jwt.go

type Config struct {App   App   `mapstructure:"app" json:"app" yaml:"app"`Log   Log   `mapstructure:"log" json:"log" yaml:"log"`Jwt   Jwt   `mapstructure:"jwt" json:"jwt" yaml:"jwt"`Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}

redis apis/user_web/config/redis.go

type Redis struct {Host     string `mapstructure:"host" json:"host" yaml:"host"`Port     string `mapstructure:"port" json:"port" yaml:"port"`DB       int    `mapstructure:"db" json:"db" yaml:"db"`Password string `mapstructure:"password" json:"password" yaml:"password"`
}

config apis/user_web/config/config.go

type Config struct {App   App   `mapstructure:"app" json:"app" yaml:"app"`Log   Log   `mapstructure:"log" json:"log" yaml:"log"`Jwt   Jwt   `mapstructure:"jwt" json:"jwt" yaml:"jwt"`Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}

自定义环境常量

mymicro/apis/user_web/initialize/internal/cfgEnv.go

const (DebugEnv = "config-debug.yaml"ProdEnv  = "config-prod.yaml"
)

viper集成

mymicro/apis/user_web/initialize/config.go

逻辑:

  • 按照 配置文件选择 参数>flag解析>自定义常量>gin环境 进行配置文件选择
  • new viper实例,配置viper
  • readinconfig 读配置
  • watchconfig 监控
  • 反序列化绑定到全局config struct
package initializeimport ("flag""fmt""github.com/fsnotify/fsnotify""github.com/gin-gonic/gin""github.com/spf13/viper""os""user_web/global""user_web/initialize/internal"
)func InitConfig(args ...string) *viper.Viper {// 配置文件选择 参数>flag解析>自定义常量>gin环境var config stringif len(args) == 0 {flag.StringVar(&config, "c", "", "choose config file")flag.Parse()if config == "" {if os.Getenv(internal.DebugEnv) == "" {switch gin.Mode() {case gin.DebugMode:config = internal.DebugEnvcase gin.ReleaseMode:config = internal.ProdEnv}} else {config = internal.DebugEnv}}} else {config = args[0]}fmt.Println("config :", config)// viper配置vip := viper.New()vip.SetConfigFile(config)vip.SetConfigType("yaml")// 读err := vip.ReadInConfig()if err != nil {panic(err.Error())}// 监控vip.WatchConfig()// 开启监控vip.OnConfigChange(func(in fsnotify.Event) {change := in.Namefmt.Printf("%s changed", change)// 反序列化if err = vip.Unmarshal(&global.App.Config); err != nil {panic(err.Error())}})// 绑定if err = vip.Unmarshal(&global.App.Config); err != nil {panic(err.Error())}fmt.Println("==========config init success=============")return vip
}

全局配置

type app struct {Config config.ConfigVip    *viper.Viper
}var App = new(app)

日志集成

说明:选择 主流zap日志库 集成到gin

日志切割

mymicro/apis/user_web/initialize/internal/file_rotate_logs.go

  • 定义 WriteSyncer
import (rotatelogs "github.com/lestrrat-go/file-rotatelogs""go.uber.org/zap/zapcore""os""path""time""user_web/global"
)var FileRotateLogs = new(fileRotateLogs)type fileRotateLogs struct {
}func (f *fileRotateLogs) GetWriteSyncer(level string) (zapcore.WriteSyncer, error) {fileWriter, err := rotatelogs.New(// 输出文件名path.Join(global.App.Config.Log.Director, "%Y-%m-%d", level+".log"),rotatelogs.WithClock(rotatelogs.Local),rotatelogs.WithMaxAge(time.Duration(global.App.Config.Log.MaxAge)*24*time.Hour), // 日志留存时间rotatelogs.WithRotationTime(time.Hour*24))if global.App.Config.Log.LogInConsole {return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err}return zapcore.AddSync(fileWriter), err
}

定制化zap

mymicro/apis/user_web/initialize/internal/zap.go

zap配置

  • 编码器
  • 日志切割
  • 自定义时间输出格式
  • 日志等级
import ("fmt""go.uber.org/zap""go.uber.org/zap/zapcore""time""user_web/global"
)var Zap = new(_zap)type _zap struct{}// GetEncoder 获取 zapcore.Encoder
func (z *_zap) GetEncoder() zapcore.Encoder {if global.App.Config.Log.Format == "json" {return zapcore.NewJSONEncoder(z.GetEncoderConfig())}return zapcore.NewConsoleEncoder(z.GetEncoderConfig())
}// GetEncoderConfig 获取zapcore.EncoderConfig
func (z *_zap) GetEncoderConfig() zapcore.EncoderConfig {return zapcore.EncoderConfig{MessageKey:     "message",LevelKey:       "level",TimeKey:        "time",NameKey:        "logger",CallerKey:      "caller",StacktraceKey:  global.App.Config.Log.StacktraceKey,LineEnding:     zapcore.DefaultLineEnding,EncodeLevel:    global.App.Config.Log.ZapEncodeLevel(),EncodeTime:     z.CustomTimeEncoder,EncodeDuration: zapcore.SecondsDurationEncoder,EncodeCaller:   zapcore.FullCallerEncoder,}
}// GetEncoderCore 获取Encoder的 zapcore.Core
func (z *_zap) GetEncoderCore(l zapcore.Level, level zap.LevelEnablerFunc) zapcore.Core {writer, err := FileRotateLogs.GetWriteSyncer(l.String()) // 使用file-rotatelogs进行日志分割if err != nil {fmt.Printf("Get Write Syncer Failed err:%v", err.Error())return nil}return zapcore.NewCore(z.GetEncoder(), writer, level)
}// CustomTimeEncoder 自定义日志输出时间格式
func (z *_zap) CustomTimeEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {encoder.AppendString(global.App.Config.Log.Prefix + " - " + t.Format("2006/01/02 - 15:04:05.000"))
}// GetZapCores 根据配置文件的Level获取 []zapcore.Core
func (z *_zap) GetZapCores() []zapcore.Core {cores := make([]zapcore.Core, 0, 7)for level := global.App.Config.Log.TransportLevel(); level <= zapcore.FatalLevel; level++ {cores = append(cores, z.GetEncoderCore(level, z.GetLevelPriority(level)))}return cores
}// GetLevelPriority 根据 zapcore.Level 获取 zap.LevelEnablerFunc
func (z *_zap) GetLevelPriority(level zapcore.Level) zap.LevelEnablerFunc {switch level {case zapcore.DebugLevel:return func(level zapcore.Level) bool { // 调试级别return level == zap.DebugLevel}case zapcore.InfoLevel:return func(level zapcore.Level) bool { // 日志级别return level == zap.InfoLevel}case zapcore.WarnLevel:return func(level zapcore.Level) bool { // 警告级别return level == zap.WarnLevel}case zapcore.ErrorLevel:return func(level zapcore.Level) bool { // 错误级别return level == zap.ErrorLevel}case zapcore.DPanicLevel:return func(level zapcore.Level) bool { // dpanic级别return level == zap.DPanicLevel}case zapcore.PanicLevel:return func(level zapcore.Level) bool { // panic级别return level == zap.PanicLevel}case zapcore.FatalLevel:return func(level zapcore.Level) bool { // 终止级别return level == zap.FatalLevel}default:return func(level zapcore.Level) bool { // 调试级别return level == zap.DebugLevel}}
}

目录工具

mymicro/apis/user_web/utils/directory.go

判断是否存在目录

// PathExists 目录
func PathExists(path string) (bool, error) {fi, err := os.Stat(path)if err == nil {if fi.IsDir() {return true, nil}return false, errors.New("存在同名文件")}if os.IsNotExist(err) {return false, nil}return false, err
}

集成zap

mymicro/apis/user_web/initialize/log.go

逻辑:

  • 日志输出目录
  • 集成zapcore
func InitLog() *zap.Logger {if ok, _ := utils.PathExists(global.App.Config.Log.Director); !ok {log.Printf("create directory %s", global.App.Config.Log.Director)_ = os.Mkdir(global.App.Config.Log.Director, os.ModePerm)}cores := internal.Zap.GetZapCores()logger := zap.New(zapcore.NewTee(cores...))if global.App.Config.Log.ShowLine {logger = logger.WithOptions(zap.AddCaller())}log.Println("zap log init success")return logger
}

自定义校验

校验工具

mymicro/apis/user_web/utils/validator.go

逻辑:

  • 定义validator接口
  • 定义验证错误信息方法
  • 自定义验证规则
type ValidatorMessages map[string]stringtype Validator interface {GetMessages() ValidatorMessages
}func GetValidatorMsg(req interface{}, err error) string {if _, isValidatorErr := err.(validator.ValidationErrors); isValidatorErr {_, isValidator := req.(Validator)for _, v := range err.(validator.ValidationErrors) {if isValidator {if msg, exist := req.(Validator).GetMessages()[v.Field()+"."+v.Tag()]; exist {return msg}}}}return "parameter error"
}// ValidateMobile 校验mobile
func ValidateMobile(fl validator.FieldLevel) bool {mobile := fl.Field().String()ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$`, mobile)if !ok {return false}return true
}// ValidatorEmail 验证email
func ValidatorEmail(fl validator.FieldLevel) bool {email := fl.Field().String()ok, _ := regexp.MatchString(`^[a-zA-Z0-9.!#$%&’*+/=?^_`+`){|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$`, email)if !ok {return false}return true
}

校验规则

mymicro/apis/user_web/forms/user.go

// PageInfoForm 列表分页
type PageInfoForm struct {Pn    uint32 `form:"pn" json:"pn" binding:"required"`PSize uint32 `form:"p_size" json:"p_size" binding:"required"`
}type MobileReqForm struct {Mobile string `binding:"required,mobile" json:"mobile" form:"mobile"`
}type IdReqForm struct {Id int32 `binding:"required" form:"id" json:"id"`
}type CreateInfoForm struct {Nickname string `binding:"required" form:"nickname" json:"nickname"`Password string `binding:"required,min=6,max=32" json:"password" form:"password"`Mobile   string `json:"mobile" form:"mobile" binding:"required,mobile"`//Code     string `json:"code" form:"code" binding:"required,min=6,max=6"`
}type UpdateInfoForm struct {Id       int32  `binding:"required" form:"id" json:"id"`Nickname string `binding:"required" form:"nickname" json:"nickname"`Gender   string `binding:"required" form:"gender" json:"gender"`Birthday uint64 `binding:"required" form:"birthday" json:"birthday"`
}type PasswordCheckForm struct {Password string `binding:"required,min=6,max=32" json:"password" form:"password"`
}
type LoginInfo struct {Password    string `binding:"required" json:"password" form:"password"`Mobile      string `binding:"required,mobile" json:"mobile" form:"mobile"`CaptchaCode string `binding:"required,min=5,max=5" json:"captcha_code" form:"captcha_code"`CaptchaId   string `form:"captcha_id" json:"captcha_id" binding:"required"`
}func (l LoginInfo) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"mobile.required":       "手机号不能为空","mobile.mobile":         "手机号格式不对","captcha_code.required": "验证码不能为空","captcha_id.required":   "id不能为空",}
}func (p PageInfoForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"pn.required":       "页码不能为空","p_size.required":   "页数不能为空","password.required": "密码不能为空",}
}func (m MobileReqForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"mobile.required": "手机号不能为空","mobile.mobile":   "手机号格式不对",}
}func (I IdReqForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"id.required": "用户id不能为空",}
}func (c CreateInfoForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"mobile.required":   "手机号不能为空","mobile.mobile":     "手机号格式不对","password.required": "密码不能为空","nickname.required": "用户昵称不能为空",//"code.required":     "短信验证码不能为空",}
}func (u UpdateInfoForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"id.required":       "用户id不能为空","nickname.required": "用户昵称不能为空","gender.required":   "用户性别不能为空","birthday.required": "用户生日不能为空",}
}func (pw PasswordCheckForm) GetMessages() utils.ValidatorMessages {return utils.ValidatorMessages{"password.required":          "密码不能为空","encryptedPassword.required": "验证密码不能为空",}
}

集成Validator

mymicro/apis/user_web/initialize/validator.go

逻辑:

  • 注册自定义validator
// InitValidator 注册validator
func InitValidator() {if v, ok := binding.Validator.Engine().(*validator.Validate); ok {// 注册自定义验证器_ = v.RegisterValidation("mobile", utils.ValidateMobile)_ = v.RegisterValidation("email", utils.ValidatorEmail)// 注册自定义 json tag 函数v.RegisterTagNameFunc(func(fld reflect.StructField) string {name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]if name == "-" {return ""}return name})zap.S().Info("validator init success")} else {zap.S().Error("validator register failer")}
}

redis集成

apis/user_web/initialize/redis.go

  • new redis 实例并配置连接信息
  • 返回redis实例
import ("context""github.com/go-redis/redis/v8""go.uber.org/zap""user_web/global"
)func InitializeRedis() *redis.Client {client := redis.NewClient(&redis.Options{Addr:     global.App.Config.Redis.Host + ":" + global.App.Config.Redis.Port,Password: global.App.Config.Redis.Password,DB:       global.App.Config.Redis.DB,})_, err := client.Ping(context.Background()).Result()if err != nil {global.App.Log.Error("redis connect failed err:", zap.Any("err", err))return nil}zap.S().Info("redis init success")return client
}

全局错误和Response

全局错误

mymicro/apis/user_web/global/errorx/error.go

/*
CustomError
CustomErrors
Errors
*/
type CustomError struct {ErrorCode intErrorMsg  string
}
type CustomErrors struct {ValidatorError CustomErrorJWTError       CustomErrorBusinessError  CustomError
}var Errors = CustomErrors{ValidatorError: CustomError{ErrorCode: 401000,ErrorMsg:  "参数验证错误",},JWTError: CustomError{ErrorCode: 404000,ErrorMsg:  "JWT 未授权",},BusinessError: CustomError{ErrorCode: 440000,ErrorMsg:  "业务错误",},
}

全局response

mymicro/apis/user_web/global/response/response.go

  • 成功
  • 失败
  • 自定义
/*
Response
Success
Fail
FailError
FailByError
ValidatorError
JWTError
*/type Response struct {Code intMsg  stringData interface{}
}func Success(ctx *gin.Context, data interface{}) {ctx.JSON(http.StatusOK, Response{Code: 200,Msg:  "request success",Data: data,})
}func Fail(ctx *gin.Context, code int, msg string) {ctx.JSON(http.StatusOK, Response{Code: code,Msg:  msg,Data: nil,})
}func FailByError(ctx *gin.Context, err errorx.CustomError) {Fail(ctx, err.ErrorCode, err.ErrorMsg)
}
func ValidatorError(ctx *gin.Context, msg string) {Fail(ctx, errorx.Errors.ValidatorError.ErrorCode, msg)
}
func JWTError(ctx *gin.Context) {FailByError(ctx, errorx.Errors.JWTError)
}
func BusinessError(ctx *gin.Context) {FailByError(ctx, errorx.Errors.BusinessError)
}

全局变量

apis/user_web/global/global.go

type app struct {Config  config.ConfigVip     *viper.ViperSrv     proto.UserClientLog     *zap.LoggerSrvConn *grpc.ClientConnRedis   *redis.Client
}var App = new(app)

全局redis 分布式锁

apis/user_web/global/lock.go

import ("context""github.com/go-redis/redis/v8""time""user_web/utils"
)type Interface interface {Get() boolBlock(seconds int64) boolRelease() boolForceRelease()
}
type lock struct {context context.Contextname    string // 锁名字owner   string // 锁标识seconds int64  // 有效期
}// 释放锁lua脚本,防止任何客户端都能解锁
// 通过两个数组KEYS和ARGV来传递值,KEYS数组用于传递键名,而ARGV数组用于传递其他值。
const releaseLockLuaScript = `
if redis.call("get",KEYS[1])==ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
`// Lock 生成锁
func Lock(name string, seconds int64) Interface {return &lock{context.Background(),name,utils.RandString(16),seconds,}
}// Get 获取锁
func (l *lock) Get() bool {// SETNX命令获取锁return App.Redis.SetNX(l.context, l.name, l.owner, time.Duration(l.seconds)*time.Second).Val() // context key value ttl
}// Block 阻塞一段时间,尝试获取锁
func (l *lock) Block(seconds int64) bool {starting := time.Now().Unix()for {if !l.Get() {time.Sleep(time.Duration(1) * time.Second)if time.Now().Unix()-seconds >= starting {return false}} else {return true}}
}// Release 释放锁
func (l *lock) Release() bool {luaScript := redis.NewScript(releaseLockLuaScript)result := luaScript.Run(l.context, App.Redis, []string{l.name}, l.owner).Val().(int64)return result != 0
}// ForceRelease 强制释放锁
func (l *lock) ForceRelease() {App.Redis.Del(l.context, l.name).Val()
}

连接grpc

mymicro/apis/user_web/initialize/rpc_conn.go

  • dial
  • client
func ConnectUserGrpc() (*proto.UserClient, *grpc.ClientConn) {conn, err := grpc.Dial("127.0.0.1:8059", grpc.WithInsecure())if err != nil {panic(err.Error())}client := proto.NewUserClient(conn)return &client, conn
}

user api

路由

apis/user_web/router/router.go

路由接口

  • 根据自己需求,定义接口
  • 扩展路由组和使用中间件
func SetRouterGroupApi(r *gin.RouterGroup) {/*                       test 测试路由                    */r.GET("/ping", func(ctx *gin.Context) {ctx.String(http.StatusOK, "pong")})r.POST("/valid", func(ctx *gin.Context) {var register forms.CreateInfoFormif err := ctx.ShouldBindJSON(&register); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(register, err))} else {response.Success(ctx, "hello world")}})/* 用户接口*/// 注册r.POST("/register", api.RegisterHandler)// 登录r.POST("/login", api.LoginHandler)// 图片验证码r.GET("/captcha", api.GetCaptcha)// 短信验证码r.GET("/sms")authRouter := r.Group("/auth").Use(middlewares.JWTAuth(api.AppGuardName)){// 用户列表authRouter.POST("/userlist", api.UserListHandler)// 手机查用户authRouter.POST("/infoBymobile", api.UserByMobileHandler)// id查用户authRouter.POST("/infoByid", api.UserByIDHandler)// 更新用户authRouter.POST("/updateinfo", api.UpdateHandler)// 退出登录authRouter.POST("/logout", api.LogoutHandler)}r.Use(middlewares.Cors())
}

中间件

跨域cors | jwt鉴权

cors apis/user_web/middlewares/cors.go

import ("github.com/gin-gonic/gin""net/http"
)func Cors() gin.HandlerFunc {return func(c *gin.Context) {method := c.Request.Methodc.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")c.Header("Access-Control-Allow-Credentials", "true")if method == "OPTIONS" {c.AbortWithStatus(http.StatusNoContent)}}
}

jwt apis/user_web/middlewares/jwt.go

import ("github.com/dgrijalva/jwt-go""github.com/gin-gonic/gin""strconv""time""user_web/api""user_web/global""user_web/global/response"
)func JWTAuth(GuardName string) gin.HandlerFunc {return func(c *gin.Context) {tokenStr := c.Request.Header.Get("Authorization")if tokenStr == "" {response.JWTError(c)c.Abort()return}tokenStr = tokenStr[len(api.TokenType)+1:]// token解析校验token, err := jwt.ParseWithClaims(tokenStr, &api.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {return []byte(global.App.Config.Jwt.Secret), nil})// 增加黑名单校验if err != nil || api.JwtService.IsInBlackList(tokenStr) {response.JWTError(c)c.Abort()return}claims := token.Claims.(*api.CustomClaims)// token发布者校验if claims.Issuer != GuardName {response.JWTError(c)c.Abort()return}// token 续签if claims.ExpiresAt-time.Now().Unix() < global.App.Config.Jwt.RefreshGracePeriod {lock := global.Lock("refresh_token_lock", global.App.Config.Jwt.JwtBlacklistGracePeriod)id, _ := strconv.Atoi(claims.Id)if lock.Get() {user, err := api.JwtService.GetUserInfo(GuardName, int32(id))if err != nil {global.App.Log.Error(err.Error())lock.Release()} else {tokenData, _, _ := api.JwtService.CreateToken(GuardName, user)c.Header("new-token", tokenData.AccessToken)c.Header("new-expires-in", strconv.Itoa(tokenData.ExpiresIn))_ = api.JwtService.JoinBlackList(token)}}}c.Set("token", token)c.Set("id", claims.Id)}
}

路由handler

用户接口

apis/user_web/api/user.go

import ("context""github.com/dgrijalva/jwt-go""github.com/gin-gonic/gin""user_web/forms""user_web/global""user_web/global/response""user_web/proto/proto""user_web/utils"
)// RegisterHandler 注册
func RegisterHandler(ctx *gin.Context) {var form forms.CreateInfoFormif err := ctx.ShouldBindJSON(&form); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(form, err))return}// 校验短信// reg := 正则匹配//if form.Code != res_.code() {//	response.Fail(ctx,498000,"验证码错误")//}res, err := global.App.Srv.CreateUser(context.Background(), &proto.CreateUserInfo{NickName: form.Nickname,Mobile:   form.Mobile,PassWord: form.Password,})if err != nil {response.Fail(ctx, 40000, err.Error())return}response.Success(ctx, res)
}// LoginHandler 登录
func LoginHandler(ctx *gin.Context) {var login forms.LoginInfoif err := ctx.ShouldBindJSON(&login); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(login, err))return}// 校验验证码//fmt.Println(store.Get(login.CaptchaId, false))//if !store.Verify(login.CaptchaId, login.CaptchaCode, false) {//	response.Fail(ctx, 483000, "验证码错误")//	return//}// 校验短信// 查用户res, err := global.App.Srv.GetUserByMobile(context.Background(), &proto.MobileRequest{Mobile: login.Mobile,})if err != nil {response.Fail(ctx, 440000, utils.GetValidatorMsg(login, err))return}// 校验密码resp, err := global.App.Srv.CheckPassWord(context.Background(), &proto.PasswordCheckInfo{Password:          login.Password,EncryptedPassword: res.PassWord,})if err != nil || !resp.Success {response.Fail(ctx, 440000, "密码错误")return}tokenData, err, _ := JwtService.CreateToken(AppGuardName, res)if err != nil {response.Fail(ctx, 470000, err.Error())return}response.Success(ctx, tokenData)
}// LogoutHandler 退出登录
func LogoutHandler(ctx *gin.Context) {err := JwtService.JoinBlackList(ctx.Keys["token"].(*jwt.Token))if err != nil {response.Fail(ctx, 43200, "退出登录 fail")}response.Success(ctx, "退出登录 success")
}// UserListHandler 用户列表
func UserListHandler(ctx *gin.Context) {var list forms.PageInfoFormif err := ctx.ShouldBindJSON(&list); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(list, err))return}res, err := global.App.Srv.GetUserList(context.Background(), &proto.PageInfo{Pn:    list.Pn,PSize: list.PSize,})if err != nil {response.Fail(ctx, 45000, err.Error())return}response.Success(ctx, res)
}// UserByIDHandler 通过id获取用户
func UserByIDHandler(ctx *gin.Context) {var id forms.IdReqFormif err := ctx.ShouldBindJSON(&id); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(id, err))return}res, err := global.App.Srv.GetUserById(context.Background(), &proto.IdRequest{Id: id.Id,})if err != nil {response.Fail(ctx, 43100, err.Error())}response.Success(ctx, res)
}// UpdateHandler 更新用户
func UpdateHandler(ctx *gin.Context) {var info forms.UpdateInfoFormif err := ctx.ShouldBindJSON(&info); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(info, err))return}res, err := global.App.Srv.UpdateUser(ctx, &proto.UpdateUserInfo{Id:       info.Id,NickName: info.Nickname,Gender:   info.Gender,BirthDay: info.Birthday,})if err != nil {response.Fail(ctx, 45600, err.Error())return}response.Success(ctx, res)
}// UserByMobileHandler 通过mobile获取用户
func UserByMobileHandler(ctx *gin.Context) {var mobile forms.MobileReqFormif err := ctx.ShouldBindJSON(&mobile); err != nil {response.ValidatorError(ctx, utils.GetValidatorMsg(mobile, err))return}res, err := global.App.Srv.GetUserByMobile(context.Background(), &proto.MobileRequest{Mobile: mobile.Mobile,})if err != nil {response.Fail(ctx, 457000, err.Error())return}response.Success(ctx, res)
}

腾讯云短信接口

apis/user_web/api/sms.go

/*
腾讯云短信sms接口
Go 1.9版本及以上
*/
import ("github.com/gin-gonic/gin""user_web/global/response""github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common""github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors""github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
)var res_ *sms.SendSmsResponsefunc GetSMS(ctx *gin.Context) {// 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密// 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305// 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取credential := common.NewCredential("SecretId","SecretKey",)// 实例化一个client选项,可选的,没有特殊需求可以跳过cpf := profile.NewClientProfile()cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com"// 实例化要请求产品的client对象,clientProfile是可选的client, _ := sms.NewClient(credential, "ap-nanjing", cpf)// 实例化一个请求对象,每个接口都会对应一个request对象request := sms.NewSendSmsRequest()request.PhoneNumberSet = common.StringPtrs([]string{"15533332222"})request.SmsSdkAppId = common.StringPtr("1400661435")request.SignName = common.StringPtr("汐瀼测试")request.TemplateId = common.StringPtr("1362776")// 返回的resp是一个SendSmsResponse的实例,与请求对象对应res_, err := client.SendSms(request)if _, ok := err.(*errors.TencentCloudSDKError); ok {response.Fail(ctx, 467000, "调用sdk失败")return}if err != nil {response.Fail(ctx, 467000, err.Error())}// 输出json格式的字符串回包//fmt.Printf("%s", response.ToJsonString())response.Success(ctx, res_.ToJsonString())
}

jwt授权接口

apis/user_web/api/jwt.go

type jwtService struct {
}var JwtService = new(jwtService)// CustomClaims 自定义claims
type CustomClaims struct {jwt.StandardClaims
}const (TokenType    = "bearer"AppGuardName = "app"
)type TokenOutput struct {AccessToken string `yaml:"access_token"`ExpiresIn   int    `json:"expires-in"`TokenType   string `json:"token-type"`
}// CreateToken 生成token
func (js *jwtService) CreateToken(GuardName string, user *proto.UserInfoResponse) (tokenData TokenOutput, err error, token *jwt.Token) {// crypt.claims.secrettoken = jwt.NewWithClaims(jwt.SigningMethodHS256,CustomClaims{StandardClaims: jwt.StandardClaims{ExpiresAt: time.Now().Unix() + global.App.Config.Jwt.JwtTtl,Id:        strconv.Itoa(int(user.Id)),Issuer:    GuardName, // 用于在中间件区分不同客户端发的token,避免token跨端使用NotBefore: time.Now().Unix() - 1000,},},)tokenStr, err := token.SignedString([]byte(global.App.Config.Jwt.Secret))tokenData = TokenOutput{tokenStr,int(global.App.Config.Jwt.JwtTtl),TokenType,}return
}// 获取黑名单缓存key
func (js *jwtService) getBlackListKey(tokenStr string) string {return "jwt_black_list:" + utils.GenMD5(tokenStr)
}// JoinBlackList token加入黑名单
func (js *jwtService) JoinBlackList(token *jwt.Token) (err error) {nowUnix := time.Now().Unix()timer := time.Duration(token.Claims.(*CustomClaims).ExpiresAt-nowUnix) * time.Second// 将token剩余时间设置为缓存有效期,并将当前时间作为缓存value值err = global.App.Redis.SetNX(context.Background(), js.getBlackListKey(token.Raw), nowUnix, timer).Err()return
}// IsInBlackList token是否在黑名单中
func (js *jwtService) IsInBlackList(tokenStr string) bool {joinUnixStr, err := global.App.Redis.Get(context.Background(), js.getBlackListKey(tokenStr)).Result()joinUnix, err := strconv.ParseInt(joinUnixStr, 10, 64)if joinUnixStr == "" || err != nil {return false}// JwtBlacklistGracePeriod 为黑名单宽限时间,避免并发请求失败if time.Now().Unix()-joinUnix < global.App.Config.Jwt.JwtBlacklistGracePeriod {return false}return true
}// GetUserInfo 根据不同客户端token,查询不同用户表数据
func (js *jwtService) GetUserInfo(GuardName string, id int32) (info *proto.UserInfoResponse, err error) {switch GuardName {case AppGuardName:return global.App.Srv.GetUserById(context.Background(), &proto.IdRequest{Id: id,})default:err = errors.New("guard" + GuardName + "does not exist")}return
}

图片验证码接口

apis/user_web/api/captcha.go

import ("fmt""github.com/gin-gonic/gin""github.com/mojocn/base64Captcha""user_web/global/response"
)var store = base64Captcha.DefaultMemStorefunc GetCaptcha(ctx *gin.Context) {driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)cp := base64Captcha.NewCaptcha(driver, store)id, b64s, err := cp.Generate()if err != nil {//zap.S().Errorf("验证码生成失败", err.Error())response.Fail(ctx, 479000, "验证码生成失败")return}fmt.Println(store.Get(id, false))response.Success(ctx, gin.H{"id":  id,"pic": b64s,})
}

server启动

server启动

mymicro/apis/user_web/initialize/server.go

逻辑

  • 实例gin
  • 监听服务
  • 优雅退出服务
package initializeimport ("context""github.com/gin-gonic/gin""go.uber.org/zap""net/http""os""os/signal""syscall""time""user_web/global""user_web/router"
)func RunServer() {r := setRouterApi()server := &http.Server{Addr:    ":" + global.App.Config.App.Port,Handler: r,}zap.S().Infof("server start on port %s", global.App.Config.App.Port)go func() {if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {zap.S().Fatal(err.Error())}}()// 优雅退出quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quit_ = global.App.SrvConn.Close()zap.S().Info("grpc quit")zap.S().Info("server quiting")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := server.Shutdown(ctx); err != nil {zap.S().Error(err.Error())}zap.S().Info("server quited")
}func setRouterApi() *gin.Engine {gin.SetMode(gin.DebugMode)r := gin.Default()g := r.Group("/user")router.SetRouterGroupApi(g)return r
}

main 启动

apis/user_web/main.go

import ("go.uber.org/zap""user_web/global""user_web/initialize"
)func main() {// 初始化配置global.App.Vip = initialize.InitConfig()// 初始化日志global.App.Log = initialize.InitLog()zap.ReplaceGlobals(global.App.Log)// 初始化验证器initialize.InitValidator()// 连接redisglobal.App.Redis = initialize.InitializeRedis()// 初始化 grpc 连接global.App.Srv, global.App.SrvConn = initialize.ConnectUserGrpc()// 初始化serverinitialize.RunServer()
}