项目架构
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
}
- Config结构体中结构体,对应yaml文件中的一级配置
- 结构体中的变量对应yaml文件中的二级配置
- 读取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(¶m); 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
说明:根据分页获取用户信息请求