项目架构

goweb
├── bin
├── pkg
└── src
    ├── config
    │   ├── config.go
    │   └── config.yaml
    ├── go.mod
    ├── go.sum
    ├── logger
    │   ├── go.mod
    │   ├── go.sum
    │   ├── logger.go
    │   └── zap.go
    ├── main.go
    ├── middleware
    │   ├── jwt_auth_middleware.go
    │   └── weblog_middleware.go
    ├── model
    │   ├── conn.go
    │   └── user.go
    ├── module
    │   ├── login_router.go
    │   └── user
    │       ├── controller.go
    │       └── router.go
    └── service
        ├── register_api.go
        └── user_router.go

说明

config:项目配置文件

logger:项目日志的配置

middleware:项目用到的中间件

model:MySQL数据库连接以及数据库表对应的结构体

module:业务核心,实现业务逻辑以及路由

service:路由的入口, 注册路由

main.go:项目启动入口

项目所选第三方库

数据库ORM

  • gorm

JSON-API Web框架

  • Gin

JWT 认证

  • gin-jwt

日志库

  • zap

项目解析

config/config.yaml

说明:项目配置文件

log:
  # 控制台日志参数
  enableConsole: true
  consoleJSONFormat: true
  consoleLevel: Debug
  # 文件日志参数
  enableFile: true
  fileJSONFormat: false
  fileLevel: Debug
  # 文件存放路径
  fileLocation: /home/www-data/logs/base-log.log
  maxAge: 28 # 最大天数
  maxSize: 100 # 文件最大容量
  compress: true # 是否压缩
  fileExport: /home/www-data/logs

# 项目启动地址
webapi:
  uri: 0.0.0.0:8080

# 数据库连接配置
mysqlnd:
  username: root
  password: 123123
  host: 127.0.0.1
  port: 3306
  database: kcm

config/config.go

说明:解析配置文件的Go文件

package config

import (
	"gopkg.in/yaml.v3"
	"io/ioutil"
	"logger"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

var configFile []byte

type Config struct {
	Log struct {
		EnableConsole     bool   `yaml:"enableConsole"`
		ConsoleLevel      string `yaml:"consoleLevel"`
		ConsoleJSONFormat bool   `yaml:"consoleJSONFormat"`
		EnableFile        bool   `yaml:"enableFile"`
		FileJSONFormat    bool   `yaml:"fileJSONFormat"`
		FileLevel         string `yaml:"fileLevel"`
		FileLocation      string `yaml:"fileLocation"`
		MaxAge            int    `yaml:"maxAge"`
		MaxSize           int    `yaml:"maxSize"`
		Compress          bool   `yaml:"compress"`
		FileExport        string `yaml:"fileExport"`
	}

	Webapi struct {
		Uri string `yaml:"uri"`
	}

	Mysqlnd struct {
		Username string `yaml:"username"`
		Password string `yaml:"password"`
		Host     string `yaml:"host"`
		Port     string `yaml:"port"`
		Database string `yaml:"database"`
	}
}

func init() {
	var err error
	var configFilePath = filepath.Join(getCurrentAbPathByCaller(), "config.yaml")
	configFile, err = ioutil.ReadFile(configFilePath)
	if err != nil {
		logger.Fatalf("Read config yaml file err %v", err)
	}
}

func GetChannelConfig() (e *Config, err error) {
	err = yaml.Unmarshal(configFile, &e)
	return e, err
}

// 获取程序运行路径(go build)
func getCurrentDirectory() string {
	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		logger.Errorf("Get current path err %v", err)
	}
	return strings.Replace(dir, "\\", "/", -1)
}

// 获取当前执行文件绝对路径(go run)
func getCurrentAbPathByCaller() string {
	var abPath string
	_, filename, _, ok := runtime.Caller(0)
	if ok {
		abPath = path.Dir(filename)
	}
	return abPath
}
  1. Config结构体中结构体,对应yaml文件中的一级配置
  2. 结构体中的变量对应yaml文件中的二级配置
  3. 读取config.yaml文件,需要使用绝对路径

logger/logger.go

说明:项目日志相关,日志等级如下,从上而下依次递增

type Logger interface {
	Debugf(format string, args ...interface{})

	Infof(format string, args ...interface{})

	Warnf(format string, args ...interface{})

	Errorf(format string, args ...interface{})

	Fatalf(format string, args ...interface{})

	Panicf(format string, args ...interface{})
}

middleware/jwt_auth_middleware.go

说明:JWT登录验证的文件

package middleware

import (
	"crypto/md5"
	"fmt"
	jwt "github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
	"logger"
	"main/module/user"
	"time"
)

type JwtUser struct {
	UserName string
}

var identityKey = "id"

type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func AuthMiddleWare() *jwt.GinJWTMiddleware {
	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
		// 中间件名称
		Realm: "gin-jwt",
		Key:   []byte("secret key"),
		// token 过期时间
		Timeout: 24 * time.Hour,
		// token 刷新最大时间
		MaxRefresh: 24 * time.Hour,
		// 身份验证的 key 值
		IdentityKey: identityKey,
		// 登录期间的回调的函数
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if v, ok := data.(JwtUser); ok {
				return jwt.MapClaims{
					identityKey: v.UserName,
				}
			}
			return jwt.MapClaims{}
		},
		// 解析并设置用户身份信息
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
			return JwtUser{
				UserName: claims[identityKey].(string),
			}
		},
		// 根据登录信息对用户进行身份验证的回调函数
		Authenticator: func(c *gin.Context) (interface{}, error) {
			var loginVars login
			if err := c.ShouldBind(&loginVars); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			userName := loginVars.Username
			password := loginVars.Password
			res := user.SelectByUsername(userName)
			if res != nil && MD5(password) == res.Password {
				return JwtUser{
					UserName: userName,
				}, nil
			}
			return nil, jwt.ErrFailedAuthentication
		},
		// 接收用户信息并编写授权规则
		Authorizator: func(data interface{}, c *gin.Context) bool {
			if _, ok := data.(JwtUser); ok {
				return true
			}
			return false
		},
		// 自定义处理未进行授权的逻辑
		Unauthorized: func(c *gin.Context, code int, message string) {
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
		},
		// token 检索模式,用于提取 token,默认值为 header:Authorization
		TokenLookup:   "header: Authorization, query: token, cookie: jwt",
		TokenHeadName: "Bearer",
		TimeFunc:      time.Now,
	})

	if err != nil {
		logger.Debugf("JWT err: %v" + err.Error())
	}
	// https://jwt.io/ 解析
	return authMiddleware
}

func MD5(str string) string {
	data := []byte(str)
	has := md5.Sum(data)
	md5str := fmt.Sprintf("%x", has) //将[]byte转成16进制
	return md5str
}
Authenticator

middleware/weblog_middleware.go

说明:增加日志输出的数据及格式

package middleware

import (
	"github.com/gin-gonic/gin"
	"logger"
	"time"
)

func GinWebLog() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 开始时间
		startTime := time.Now()
		// 处理请求
		c.Next()
		// 结束时间
		endTime := time.Now()
		// 执行时长
		latencyTime := endTime.Sub(startTime)
		// 请求方式
		reqMethod := c.Request.Method
		// 请求路由
		reqUri := c.Request.RequestURI
		// 状态码
		statusCode := c.Writer.Status()
		// 请求IP
		clientIP := c.ClientIP()
		// 日志格式
		logger.Infof("| %3d | %13v | %15s | %s | %s |",
			statusCode,
			latencyTime,
			clientIP,
			reqMethod,
			reqUri,
		)
	}
}

效果如下:

| 200 |    2.803787ms |   172.17.100.16 | GET | /getUserByPage |

model/conn.go

说明:Gorm数据库连接

package model

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"logger"
	"main/config"
)

func MysqlConn() *gorm.DB {
	configBase, err := config.GetChannelConfig()
	if err != nil {
		logger.Fatalf("Get config failed! err: #%v", err)
		return nil
	}
	username := configBase.Mysqlnd.Username
	password := configBase.Mysqlnd.Password
	host := configBase.Mysqlnd.Host
	port := configBase.Mysqlnd.Port
	dbname := configBase.Mysqlnd.Database
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
	// 连接 mysql
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		logger.Fatalf("MySQL connect failed! err: #%v", err)
		return nil
	}
	// 设置数据库连接池参数
	sqlDB, _ := db.DB()
	// 设置数据库连接池最大连接数
	sqlDB.SetMaxOpenConns(100)
	// 连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于20,超出的连接会被连接池关闭
	sqlDB.SetMaxIdleConns(20)
	return db
}

var Db *gorm.DB

func init() {
	Db = MysqlConn()
}

model/user.go

说明:数据库用户表对应的结构体

package model

type User struct {
	UserId         int8   `gorm:"column:user_id;AUTO_INCREMENT;comment:用户ID" json:"user_id"`
	UserName       string `gorm:"column:username;comment:用户名" json:"username"`
	Password       string `gorm:"column:password;comment:密码" json:"password"`
	RoleId         int8   `gorm:"column:role_id;comment:角色ID" json:"role_id"`
	Status         string `gorm:"column:status;comment:用户是否禁用标志位,0为禁用,1为启用" json:"status"`
	Name           string `gorm:"column:name;comment:用户真实姓名" json:"name"`
	CreateByUserId int8   `gorm:"column:create_by_user_id;comment:创建者ID" json:"create_by_user_id"`
}

// TableName 自定义表名
func (User) TableName() string {
	return "users"
}
user_id

module/user/controller.go

说明:用户相关的业务处理逻辑,定义各种操作数据库的接口

package user

import (
	"errors"
	"gorm.io/gorm"
	"logger"
)

import "main/model"

func SelectByUsername(username string) *model.User {
	db := model.Db
	u := model.User{}
	res := db.Where("username = ?", username).First(&u)
	if errors.Is(res.Error, gorm.ErrRecordNotFound) {
		logger.Debugf("Select by username err:" + "未查找到相关数据")
		return nil
	}
	return &u
}

func GetUserByPage(page, pageSize int) (int64, []*model.User) {
	db := model.Db
	var users []*model.User
	var total int64
	db.Model(model.User{}).Count(&total)
	res := db.Limit(pageSize).Offset((page - 1) * pageSize).Find(&users)
	if errors.Is(res.Error, gorm.ErrRecordNotFound) {
		logger.Debugf("Get user by page err:" + "未查找到相关数据")
		return 0, nil
	}
	return total, users
}

module/user/router.go

说明:定义Web请求的接口,接受Restful Api请求,调用controller函数进行处理,并返回结果

package user

import (
	"github.com/gin-gonic/gin"
)

func GetUserByPageHandler(c *gin.Context) {
	type Param struct {
		Page     int `form:"page" json:"page" binding:"required"`
		PageSize int `form:"pageSize" json:"pageSize" binding:"required"`
	}
	var param Param

	if err := c.ShouldBind(&param); err != nil {
		c.JSON(400, gin.H{
			"status_code": 400,
			"message":     "参数错误",
		})
		return
	}
	total, data := GetUserByPage(param.Page, param.PageSize)
	if data == nil {
		c.JSON(200, gin.H{
			"status_code": 200,
			"message":     "获取用户失败",
		})
		return
	}
	c.JSON(200, gin.H{
		"status_code": 200,
		"message":     "获取用户成功",
		"total":       total,
		"data":        data,
	})
}

module/login_router.go

说明:用户登录的接口

package module

import (
	"github.com/gin-gonic/gin"
	"main/middleware"
)

func LoginHandler(c *gin.Context) {
	middleware.AuthMiddleWare().LoginHandler(c)
}

service/user_router.go

说明:注册用户相关的路由

package service

import (
	"github.com/gin-gonic/gin"
	"main/middleware"
	"main/module/user"
)

func userRouter(e *gin.Engine) {
	authMiddleware := middleware.AuthMiddleWare()
	e.Use(authMiddleware.MiddlewareFunc())
	{
		e.GET("/getUserByPage", user.GetUserByPageHandler)
	}
}
e.Use(authMiddleware.MiddlewareFunc())

service/register_api.go

说明:注册全局路由,初始化Gin

package service

import (
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"logger"
	"main/config"
	"main/middleware"
	"main/module"
)

type Option func(*gin.Engine)

var options []Option

// Include 注册app的路由配置
func Include(opts ...Option) {
	options = append(options, opts...)
}

// Init 初始化
func Init() *gin.Engine {
	r := gin.New()
	// https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies
	err := r.SetTrustedProxies(nil)
	if err != nil {
		logger.Fatalf("Gin set trusted proxies failed! err: #%v", err)
	}
	r.Use(middleware.GinWebLog())
	r.Use(gin.Recovery())
	swagHandler := ginSwagger.WrapHandler(swaggerFiles.Handler)
	r.GET("/swagger/*any", swagHandler)

	authMiddleware := middleware.AuthMiddleWare()

	r.POST("/login", module.LoginHandler)

	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	})

	Include(userRouter)

	for _, opt := range options {
		opt(r)
	}
	return r
}

func StartApi() {
	// 初始化路由
	r := Init()
	configBase, err := config.GetChannelConfig()
	if err != nil {
		logger.Fatalf("Get config failed! err: #%v", err)
	}
	if err := r.Run(configBase.Webapi.Uri); err != nil {
		logger.Fatalf("Run web server failed! err: #%v", err)
	}
}

main.go

说明:项目启动入口,初始化全局日志参数配置

package main

import (
	"logger"
	"main/config"
	"main/service"
)

func main() {
	service.StartApi()
}

func init() {
	configBase, err := config.GetChannelConfig()
	if err != nil {
		logger.Fatalf("Get channel config failed! err: %v", err)
	}
	//为日志指定参数
	configInit := logger.Configuration{
		EnableConsole:     configBase.Log.EnableConsole,
		ConsoleJSONFormat: configBase.Log.ConsoleJSONFormat,
		ConsoleLevel:      logger.GetLevel(configBase.Log.ConsoleLevel),
		EnableFile:        configBase.Log.EnableFile,
		FileJSONFormat:    configBase.Log.FileJSONFormat,
		FileLevel:         logger.GetLevel(configBase.Log.FileLevel),
		FileLocation:      configBase.Log.FileLocation,
		MaxAge:            configBase.Log.MaxAge,
		MaxSize:           configBase.Log.MaxSize,
		Compress:          configBase.Log.Compress,
	}
	err = logger.InitGlobalLogger(configInit, logger.InstanceZapLogger)
	if err != nil {
		logger.Fatalf("Could not instantiate log! err: %v", err)
	}
}

启动项目

项目src目录下,执行:

go run main.go

在这里插入图片描述

请求验证

/login

说明:登录请求

在这里插入图片描述

在这里插入图片描述

/getUserByPage

说明:根据分页获取用户信息请求

在这里插入图片描述

在这里插入图片描述

服务端日志

在这里插入图片描述

项目地址