以下是我的Go实现的博客网站项目中使用的一些依赖项的:
-
github.com/gin-contrib/cors
: 用于Gin框架的中间件,提供跨源资源共享(CORS)支持。 -
github.com/gin-contrib/multitemplate
: 用于与Gin Web框架一起使用多模板(单个文件中的多个模板)的包。 -
github.com/gin-gonic/gin
: Gin是Go语言的Web框架。这是你的主要Web框架。 -
github.com/go-playground/locales
: 提供处理本地化消息和翻译的支持。 -
github.com/go-playground/universal-translator
: Go Playground生态系统的一部分,用于以通用方式处理翻译。 -
github.com/go-playground/validator/v10
: 用于Go的功能强大的验证器库。 -
github.com/golang-jwt/jwt/v5
: 在Go中实现JSON Web Token(JWT)功能。 -
github.com/lestrrat-go/file-rotatelogs
: 实现基于时间的日志轮换,支持根据时间进行轮换。 -
github.com/qiniu/go-sdk/v7
: 七牛云存储的官方Go SDK。 -
github.com/rifflock/lfshook
: 用于Logrus日志库的挂钩,支持将日志条目写入本地文件。 -
github.com/sirupsen/logrus
: 用于Go的结构化日志记录器(用于日志记录)。 -
golang.org/x/crypto
: 来自Go团队的密码库。 -
gopkg.in/ini.v1
: 用于处理INI配置文件的包。 -
gorm.io/driver/mysql
: GORM的MySQL驱动,GORM是Go中流行的ORM。 -
gorm.io/gorm
: 用于Go的GORM ORM库,用于数据库交互。
跨域支持:github.com/gin-contrib/cors
middleware/cors.go
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"time"
)
// Cors 跨域中间件
func Cors() gin.HandlerFunc {
return cors.New(
cors.Config{
//AllowAllOrigins: true,
AllowOrigins: []string{"*"}, // 等同于允许所有域名 #AllowAllOrigins: true
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"*", "Authorization"},
ExposeHeaders: []string{"Content-Length", "text/plain", "Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
},
)
}
main.go
package main
import (
"github.com/wejectchen/ginblog/routes"
)
func main() {
// 引入路由组件
routes.InitRouter()
}
routes/router.go
package routes
import (
"github.com/gin-contrib/multitemplate" //用于与Gin Web框架一起使用多模板(单个文件中的多个模板)的包。
"github.com/gin-gonic/gin"
)
func createMyRender() multitemplate.Renderer {
p := multitemplate.NewRenderer()
//...
return p
}
func InitRouter() {
r := gin.New()
r.Use(middleware.Cors())
}
Web框架github.com/gin-gonic/gin多模板github.com/gin-contrib/multitemplate
routes/router.go
package routes
import (
"github.com/gin-contrib/multitemplate"
"github.com/gin-gonic/gin"
"github.com/wejectchen/ginblog/api/v1"
"github.com/wejectchen/ginblog/middleware"
"github.com/wejectchen/ginblog/utils"
)
func createMyRender() multitemplate.Renderer {
p := multitemplate.NewRenderer()
p.AddFromFiles("admin", "web/admin/dist/index.html")
p.AddFromFiles("front", "web/front/dist/index.html")
return p
}
func InitRouter() {
gin.SetMode(utils.AppMode)
r := gin.New()
// 设置信任网络 []string
// nil 为不计算,避免性能消耗,上线应当设置
_ = r.SetTrustedProxies(nil)
r.HTMLRender = createMyRender()
r.Use(middleware.Logger())
r.Use(gin.Recovery())
r.Use(middleware.Cors())
r.Static("/static", "./web/front/dist/static")
r.Static("/admin", "./web/admin/dist")
r.StaticFile("/favicon.ico", "/web/front/dist/favicon.ico")
r.GET("/", func(c *gin.Context) {
c.HTML(200, "front", nil)
})
r.GET("/admin", func(c *gin.Context) {
c.HTML(200, "admin", nil)
})
/*
后台管理路由接口
*/
auth := r.Group("api/v1")
auth.Use(middleware.JwtToken())
{
// 用户模块的路由接口
auth.GET("admin/users", v1.GetUsers)
auth.PUT("user/:id", v1.EditUser)
auth.DELETE("user/:id", v1.DeleteUser)
//修改密码
auth.PUT("admin/changepw/:id", v1.ChangeUserPassword)
// 分类模块的路由接口
auth.GET("admin/category", v1.GetCate)
auth.POST("category/add", v1.AddCategory)
auth.PUT("category/:id", v1.EditCate)
auth.DELETE("category/:id", v1.DeleteCate)
// 文章模块的路由接口
auth.GET("admin/article/info/:id", v1.GetArtInfo)
auth.GET("admin/article", v1.GetArt)
auth.POST("article/add", v1.AddArticle)
auth.PUT("article/:id", v1.EditArt)
auth.DELETE("article/:id", v1.DeleteArt)
// 上传文件
auth.POST("upload", v1.UpLoad)
// 更新个人设置
auth.GET("admin/profile/:id", v1.GetProfile)
auth.PUT("profile/:id", v1.UpdateProfile)
// 评论模块
auth.GET("comment/list", v1.GetCommentList)
auth.DELETE("delcomment/:id", v1.DeleteComment)
auth.PUT("checkcomment/:id", v1.CheckComment)
auth.PUT("uncheckcomment/:id", v1.UncheckComment)
}
/*
前端展示页面接口
*/
router := r.Group("api/v1")
{
// 用户信息模块
router.POST("user/add", v1.AddUser)
router.GET("user/:id", v1.GetUserInfo)
router.GET("users", v1.GetUsers)
// 文章分类信息模块
router.GET("category", v1.GetCate)
router.GET("category/:id", v1.GetCateInfo)
// 文章模块
router.GET("article", v1.GetArt)
router.GET("article/list/:id", v1.GetCateArt)
router.GET("article/info/:id", v1.GetArtInfo)
// 登录控制模块
router.POST("login", v1.Login)
router.POST("loginfront", v1.LoginFront)
// 获取个人设置信息
router.GET("profile/:id", v1.GetProfile)
// 评论模块
router.POST("addcomment", v1.AddComment)
router.GET("comment/info/:id", v1.GetComment)
router.GET("commentfront/:id", v1.GetCommentListFront)
router.GET("commentcount/:id", v1.GetCommentCount)
}
_ = r.Run(utils.HttpPort)
}
多语言github.com/go-playground/locales 通用方式处理翻译github.com/go-playground/universal-translator
package validator
import (
"fmt"
"github.com/go-playground/locales/zh_Hans_CN"
unTrans "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhTrans "github.com/go-playground/validator/v10/translations/zh"
"github.com/wejectchen/ginblog/utils/errmsg"
"reflect"
)
// Validate 数据验证模块
func Validate(data any) (string, int) {
validate := validator.New()
uni := unTrans.New(zh_Hans_CN.New())
trans, _ := uni.GetTranslator("zh_Hans_CN")
err := zhTrans.RegisterDefaultTranslations(validate, trans)
if err != nil {
fmt.Println("err:", err)
}
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label")
return label
})
err = validate.Struct(data)
if err != nil {
for _, v := range err.(validator.ValidationErrors) {
return v.Translate(trans), errmsg.ERROR
}
}
return "", errmsg.SUCCESS
}
go通用验证框架github.com/go-playground/validator/v10
上面例子
jwt github.com/golang-jwt/jwt/v5
package middleware
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/wejectchen/ginblog/utils"
"github.com/wejectchen/ginblog/utils/errmsg"
"net/http"
"strings"
)
type JWT struct {
JwtKey []byte
}
func NewJWT() *JWT {
return &JWT{
[]byte(utils.JwtKey),
}
}
type MyClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// 定义错误
var (
TokenExpired = errors.New("token已过期,请重新登录。")
TokenNotValidYet = errors.New("token无效,请重新登录。")
TokenMalformed = errors.New("token不正确,请重新登录。")
TokenInvalid = errors.New("这不是一个token,请重新登录。")
)
// CreateToken 生成token
func (j *JWT) CreateToken(claims MyClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.JwtKey)
}
// ParserToken 解析token
func (j *JWT) ParserToken(tokenString string) error {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return j.JwtKey, nil
})
// 验证token
if token.Valid {
return nil
} else if errors.Is(err, jwt.ErrTokenMalformed) {
return TokenMalformed
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
return TokenExpired
} else if errors.Is(err, jwt.ErrTokenSignatureInvalid) {
return TokenInvalid
} else {
return TokenNotValidYet
}
}
// JwtToken jwt中间件
// todo 优化此类代码
func JwtToken() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
tokenHeader := c.Request.Header.Get("Authorization")
if tokenHeader == "" {
code = errmsg.ERROR_TOKEN_EXIST
c.JSON(http.StatusOK, gin.H{
"status": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
checkToken := strings.Split(tokenHeader, " ")
if len(checkToken) == 0 {
c.JSON(http.StatusOK, gin.H{
"status": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
if len(checkToken) != 2 || checkToken[0] != "Bearer" {
c.JSON(http.StatusOK, gin.H{
"status": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
j := NewJWT()
// 解析token
err := j.ParserToken(checkToken[1])
if err != nil {
if errors.Is(err, TokenExpired) {
c.JSON(http.StatusOK, gin.H{
"status": errmsg.ERROR,
"message": "token授权已过期,请重新登录",
"data": nil,
})
c.Abort()
return
}
// 其他错误
c.JSON(http.StatusOK, gin.H{
"status": errmsg.ERROR,
"message": err.Error(),
"data": nil,
})
c.Abort()
return
}
//c.Set("username",)
c.Next()
}
}
jwt token生成
// token生成函数
func setToken(c *gin.Context, user model.User) {
j := middleware.NewJWT()
claims := middleware.MyClaims{
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
Issuer: "GinBlog",
},
}
token, err := j.CreateToken(claims)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": errmsg.ERROR,
"message": errmsg.GetErrMsg(errmsg.ERROR),
"token": token,
})
}
c.JSON(http.StatusOK, gin.H{
"status": 200,
"data": user.Username,
"id": user.ID,
"message": errmsg.GetErrMsg(200),
"token": token,
})
return
}
github.com/lestrrat-go/file-rotatelogs : 实现基于时间的日志轮换,支持根据时间进行轮换。
github.com/rifflock/lfshook 用于将日志文件写入本地文件
github.com/sirupsen/logrus : 用于Go的结构化日志记录器(用于日志记录)
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
retalog "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"os"
"time"
)
// Logger 日志中间件
// todo 可考虑更换其他日志中间件
func Logger() gin.HandlerFunc {
filePath := "log/log"
//linkName := "latest_log.log"
scr, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
fmt.Println("err:", err)
}
logger := logrus.New()
logger.Out = scr
logger.SetLevel(logrus.DebugLevel)
logWriter, _ := retalog.New(
filePath+"%Y%m%d.log",
retalog.WithMaxAge(7*24*time.Hour),
retalog.WithRotationTime(24*time.Hour),
//retalog.WithLinkName(linkName),
)
writeMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}
Hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
logger.AddHook(Hook)
return func(c *gin.Context) {
startTime := time.Now()
c.Next()
stopTime := time.Since(startTime).Milliseconds()
spendTime := fmt.Sprintf("%d ms", stopTime)
hostName, err := os.Hostname()
if err != nil {
hostName = "unknown"
}
statusCode := c.Writer.Status()
clientIp := c.ClientIP()
userAgent := c.Request.UserAgent()
dataSize := c.Writer.Size()
if dataSize < 0 {
dataSize = 0
}
method := c.Request.Method
path := c.Request.RequestURI
entry := logger.WithFields(logrus.Fields{
"HostName": hostName,
"status": statusCode,
"SpendTime": spendTime,
"Ip": clientIp,
"Method": method,
"Path": path,
"DataSize": dataSize,
"Agent": userAgent,
})
if len(c.Errors) > 0 {
entry.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
}
if statusCode >= 500 {
entry.Error()
} else if statusCode >= 400 {
entry.Warn()
} else {
entry.Info()
}
}
}
golang.org/x/crypto: 来自Go团队的密码库
import (
"golang.org/x/crypto/bcrypt"
)
// ScryptPw 生成密码
func ScryptPw(password string) string {
const cost = 10
HashPw, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
log.Fatal(err)
}
return string(HashPw)
}
密码比对
PasswordErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if PasswordErr != nil {
return user, errmsg.ERROR_PASSWORD_WRONG
}
gopkg.in/ini.v1: 用于处理INI配置文件的包
config.ini
[server]
# debug 开发模式,release 生产模式
AppMode = debug
HttpPort = :3000
JwtKey = yourKey
[database]
Db = mysql
DbHost = 127.0.0.1
DbPort = 3306
DbUser = newroot
DbPassWord = c22222
DbName = ginblog
[qiniu]
Zone = 1 # 1:华东/华东-浙江;2:华北;3:华南,不填默认华北。境外服务器特殊使用环境自行配置
AccessKey =
SecretKey =
Bucket =
QiniuSever =
import (
"fmt"
"gopkg.in/ini.v1"
)
// 初始化
func init() {
file, err := ini.Load("config/config.ini")
if err != nil {
fmt.Println("配置文件读取错误,请检查文件路径:", err)
}
LoadServer(file)
}
func LoadServer(file *ini.File) {
AppMode = file.Section("server").Key("AppMode").MustString("debug")
HttpPort = file.Section("server").Key("HttpPort").MustString(":3000")
JwtKey = file.Section("server").Key("JwtKey").MustString("89js82js72")
}
gorm.io/driver/mysql: GORM的MySQL驱动,GORM是Go中流行的ORM。
gorm.io/gorm: 用于Go的GORM ORM库,用于数据库交互。
package model
import (
"fmt"
"github.com/wejectchen/ginblog/utils"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"os"
"time"
)
var db *gorm.DB
var err error
func InitDb() {
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
utils.DbUser,
utils.DbPassWord,
utils.DbHost,
utils.DbPort,
utils.DbName,
)
db, err = gorm.Open(mysql.Open(dns), &gorm.Config{
// gorm日志模式:silent
Logger: logger.Default.LogMode(logger.Silent),
// 外键约束
DisableForeignKeyConstraintWhenMigrating: true,
// 禁用默认事务(提高运行速度)
SkipDefaultTransaction: true,
NamingStrategy: schema.NamingStrategy{
// 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user`
SingularTable: true,
},
})
if err != nil {
fmt.Println("连接数据库失败,请检查参数:", err)
os.Exit(1)
}
// 迁移数据表,在没有数据表结构变更时候,建议注释不执行
// 注意:初次运行后可注销此行
_ = db.AutoMigrate(&User{}, &Article{}, &Category{}, Profile{}, Comment{})
sqlDB, _ := db.DB()
// SetMaxIdleCons 设置连接池中的最大闲置连接数。
sqlDB.SetMaxIdleConns(10)
// SetMaxOpenCons 设置数据库的最大连接数量。
sqlDB.SetMaxOpenConns(100)
// SetConnMaxLifetiment 设置连接的最大可复用时间。
sqlDB.SetConnMaxLifetime(10 * time.Second)
}
User.go
package model
import (
"github.com/wejectchen/ginblog/utils/errmsg"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"log"
)
type User struct {
gorm.Model
Username string `gorm:"type:varchar(20);not null " json:"username" validate:"required,min=4,max=12" label:"用户名"`
Password string `gorm:"type:varchar(500);not null" json:"password" validate:"required,min=6,max=120" label:"密码"`
Role int `gorm:"type:int;DEFAULT:2" json:"role" validate:"required,gte=2" label:"角色码"`
}
// CheckUser 查询用户是否存在
func CheckUser(name string) (code int) {
var user User
db.Select("id").Where("username = ?", name).First(&user)
if user.ID > 0 {
return errmsg.ERROR_USERNAME_USED //1001
}
return errmsg.SUCCESS
}
// CheckUpUser 更新查询
func CheckUpUser(id int, name string) (code int) {
var user User
db.Select("id, username").Where("username = ?", name).First(&user)
if user.ID == uint(id) {
return errmsg.SUCCESS
}
if user.ID > 0 {
return errmsg.ERROR_USERNAME_USED //1001
}
return errmsg.SUCCESS
}
// CreateUser 新增用户
func CreateUser(data *User) int {
//data.Password = ScryptPw(data.Password)
err := db.Create(&data).Error
if err != nil {
return errmsg.ERROR // 500
}
return errmsg.SUCCESS
}
// GetUser 查询用户
func GetUser(id int) (User, int) {
var user User
err := db.Limit(1).Where("ID = ?", id).Find(&user).Error
if err != nil {
return user, errmsg.ERROR
}
return user, errmsg.SUCCESS
}
// GetUsers 查询用户列表
func GetUsers(username string, pageSize int, pageNum int) ([]User, int64) {
var users []User
var total int64
if username != "" {
db.Select("id,username,role,created_at").Where(
"username LIKE ?", username+"%",
).Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&users)
db.Model(&users).Where(
"username LIKE ?", username+"%",
).Count(&total)
return users, total
}
db.Select("id,username,role,created_at").Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&users)
db.Model(&users).Count(&total)
if err != nil {
return users, 0
}
return users, total
}
// EditUser 编辑用户信息
func EditUser(id int, data *User) int {
var user User
var maps = make(map[string]interface{})
maps["username"] = data.Username
maps["role"] = data.Role
err = db.Model(&user).Where("id = ? ", id).Updates(maps).Error
if err != nil {
return errmsg.ERROR
}
return errmsg.SUCCESS
}
// ChangePassword 修改密码
func ChangePassword(id int, data *User) int {
//var user User
//var maps = make(map[string]interface{})
//maps["password"] = data.Password
err = db.Select("password").Where("id = ?", id).Updates(&data).Error
if err != nil {
return errmsg.ERROR
}
return errmsg.SUCCESS
}
// DeleteUser 删除用户
func DeleteUser(id int) int {
var user User
err = db.Where("id = ? ", id).Delete(&user).Error
if err != nil {
return errmsg.ERROR
}
return errmsg.SUCCESS
}
// BeforeCreate 密码加密&权限控制
func (u *User) BeforeCreate(_ *gorm.DB) (err error) {
u.Password = ScryptPw(u.Password)
u.Role = 2
return nil
}
func (u *User) BeforeUpdate(_ *gorm.DB) (err error) {
u.Password = ScryptPw(u.Password)
return nil
}
// ScryptPw 生成密码
func ScryptPw(password string) string {
const cost = 10
HashPw, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
log.Fatal(err)
}
return string(HashPw)
}
// CheckLogin 后台登录验证
func CheckLogin(username string, password string) (User, int) {
var user User
var PasswordErr error
db.Where("username = ?", username).First(&user)
PasswordErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if user.ID == 0 {
return user, errmsg.ERROR_USER_NOT_EXIST
}
if PasswordErr != nil {
return user, errmsg.ERROR_PASSWORD_WRONG
}
if user.Role != 1 {
return user, errmsg.ERROR_USER_NO_RIGHT
}
return user, errmsg.SUCCESS
}
// CheckLoginFront 前台登录
func CheckLoginFront(username string, password string) (User, int) {
var user User
var PasswordErr error
db.Where("username = ?", username).First(&user)
PasswordErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if user.ID == 0 {
return user, errmsg.ERROR_USER_NOT_EXIST
}
if PasswordErr != nil {
return user, errmsg.ERROR_PASSWORD_WRONG
}
return user, errmsg.SUCCESS
}