Go微服务接口:Gin
原创大约 13 分钟
Gin-GRPC微服务技术架构

GRPC作为服务提供者,负责提供所需服务,以及与数据库的交互。
Protobuf实现通信数据的序列化和反序列化。
Gin不仅作为服务接口,还负责业务逻辑的实现。除此外,它还需要做下面这些工作。
表单验证
:验证请求数据是否合法。安全访问
:保证请求的安全与用户访问的便利,例如,JWT、限流
、熔断
、降级
等措施。组件交互
:例如,需要调用外部组件实现短信发送、支付、消息推送等。服务跨域
:解决服务访问中的跨域问题。请求拦截
:拒绝没有权限的访问,以及某些需要做统一处理的服务,例如请求链路的解密。负载均衡
:实现多个服务的负载均衡访问。
总体来说,Gin-GRPC
架构下的简单微服务两层架构,头重脚轻:Gin服务的职责过于繁重,需要再做拆分。
环境和脚本
> go get -u github.com/anaskhan96/go-password-encoder
> go get -u github.com/uber-go/zap
> go get -u github.com/spf13/viper
> go get -u github.com/dgrijalva/jwt-go
> go get -u github.com/mojocn/base64Captcha
proto(Gin和GRPC共用)
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = "./;proto";
// 用户服务接口
service UserService {
// 获取用户的列表
rpc List(Page) returns (UserList) {}
// 获取用户详情
rpc InfoByName(UsernameRequest) returns (UserInfo) {}
rpc InfoById(UseridInfo) returns (UserInfo) {}
// 注册用户
rpc Register(CreateUserInfo) returns (UserInfo) {}
// 更新用户
rpc Update(UpdateUserInfo) returns (google.protobuf.Empty) {}
// 验证用户密码
rpc Password(PasswordInfo) returns (ValidateResult) {}
}
// 分页信息
message Page {
uint32 page = 1;// 页码
uint32 size = 2;// 每页条数
}
message UsernameRequest {
string username = 1;
}
message UseridInfo {
uint32 id = 1;
}
message CreateUserInfo {
string username = 1;
string password = 2;
string nickname = 3;
string createtime = 4;
}
message UpdateUserInfo {
uint32 id = 1;
string nickname = 2;
string password = 3;
uint64 birthday = 4;
uint32 gender = 5;
uint32 role = 6;
}
message PasswordInfo {
string password = 1;
string encrypt = 2;
}
message UserList {
uint32 total = 1;
repeated UserInfo data = 2;
}
message UserInfo {
uint32 id = 1;
string username = 2;
string password = 3;
string nickname = 4;
string email = 5;
uint64 birthday = 6;
uint32 gender = 7;
uint32 role = 8;
string createtime = 9;
bool isdeleted = 10;
}
message ValidateResult {
bool result = 1;
uint32 errcode = 2;
string message = 3;
}
验证表单
package forms
type PasswordLoginForm struct {
// 自定义validator做手机号码验证
Username string `form:"username" json:"username" binding:"required,mobile,min=11,max=11"`
Password string `form:"password" json:"password" binding:"required,min=6,max=32"`
Captcha string `form:"captcha" json:"captcha" binding:"required,min=5,max=5,number"`
CaptchaId string `form:"captchaid" json:"captchaid" binding:"required"`
}
type RegisterForm struct {
Username string `form:"username" json:"username" binding:"required,mobile,min=11,max=11"`
Password string `form:"password" json:"password" binding:"required,min=6,max=32"`
Code string `form:"code" json:"code" binding:"required,min=5,max=5"`
}
type UpdateUserForm struct {
Nickname string `form:"nickname" json:"nickname" binding:"required,min=2,max=16"`
Email string `form:"email" json:"email" binding:"required,email"`
Gender string `form:"gender" json:"gender" binding:"required,oneof=0 1"`
Birthday string `form:"birthday" json:"birthday" binding:"required,datetime=2006-01-02"`
}
配置结构体
package config
type UserSrv struct {
Name string `mapstructure:"name" json:"name"`
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
type LoggerConfig struct {
Level string `mapstructure:"level" json:"level"`
Path string `mapstructure:"path" json:"path"`
}
type ServerConfig struct {
Env string `mapstructure:"env" json:"env"`
Name string `mapstructure:"name" json:"name"`
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
UserSrvInfo UserSrv `mapstructure:"usersrv" json:"usersrv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
AliSmsInfo AliSmsConfig `mapstructure:"sms" json:"sms"`
RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`
LoggerInfo LoggerConfig `mapstructure:"logger" json:"logger"`
}
全局变量
package global
import (
ut "github.com/go-playground/universal-translator"
"gomicroservice/userapi/config"
"gomicroservice/userapi/proto"
)
const (
ENVIRONMENT_DEV = "dev"
ENVIRONMENT_PRO = "pro"
)
var (
ServerConfig *config.ServerConfig = new(config.ServerConfig)
UserSrvClient proto.UserServiceClient
Translator ut.Translator
ExpiredTime int64 = 3600
)
package response
type UserResponse struct {
Id int32 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Birthday string `json:"birthday"`
Gender int32 `json:"gender"`
Role int32 `json:"role"`
Createtime string `json:"createtime"`
Isdeleted bool `json:"isdeleted"`
}
中间件
权限验证
package middleware
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
)
// IsAdminAuth 验证是否管理员用户
func IsAdminAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
claims, _ := ctx.Get("claims")
currentUser := claims.(*CustomClaims)
if currentUser.AuthorityId != 2 {
zap.S().Infof("[用户%d]无操作权限", currentUser.ID)
ctx.JSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": "操作不允许",
"data": gin.H{},
})
ctx.Abort()
return
}
ctx.Next()
}
}
跨域请求
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// Cors 处理跨域请求
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, DELETE, PATCH, PUT")
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)
} else {
context.Next()
}
}
}
JWT
package middleware
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"gomicroservice/userapi/global"
"net/http"
"time"
)
type CustomClaims struct {
ID uint
Nickname string
AuthorityId uint
jwt.StandardClaims
}
type JWT struct {
SigningKey []byte
}
var (
TokenExpired = errors.New("token is expired")
TokenNotValidYet = errors.New("token not active yet")
TokenMalformed = errors.New("that's not even a token")
TokenInvalid = errors.New("couldn't handle this token")
)
// JWTAuth JWT鉴权登录
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 这里jwt鉴权取头部信息 x-token 登录时回返回token信息
// 前端需要把token存储到cookie或者本地localStorage中
// 不过需要跟后端约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"msg": "请先登录",
"data": gin.H{},
})
c.Abort()
return
}
j := NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if errors.Is(err, TokenExpired) {
if errors.Is(err, TokenExpired) {
c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"msg": "授权已过期",
"data": gin.H{},
})
c.Abort()
return
}
}
c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"msg": "未登录",
"data": gin.H{},
})
c.Abort()
return
}
// 将登录用户的id信息保存到请求中,后续auth
c.Set("claims", claims)
c.Set("userId", claims.ID)
c.Next()
}
}
// NewJWT 初始化JWT
func NewJWT() *JWT {
return &JWT{
[]byte(global.ServerConfig.JWTInfo.SigningKey), // 可以设置过期时间
}
}
// CreateToken 创建token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// ParseToken 解析token
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
return j.SigningKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// Token is expired
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if token != nil {
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
} else {
return nil, TokenInvalid
}
}
// RefreshToken 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
return j.CreateToken(*claims)
}
return "", TokenInvalid
}
初始化日志
package initialize
import (
"go.uber.org/zap"
"gomicroservice/userapi/utils"
)
// InitialLogger 初始化日志组件
func InitialLogger() {
logger, _ := utils.GlobalLogger()
zap.ReplaceGlobals(logger)
}
package utils
import (
"go.uber.org/zap"
)
// GlobalLogger 日志工具
func GlobalLogger() (*zap.Logger, error) {
// 生产环境会打印JSON格式的日志,而开发环境不会
cfg := zap.NewProductionConfig()
//cfg := zap.NewDevelopmentConfig()
cfg.OutputPaths = []string{
"/Users/bear/home/work/logs/user_api.log", // 输出到文件
//"stderr", // 输出到标准错误流
"stdout", // 输出到控制台
}
return cfg.Build()
}
初始化配置
package initialize
import (
"encoding/json"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"gomicroservice/userapi/global"
)
func InitialConfiguration(env string) {
// 从本地配置文件中获取配置
vip := viper.New()
switch env {
case "dev":
vip.SetConfigFile("userapi/config-dev.yaml")
case "pro":
vip.SetConfigFile("userapi/config-pro.yaml")
default:
vip.SetConfigFile("userapi/config-dev.yaml")
}
if err := vip.ReadInConfig(); err != nil {
zap.S().Panicf("读取配置文件失败:%s", err.Error())
}
// 解析出配置文件
if err := vip.Unmarshal(global.NacosConfig); err != nil {
zap.S().Panicf("解析配置文件失败:%s", err.Error())
}
zap.S().Infof("配置文件信息:%v", global.NacosConfig)
// 动态监控变化
vip.WatchConfig()
vip.OnConfigChange(func(in fsnotify.Event) {
_ = vip.ReadInConfig()
_ = vip.Unmarshal(global.NacosConfig)
zap.S().Infof("配置文件修改了:%v", global.NacosConfig)
})
}
初始化路由
package initialize
import (
"github.com/gin-gonic/gin"
"gomicroservice/userapi/middleware"
"gomicroservice/userapi/router"
)
// InitialRouters 路由初始化
func InitialRouters() *gin.Engine {
roots := gin.Default()
// 处理跨域请求
roots.Use(middleware.Cors())
// 路由前缀
ApiGroup := roots.Group("/v1")
// 系统路由
router.InitSystemRouter(ApiGroup)
// 用户路由
router.InitUserRouter(ApiGroup)
return roots
}
初始化系统相关路由。
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gomicroservice/userapi/api"
)
// InitSystemRouter 初始化系统路由
func InitSystemRouter(Router *gin.RouterGroup) {
zap.S().Infof("系统相关路由初始化")
SystemRouter := Router.Group("/system")
{
// 图形验证码
SystemRouter.GET("/captcha", api.Captcha)
//SystemRouter.GET("/verify", api.VerifyCaptcha)
// 短信
SystemRouter.POST("/sms", api.SendSms)
}
}
初始化用户相关路由。
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gomicroservice/userapi/api"
"gomicroservice/userapi/middleware"
)
// InitUserRouter 初始化用户路由
func InitUserRouter(router *gin.RouterGroup) {
zap.S().Infof("用户相关路由初始化")
userRouter := router.Group("/users")
{
userRouter.POST("/register", api.Register)
userRouter.POST("/login/password", api.PasswordLogin)
// 加入鉴权中间件,需要注意中间件的出现顺序
userRouter.GET("/list", middleware.JWTAuth(), middleware.IsAdminAuth(), api.List)
userRouter.GET("/infoById", middleware.JWTAuth(), api.InfoById)
userRouter.GET("/infoByName", middleware.JWTAuth(), api.InfoByName)
userRouter.POST("/update", middleware.JWTAuth(), api.Update)
userRouter.POST("/password", middleware.JWTAuth(), api.Password)
}
}
初始化翻译器
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"
"github.com/go-playground/validator/v10"
entranslations "github.com/go-playground/validator/v10/translations/en"
zhtranslations "github.com/go-playground/validator/v10/translations/zh"
"go.uber.org/zap"
"gomicroservice/userapi/global"
"reflect"
"strings"
)
// InitTranslator 验证器注册翻译器
func InitTranslator(locale string) (err error) {
// 修改默认的validator引擎属性
if validate, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json中tag的函数
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 创建中文翻译器
zhCn := zh.New()
// 创建英文翻译器
enTrans := en.New()
// 第一个参数是备用语言环境,后面是应该支持的语言环境
uni := ut.New(enTrans, zhCn, enTrans)
if global.Translator, ok = uni.GetTranslator("zh"); ok {
switch locale {
case "en":
err = entranslations.RegisterDefaultTranslations(validate, global.Translator)
if err != nil {
zap.S().Panicf("英文翻译器注册失败: %v", err)
return
}
case "zh":
err = zhtranslations.RegisterDefaultTranslations(validate, global.Translator)
if err != nil {
zap.S().Panicf("中文翻译器注册失败: %v", err)
return
}
default:
err = entranslations.RegisterDefaultTranslations(validate, global.Translator)
if err != nil {
zap.S().Panicf("英文翻译器注册失败: %v", err)
return
}
}
zap.S().Infof("翻译器注册成功")
} else {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
}
return err
}
初始化连接GRPC服务
package initialize
import (
"fmt"
"go.uber.org/zap"
"gomicroservice/userapi/global"
"gomicroservice/userapi/proto"
"gomicroservice/userapi/utils/otgrpc"
"google.golang.org/grpc"
)
// InitialServerConnection 初始化服务连接
func InitialServerConnection() {
// 连接用户grpc服务
conn, err := grpc.Dial(fmt.Sprintf("%s:%d",
global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port),
grpc.WithInsecure(),
)
if err != nil {
zap.S().Panicf("初始化服务连接失败:%s", err.Error())
}
// 调用接口
global.UserSrvClient = proto.NewUserServiceClient(conn)
}
启动代码
package main
import (
"fmt"
"github.com/gin-gonic/gin/binding"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"gomicroservice/userapi/global"
"gomicroservice/userapi/initialize"
"gomicroservice/userapi/utils"
myValidator "gomicroservice/userapi/validator"
)
/**
* api:服务接口
* config:全局配置
* forms:表单验证struct
* global:全局变量
* initialize:初始化相关
* middleware:中间件服务调用
* proto:proto文件及其生成的go源码
* router:路由分组
* tmp:临时文件,由nacos自动生成
* utils:工具类
* validator:自定义验证器
*/
// 用户服务接口
func main() {
/*
* S()可以获取一个全局的sugar,可以自己设置一个全局的logger
* 以后在任何地方使用时,只需要调用zap.S()即可
* 如果是在生产环境下的配置,也就是NewProductionConfig,那么Debug将不起作用,但NewDevelopmentConfig可以输出Debug日志
* 日志分级:debug,info,warn,error,fetal
*/
// 1. 初始化日志
initialize.InitialLogger()
// 2. 初始化配置
initialize.InitialConfiguration(global.ENVIRONMENT_DEV)
// 3. 初始化路由
roots := initialize.InitialRouters()
// 4. 初始化翻译器
if err := initialize.InitTranslator("zh"); err != nil {
zap.S().Panicf("初始化翻译器失败:%s", err.Error())
return
}
// 5. 初始化和grpc服务的连接
initialize.InitialServerConnection()
// 生产环境才允许动态获取接口
if global.ENVIRONMENT_PRO == global.ServerConfig.Env {
port, err := utils.GetAvailablePort()
if err == nil {
global.ServerConfig.Port = port
}
}
// 6. 注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", myValidator.ValidateMobile)
_ = v.RegisterTranslation("mobile", global.Translator, func(ut ut.Translator) error {
return ut.Add("mobile", "{0}手机号码格式不正确", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
} else {
zap.S().Panicf("注册验证器失败")
}
// 7. 运行gin服务
zap.S().Infof("启动用户gin接口:%d", global.ServerConfig.Port)
if err := roots.SetTrustedProxies([]string{global.ServerConfig.Host}); err != nil {
zap.S().Panicf("启动用户gin接口失败:%s", err.Error())
return
}
if err := roots.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.S().Panicf("启动用户gin接口失败:%s", err.Error())
return
}
}
服务接口(部分)
用户相关接口。
package api
import (
"context"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"gomicroservice/userapi/forms"
"gomicroservice/userapi/global"
"gomicroservice/userapi/global/response"
"gomicroservice/userapi/middleware"
"gomicroservice/userapi/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
"strconv"
"strings"
"time"
)
func ReplacePrefixName(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
res := strings.Index(field, ".") + 1
// temp的内容为json字符串
temp := field[res:]
switch temp {
case "username":
rsp["用户名"] = err
case "password":
rsp["密码"] = err
case "repassword":
rsp["确认密码"] = err
case "email":
rsp["邮箱"] = err
case "phone":
rsp["手机号"] = err
case "age":
rsp["年龄"] = err
case "gender":
rsp["性别"] = err
}
}
return rsp
}
// TranslateGrpcErrorCode2HttpCode 将grpc的状态码转换为http的状态码
func TranslateGrpcErrorCode2HttpCode(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{
"code": http.StatusNotFound,
"message": "数据不存在",
"data": gin.H{},
})
case codes.Internal:
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "内部错误",
"data": gin.H{},
})
case codes.InvalidArgument:
ctx.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": "参数无效",
"data": gin.H{},
})
case codes.Unavailable:
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "服务不可用",
"data": gin.H{},
})
default:
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "其他错误",
"data": gin.H{},
})
}
return
}
}
}
func HandleValidateError(ctx *gin.Context, err error) {
zap.S().Infof("表单验证出错:%s", err.Error())
errs, ok := err.(validator.ValidationErrors)
if !ok {
//TranslateGrpcErrorCode2HttpCode(err, ctx)
//return
ctx.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"msg": err.Error(),
"data": gin.H{},
})
return
}
ctx.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"msg": ReplacePrefixName(errs.Translate(global.Translator)),
"data": gin.H{},
})
}
// List 获取用户列表
func List(ctx *gin.Context) {
var page = ctx.DefaultQuery("page", "1")
pageInt, _ := strconv.Atoi(page)
var size = ctx.DefaultQuery("size", "10")
sizeInt, _ := strconv.Atoi(size)
zap.S().Infof("获取用户列表页:page=%s, size=%s", page, size)
claims, _ := ctx.Get("claims")
visitor := claims.(*middleware.CustomClaims)
zap.S().Infof("访问者:[用户%d]", visitor.ID)
// 调用接口
users, err2 := global.UserSrvClient.List(context.Background(),
&proto.Page{
Page: uint32(pageInt),
Size: uint32(sizeInt),
})
if err2 != nil {
zap.S().Infof("调用【获取用户列表服务】失败:%s", err2.Error())
TranslateGrpcErrorCode2HttpCode(err2, ctx)
return
}
result := make([]interface{}, 0)
for _, value := range users.Data {
user := response.UserResponse{
Id: int32(value.Id),
Username: value.Username,
Password: value.Password,
Nickname: value.Nickname,
//Birthday: time.Time(time.Unix(int64(value.Birthday), 0)),
//Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),
Birthday: time.Time(time.Unix(int64(value.Birthday), 0)).Format("2006-01-02"),
Gender: int32(value.Gender),
Role: int32(value.Role),
Email: value.Email,
Isdeleted: value.Isdeleted,
Createtime: value.Createtime,
}
result = append(result, user)
}
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "获取用户列表成功",
"data": gin.H{
"size": users.Total,
"data": result,
},
})
}
// InfoById 根据用户名获取用户信息
func InfoById(ctx *gin.Context) {
// TODO
}
// InfoByName 根据用户id获取用户信息
func InfoByName(ctx *gin.Context) {
// TODO
}
// Register 用户注册
func Register(ctx *gin.Context) {
// 表单验证
registerForm := forms.RegisterForm{}
zap.S().Infof("用户注册参数:username=%s, password=%s, code=%s",
registerForm.Username, registerForm.Password, registerForm.Code)
if err1 := ctx.ShouldBind(®isterForm); err1 != nil {
HandleValidateError(ctx, err1)
return
}
// 注册的逻辑
// 先查询用户名是否已存在
user, err3 := global.UserSrvClient.InfoByName(context.Background(), &proto.UsernameRequest{
Username: registerForm.Username,
})
if err3 == nil || user != nil {
zap.S().Infof("用户名已存在")
ctx.JSON(http.StatusConflict, gin.H{
"code": http.StatusConflict,
"msg": "用户名已存在",
"data": gin.H{},
})
return
}
user2, err4 := global.UserSrvClient.Register(context.Background(), &proto.CreateUserInfo{
Username: registerForm.Username,
Password: registerForm.Password,
Nickname: registerForm.Username, // 昵称默认就是手机号
Createtime: time.Now().Format("2006-01-02 15:04:05"),
})
if err4 != nil {
zap.S().Infof("用户注册失败:%s", err4.Error())
TranslateGrpcErrorCode2HttpCode(err4, ctx)
return
}
if user2.Id != 0 {
zap.S().Infof("用户注册成功")
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "用户注册成功",
"data": gin.H{},
})
} else {
zap.S().Infof("用户注册失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "用户注册失败",
"data": gin.H{},
})
}
}
// PasswordLogin 用户名密码登录
func PasswordLogin(ctx *gin.Context) {
// 表单验证
passwordLoginForm := forms.PasswordLoginForm{}
zap.S().Infof("用户登录参数:username=%s, password=%s",
passwordLoginForm.Username, passwordLoginForm.Password)
if err1 := ctx.ShouldBind(&passwordLoginForm); err1 != nil {
HandleValidateError(ctx, err1)
return
}
// 图形验证码验证
if !store.Verify(passwordLoginForm.CaptchaId, passwordLoginForm.Captcha, true) {
zap.S().Infof("图形验证码验证失败")
ctx.JSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": "验证失败",
"data": gin.H{},
})
return
}
// 登录的逻辑
user, err2 := global.UserSrvClient.InfoByName(context.Background(), &proto.UsernameRequest{
Username: passwordLoginForm.Username,
})
if err2 != nil {
ctx.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "用户不存在",
"data": gin.H{},
})
return
}
// 检查密码
rsp, err3 := global.UserSrvClient.Password(context.Background(), &proto.PasswordInfo{
Password: passwordLoginForm.Password,
Encrypt: user.Password,
})
if err3 != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "登录失败",
"data": gin.H{},
})
} else {
if rsp.Result {
// 集成JWT
newJWT := middleware.NewJWT()
claims := middleware.CustomClaims{
ID: uint(user.Id),
Nickname: user.Nickname,
AuthorityId: uint(user.Role),
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix(), // 签名的生效时间
ExpiresAt: time.Now().Unix() + 60*60*24*7, // 签名的失效时间
Issuer: "goms",
},
}
token, err4 := newJWT.CreateToken(claims)
if err4 != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "生成token失败",
"data": gin.H{},
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "登录成功",
"data": gin.H{
"id": user.Id,
"nickname": user.Nickname,
"token": token,
"expire": strconv.Itoa(int(claims.ExpiresAt) * 1000),
},
})
} else {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "密码错误",
"data": gin.H{},
})
}
}
}
// Update 更新用户
func Update(ctx *gin.Context) {
// TODO
}
// Password 校验密码
func Password(ctx *gin.Context) {
// TODO
}
系统相关接口。
package api
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"go.uber.org/zap"
"gomicroservice/userapi/forms"
"gomicroservice/userapi/global"
"math/rand"
"net/http"
"strings"
"time"
)
var store = base64Captcha.DefaultMemStore
// Captcha 生成图形验证码
func Captcha(ctx *gin.Context) {
driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)
cap := base64Captcha.NewCaptcha(driver, store)
id, base64, answer, err := cap.Generate()
if err != nil {
zap.S().Errorf("生成验证码错误:%s", err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "图形验证码获取失败",
"data": gin.H{},
})
return
}
// 返回图形验证码id和base64
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "图形验证码获取成功",
"data": gin.H{
"captid": id,
"base64": base64,
"answer": answer,
},
})
}
配置文件
env: "env"
name: "user-api"
host: "127.0.0.1"
port: 8080
usersrv:
name: "usersrv"
host: "127.0.0.1"
port: 9090
jwt:
key: "......"
logger:
level: "info"
path: "/home/work/logs/go/user-api.log"
感谢支持
更多内容,请移步《超级个体》。