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

news2024/12/23 20:38:02

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

前言

前段时间,因为本地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 sdk
	11. 全局错误和响应封装
	12. gin中间件 cors跨域
	13. 自定义校验器实现
	14. grpc
	15. 图片验证码
篇幅较长,关键代码会有注释,可以作为微服务练手级项目,加深自己对微服务体系的理解。

用户微服务

用户服务 srv

目录结构

在这里插入图片描述

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.DeletedAt
	IsDeleted bool
}

/*
User
 1. 对称加密
 2. 非对称加密
 3. md5 信息摘要算法
    密码如果不可以反解,用户找回密码
*/
type User struct {
	BaseModel
	Mobile   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表示男'"` // 注意comment
	Role     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 writer
		logger.Config{
			SlowThreshold: time.Second, // 慢 SQL 阈值
			LogLevel:      logger.Info, // Log level
			Colorful:      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 writer
		logger.Config{
			SlowThreshold: time.Second, // 慢 SQL 阈值
			LogLevel:      logger.Info, // Log level
			Colorful:      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)
	<-quit
	ctx, 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 = 100
		case pageSize <= 0:
			pageSize = 10
		}

		offset := (page - 1) * pageSize
		return 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
	// 返回所有 list
	res := global.App.DB.Find(&users)
	if res.Error != nil {
		return nil, res.Error
	}
	rsp := &proto.UserListResponse{}
	// 返回字段数 count
	rsp.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.User
	res := 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.User
	res := 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.Mobile
	user.NickName = req.NickName
	pwd := cryptx.EncodePwd(req.PassWord)
	fmt.Println(pwd)
	user.Password = pwd
	res = 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 = &birthday
	user.NickName = req.NickName
	user.Gender = req.Gender
	res = 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就可以了

目录结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QRrpuZx-1682155691239)(C:\Users\小冉\AppData\Roaming\Typora\typora-user-images\image-20230421143325042.png)]

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) + 65
		bytes[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_web
  env: debug
  port: 8070
  url: 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.1
  port: 6379
  db: 0
  password:

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

app:
  name: user_web
  env: production

配置结构体

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

mymicro/apis/user_web/config/app.go

package config

type 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 config

import (
	"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.LowercaseLevelEncoder
	case l.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
		return zapcore.LowercaseColorLevelEncoder
	case l.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
		return zapcore.CapitalLevelEncoder
	case l.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
		return zapcore.CapitalColorLevelEncoder
	default:
		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.DebugLevel
	case "info":
		return zapcore.InfoLevel
	case "warn":
		return zapcore.WarnLevel
	case "error":
		return zapcore.WarnLevel
	case "dpanic":
		return zapcore.DPanicLevel
	case "panic":
		return zapcore.PanicLevel
	case "fatal":
		return zapcore.FatalLevel
	default:
		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 initialize

import (
	"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 string
	if 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.DebugEnv
				case 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.Name
		fmt.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.Config
	Vip    *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]string

type 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 int
	ErrorMsg  string
}
type CustomErrors struct {
	ValidatorError CustomError
	JWTError       CustomError
	BusinessError  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 int
	Msg  string
	Data 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.Config
	Vip     *viper.Viper
	Srv     proto.UserClient
	Log     *zap.Logger
	SrvConn *grpc.ClientConn
	Redis   *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() bool
	Block(seconds int64) bool
	Release() bool
	ForceRelease()
}
type lock struct {
	context context.Context
	name    string // 锁名字
	owner   string // 锁标识
	seconds int64  // 有效期
}

// 释放锁lua脚本,防止任何客户端都能解锁
// 通过两个数组KEYS和ARGV来传递值,KEYS数组用于传递键名,而ARGV数组用于传递其他值。
const releaseLockLuaScript = `
if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 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.CreateInfoForm
		if 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.Method

		c.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.CreateInfoForm
	if 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.LoginInfo
	if 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.PageInfoForm
	if 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.IdReqForm
	if 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.UpdateInfoForm
	if 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.MobileReqForm
	if 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.SendSmsResponse

func 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.secret
	token = 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.DefaultMemStore

func 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 initialize

import (
	"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()
	// 连接redis
	global.App.Redis = initialize.InitializeRedis()
	// 初始化 grpc 连接
	global.App.Srv, global.App.SrvConn = initialize.ConnectUserGrpc()
	// 初始化server
	initialize.RunServer()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/448587.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL Server基础 第二章 表结构管理

目录 一、数据类型 1&#xff0c;字符类数据类型 2&#xff0c;数值型数据类型 3&#xff0c;日期/时间型数据类型 二、主键&#xff08;Primary key&#xff09; 三、默认值 四、唯一键&#xff08;Unique&#xff09; 五、自增标识 六、约束 七、外键 一、数据类型 …

node+vue+mysql+java健身房网站管理系统

通过大数据管理的方法对健身房管理系统进行了详细的设计说明以及介绍&#xff0c;对健身房管理系统进行了开发和实践。作为一个健身房网站&#xff0c;它为用户提供了一个良好的求知平台。让用户能更好地了解健身带来的好处。前端技术&#xff1a;nodejsvueelementui,视图层其实…

3D智能四向穿梭车在电商物流中的应用|HEGERLS箱式四向穿梭车系统在服装制造仓的创新应用

四向穿梭车技术是物流仓储系统中的革命性技术&#xff0c;也由于四向穿梭车对于场地的适应性非常强的特性&#xff0c;可实现前后、左右、上下六维运行&#xff0c;结合提升机的布置和输送系统的布局&#xff0c;灵活调整四向车的空间布局&#xff0c;使某些不规则场地和闲置空…

NXP公司LPC21xx+热敏电阻实现温度检测

LPC2131/32/34/36/38微控制器基于16位/32位Arm7TDMI-S™CPU&#xff0c;支持实时仿真和嵌入式跟踪&#xff0c;具有尺寸小&#xff0c;功耗低&#xff0c;多个32位定时器、单/双10位8通道ADC、10位DAC、PWM通道、47个GPIO线&#xff08;它们拥有多达9个边沿或电平触发的外部中断…

SpringCloud 项目如何方便 maven 打包以及本地开发

一、背景 springcloud-alibaba &#xff0c;使用 nacos 做配置中心&#xff0c;maven 作为构建工具。为了防止 test 、prod 环境配置文件覆盖问题&#xff0c;使用 mvn -P 命令。 二、项目 pom 文件 1. 利用 resources 标签来指定目录&#xff0c;build > resources 标签&a…

【轴承故障检测】滚动轴承中进行基于振动的故障诊断研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

110.【十万字带你深入学习23种设计模式】

Java 23种设计模式 (一)、设计模式相关内容介绍1.软件设计模式概述(1).软件设计模式的产生背景(2).软件设计模式的概念(3).学习设计模式的重要性(4).设计模式分类 2.UML图(1).类图概述(2).类图的作用(3).类图表示法 3.软件设计原则(1).开闭原则 (重写不修改)(2).里氏代换原则 (…

Node内置模块 【https模块】

文章目录 &#x1f31f;前言&#x1f31f;https模块&#x1f31f;https原理&#x1f31f;SSL&#x1f31f;使用https&#x1f31f;搭建HTTPS服务器&#x1f31f;写在最后 &#x1f31f;前言 哈喽小伙伴们&#xff0c;新的专栏 Node 已开启&#xff1b;这个专栏里边会收录一些N…

不得不说的结构型模式-组合模式

组合模式是一种结构型设计模式&#xff0c;它允许您将对象组合成树形结构&#xff0c;并以统一的方式处理它们。该模式基于递归组合的想法&#xff0c;其中一个组件可以由许多更小的组件组成&#xff0c;这些更小的组件可以由更小的组件组成&#xff0c;以此类推。 在组合模式中…

Latex安装与简介

文章目录 Latex一.步入Latex的两种方式:1.下载安装与Vscode集成2.在线的编辑器: overleaf 二.解决中文输入的问题三.简单介绍 Latex 一.步入Latex的两种方式: 1.下载安装与Vscode集成 texlive下载与安装参考: https://blog.csdn.net/weixin_47581344/article/details/1243560…

Midjourney 提示词工具(10 个国内外最好最推荐的)

Midjourney&#xff0c;是一个革命性的基于人工智能的艺术生成器&#xff0c;可以从被称为提示的简单文本描述中生成令人惊叹的图像。Midjourney已经迅速成为艺术家、设计师和营销人员的首选工具&#xff08;包括像我这样根本不会设计任何东西的无能之辈&#xff09;。 为了帮…

前端优化的分析

前端优化的分析 目录概述需求&#xff1a; 设计思路实现思路分析渲染层性能更好的API 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,cha…

强化学习技巧

此软件包处于维护模式&#xff0c;请使用Stable-Baselines3 (SB3)获取最新版本。您可以在 SB3 文档中找到迁移指南。 本节的目的是帮助您进行强化学习实验。它涵盖了有关 RL 的一般建议&#xff08;从哪里开始、选择哪种算法、如何评估算法等&#xff09;&#xff0c;以及使用自…

嵌入式QT (Qt 信号与槽)

一、Qt 信号与槽机制 因为有了信号与槽的编程机制&#xff0c;在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。 信号&#xff08;Signal&#xff09;就是在特定情况下被发射的事件。 GUI 程序设计的主要内容就是对界面上各组件的信号的响应&#xff0c;只需要知道…

MySQL_第05章_排序与分页

第05章_排序与分页 讲师&#xff1a;尚硅谷 - 宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a; http://www.atguigu.com 1. 排序数据 1.1 排序规则 使用 ORDER BY 子句排序 ASC&#xff08;ascend&#xff09;: 升序 DESC&#xff08;desc…

物流管理APP软件开发公司 让货运变得更简单

随着互联网技术的发展&#xff0c;人们的生活方式也发生了很大的变化&#xff0c;移动互联网如今已经深入到生活的方方面面&#xff0c;就连物流运输行业也开始涌现出各种货运物流管理APP软件&#xff0c;让整个物流管理过程更加简单。下面我们一起来看一下为什么越来越多的运输…

数据库实验 | 第4关:修改多个数据表的存储过程

任务描述 本关任务&#xff1a; 图书管理数据库有读者reader图书book借阅数据表 读者表reader有读者证号dzzh、姓名xm、性别xb、身份sf、电话号码dhhm字段 图书表book有条形码txm、书名sm、分类号flh,作者zz,出版社cbs,出版日期cbrq,售价sj,典藏类别dclb,在库zk,币种bz字段 …

【DEBUG】错误手册集

文章目录 1.sshd启动报错,无法绑定端口2.克隆后的虚拟机可以联网&#xff0c;但是Xshell连接失败的解决办法 1.sshd启动报错,无法绑定端口 (1) 首先排查是否有端口占用&#xff08;没有发现问题&#xff09; netstat -ano | grep sshd(2) 查看 message 日志存在如下错误信息&…

MySQL数据库,联合查询

目录 1. 联合查询 1.1 内查询 1.2 外查询 1.3 自连接 1.4 子查询 1.5 合并查询 1. 联合查询 联合查询&#xff0c;简单的来讲就是多个表联合起来进行查询。这样的查询在我们实际的开发中会用到很多&#xff0c;因此会用笛卡尔积的概念。 啥是笛卡尔积&#xff1f;两张表…

深度学习 -- 张量操作与线性回归 张量的数学运算以及用张量构建线性回归模型

前言 这篇博客继承前篇博客的内容&#xff0c;将对张量的操作进行阐述&#xff0c;同时在理解张量的一些数学的基础上&#xff0c;配合机器学习的理论&#xff0c;在pytorch环境中进行一元线性回归模型的构建。 张量的拼接与切分 torch.cat() 功能&#xff1a;将张量按维度d…