Gin项目 -- 博客系统
1. 项目整体架构
基于 Vue3 + Gin + Gorm + ElasticSearch + Websocket 的前后端分离项目
golang v1.19 以上
2. 项目目录结构

3. 后端项目环境搭建
1. 配置Goproxy
GOPROXY=https://goproxy.cn,direct

2. yaml 文件
mysql:
host: 62.234.221.57
port: 33060
db: blog
user: root
password: 123456
max-idle-conns: 10
max-open-conns: 100
log_level: dev
logger:
level: info
prefix: '[blog]'
director: log
show_line: true
log_in_console: true
system:
host: "0.0.0.0"
port: 8089
env: release
site-info:
created_at: "2023-12-06"
bei_an: 渝ICP备20231206654号-1
title: 个人博客系统
qq_image: http://www.aicx365.com/static/img/footer/qq.png
version: 0.1
email: 992838239@qq.com
wechat_image: http://www.aicx365.com/static/img/footer/we_chat.png
name: 个人博客
job: 后端开发
addr: 重庆市
slogan: Golang个人博客
slogan_en: GOLANGGERENBOKE
web: http://www.aicx365.com/
gitee_url: https://gitee.com/qidaii
3. 配置文件
config/enter.go
package config
type Config struct {
Mysql Mysql `yaml:"mysql"`
Logger Logger `yaml:"logger"`
System System `yaml:"system"`
SiteInfo SiteInfo `yaml:"site-info"`
}
config/conf_mysql.go
package config
type Mysql struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Config string `yaml:"config"` // 高级配置,如 charset 等
DB string `yaml:"db"`
User string `yaml:"user"`
Password string `yaml:"password"`
MaxIdleConns int `json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `json:"max-open-conns" yaml:"max-open-conns"` // 最大建立连接数
LogLevel string `yaml:"log_level"` // 日志等级,debug就是输出全部SQL,dev,release(线上环境)
}
config/conf_logger.go
package config
type Logger struct {
Level string `yaml:"level"`
Prefix string `yaml:"prefix"`
Director string `yaml:"director"`
ShowLine bool `yaml:"show-line"` // 是否显示行号
LogInConsole bool `yaml:"log-in-console"` // 是否显示打印的路径
}
config/conf_system.go
package config
import "fmt"
type System struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Env string `yaml:"env"`
}
func (s System) Addr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port)
}
config/conf_site_info.go
package config
type SiteInfo struct {
CreatedAt string `yaml:"created_at" json:"created_at"`
BeiAn string `yaml:"bei_an" json:"bei_an"`
Title string `yaml:"title" json:"title"`
QQImage string `yaml:"qq_image" json:"qq_image"`
Version string `yaml:"version" json:"version"`
Email string `yaml:"email" json:"email"`
WechatImage string `yaml:"wechat_image" json:"wechat_image"`
Name string `yaml:"name" json:"name"`
Job string `yaml:"job" json:"job"`
Addr string `yaml:"addr" json:"addr"`
Slogan string `yaml:"slogan" json:"slogan"`
Slogan_en string `yaml:"slogan_En" json:"slogan_En"`
Web string `yaml:"web" json:"web"`
GiteeUrl string `yaml:"gitee_Url" json:"giteeUrl"`
}
4. 读取配置文件
1. 下载yaml文件读取模块
go get gopkg.in/yaml.v2
core/conf.go
package core
import (
"fmt"
"gin_blog/config"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
)
// 读取yaml文件的配置
func InitConf() {
const ConfigFile = "settings.yaml"
c := &config.Config{}
yamlConf, err := ioutil.ReadFile(ConfigFile)
if err != nil {
panic(fmt.Errorf("读取配置文件初失败: %s", err))
}
err = yaml.Unmarshal(yamlConf, c)
if err != nil {
log.Fatalf("配置文件初始化失败: %v", err)
}
log.Println("配置文件初始化成功!")
fmt.Println(c)
}
main.go
package main
import "gin_blog/core"
func main() {
// 读取配置文件
core.InitConf()
}
2. 将配置文件设置到全局变量中
core/conf.go
package core
import (
"fmt"
"gin_blog/config"
"gin_blog/global"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
)
// 读取yaml文件的配置
func InitConf() {
const ConfigFile = "settings.yaml"
c := &config.Config{}
yamlConf, err := ioutil.ReadFile(ConfigFile)
if err != nil {
panic(fmt.Errorf("读取配置文件初失败: %s", err))
}
err = yaml.Unmarshal(yamlConf, c)
if err != nil {
log.Fatalf("配置文件初始化失败: %v", err)
}
log.Println("配置文件初始化成功!")
global.Config = c // 将读取到的值设置到全局变量中
}
global/global.go
package global
import "gin_blog/config"
var (
Config *config.Config
)
5. 配置日志系统
1. 下载logrus模块
go get github.com/sirupsen/logrus
2. 自定义Logger
core/logrus.go
package core
import (
"bytes"
"fmt"
"gin_blog/global"
"github.com/sirupsen/logrus"
"os"
"path"
)
// 日志颜色
const (
red = 31
yellow = 33
blue = 36
gray = 37
)
type LogFormatter struct{}
func (t *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// 根据不同的level 显示颜色
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = gray
case logrus.WarnLevel:
levelColor = yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = red
default:
levelColor = blue
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
prefix := global.Config.Logger.Prefix
// 自定义日期格式
timestamp := entry.Time.Format("2006-01-02 15:04:05")
if entry.HasCaller() {
// 自定义文件路径
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
// 自定义输出格式
fmt.Fprintf(b, "%s[%s] \x1b[%dm[%s]\x1b[0m %s %s %s]\n", prefix, timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message)
} else {
fmt.Fprintf(b, "%s[%s] \x1b[%dm[%s]\x1b[0m %s]\n", prefix, timestamp, levelColor, entry.Level, entry.Message)
}
return b.Bytes(), nil
}
func InitLogger() *logrus.Logger {
mLog := logrus.New() // 创建一个实例
mLog.SetOutput(os.Stdout) // 设置输出类型
mLog.SetReportCaller(global.Config.Logger.ShowLine) // 开启返回函数和行号
mLog.SetFormatter(&LogFormatter{}) // 设置自己定义的Formatter
level, err := logrus.ParseLevel(global.Config.Logger.Level)
if err != nil {
level = logrus.InfoLevel
}
mLog.SetLevel(level) // 设置最低的Level
InitDefaultLogger() // 修改 logrus 全局log样式
return mLog
}
func InitDefaultLogger() {
// 修改 logrus 全局log样式
logrus.SetOutput(os.Stdout) // 设置输出类型
logrus.SetReportCaller(global.Config.Logger.ShowLine) // 开启返回函数和行号
logrus.SetFormatter(&LogFormatter{}) // 设置自己定义的Formatter
level, err := logrus.ParseLevel(global.Config.Logger.Level)
if err != nil {
level = logrus.InfoLevel
}
logrus.SetLevel(level) // 设置最低的Level
}
2. 设置到全局变量
package global
import (
"gin_blog/config"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
var (
Config *config.Config
Log *logrus.Logger
)
3. 初始化日志
main.go
package main
import (
"gin_blog/core"
"gin_blog/global"
)
func main() {
// 读取配置文件
core.InitConf()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
}
6. 初始化 Gorm 连接
1. 安装gorm和mysql驱动
go get gorm.io/driver/mysql
go get gorm.io/gorm
2. gorm初始化代码
config/conf_mysql.go
// 添加如下内容
func (m *Mysql) Dsn() string {
return m.User + ":" + m.Password + "@tcp(" + m.Host + ":" + strconv.Itoa(m.Port) + ")/" + m.DB + "?" + m.Config
}
core/gorm.go
package core
import (
"fmt"
"gin_blog/global"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
func InitGorm() *gorm.DB {
if global.Config.Mysql.Host == "" {
global.Log.Warn("未配置mysql,取消gorm连接")
return nil
}
dsn := global.Config.Mysql.Dsn()
var mysqlLogger logger.Interface
if global.Config.System.Env == "debug" {
// 开发环境显示所有的sql语句
mysqlLogger = logger.Default.LogMode(logger.Info)
} else {
mysqlLogger = logger.Default.LogMode(logger.Error)
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: mysqlLogger,
})
if err != nil {
global.Log.Error(fmt.Sprintf("[%s] mysql 连接失败", dsn))
panic(err)
}
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(global.Config.Mysql.MaxIdleConns) // 最大空闲连接数
sqlDB.SetMaxOpenConns(global.Config.Mysql.MaxOpenConns) // 最多可容纳连接数
sqlDB.SetConnMaxLifetime(time.Hour * 4) // 连接最大服用时间,不能超过mysql的 wait_timeout
return db
}
3. 设置到全局变量
global/global.go
package global
import (
"gin_blog/config"
"gorm.io/gorm"
)
var (
Config *config.Config
DB *gorm.DB
)
main.go
package main
import (
"gin_blog/core"
"gin_blog/global"
)
func main() {
// 读取配置文件
core.InitConf()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
// 建立mysql连接
global.DB = core.InitGorm()
}
7. 初始化 Redis 连接
0. 官方文档
go-redis
0. yaml 配置
settings.yaml
redis:
host: "81.70.168.242"
port: 6379
password: ""
1. 下载 redis 模块
go get github.com/go-redis/redis/v8
2. Redis 初始化
core/redis.go
package core
import (
"gin_any_chat/global"
"github.com/go-redis/redis/v8"
)
func InitRedis() *redis.Client {
if global.Config.Redis.Host == "" {
global.Logger.Warn("未配置redis,取消redis连接")
return nil
}
client := redis.NewClient(global.Config.Redis.Dns0())
return client
}
config/redis_conf.go
package config
import (
"github.com/go-redis/redis/v8"
"strconv"
)
type Redis struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
}
func (r *Redis) Dns0() *redis.Options {
return &redis.Options{
Addr: r.Host + ":" + strconv.Itoa(r.Port),
Password: r.Password, // no password set
DB: 0, // use default DB
}
}
3. 设置到全局变量
global/global.go
package global
import (
"gin_any_chat/config"
"gin_any_chat/models/response"
"github.com/go-redis/redis/v8"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ErrorMap map[response.ErrorCode]string
var (
Config *config.Config
Logger *logrus.Logger
DB *gorm.DB
RDS *redis.Client
)
main.go
package main
import (
"gin_any_chat/common"
"gin_any_chat/core"
"gin_any_chat/global"
"gin_any_chat/routers"
)
func main() {
// 1. 配置文件初始化
core.InitConf()
// 2.读错误码 JSON 文件到内存中
core.ReadErrorCodeFromJson()
// 3. 日志配置初始化, 并设置到全局变量
global.Logger = core.InitLogger()
// 4. 建立 MySQL 连接, 并设置到全局变量
global.DB = core.InitGorm()
// 5. 建立 Redis 连接
global.RDS = core.InitRedis()
// 6. 命令行参数解析
option := common.Parse()
if common.IsWebStop(option) {
common.SwitchOption(option)
return
}
common.SwitchOption(option)
// 7. 根据配置文件, 启动服务器
router := routers.InitRouter()
addr := global.Config.System.Addr()
global.Logger.Infof("项目启动成功,运行在: %s", addr)
router.Run(addr)
}
8. 初始化路由
1. 下载gin模块
go get github.com/gin-gonic/gin
2. 路由代码
routers/enter.go
package routers
import (
"gin_blog/global"
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
gin.SetMode(global.Config.System.Env) // 设置Gin运行等级
router := gin.Default()
router.GET("", func(c *gin.Context) {
c.String(200, "xxx")
})
return router
}
3. 初始化路由
package main
import (
"gin_blog/core"
"gin_blog/global"
"gin_blog/routers"
)
func main() {
// 读取配置文件
core.InitConf()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
// 建立mysql连接
global.DB = core.InitGorm()
// 初始化路由
routers.InitRouter()
}
9. 项目运行
main.go
package main
import (
"gin_blog/core"
"gin_blog/global"
"gin_blog/routers"
)
func main() {
// 读取配置文件
core.InitConf()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
// 建立mysql连接
global.DB = core.InitGorm()
// 初始化路由
router := routers.InitRouter()
// 项目运行
addr := global.Config.System.Addr()
global.Log.Infof("项目启动成功,运行在: %s", addr)
router.Run(addr)
}
10. 路由分包
1. API分包
api/settings_api/enter.go
package settings_api
type SettingsApi struct {
}
api/settings_api/settings_info.go
package settings_api
import (
"github.com/gin-gonic/gin"
)
func (s SettingsApi) SettingsInfoView(c *gin.Context) {
c.JSON(200, gin.H{"msg": "xxx"})
}
api/enter.go
package api
import "gin_blog/api/settings_api"
type ApiGroup struct {
SettingsApi settings_api.SettingsApi
}
var ApiGroupApp = new(ApiGroup)
2. 路由分包分组
routers/enter.go
package routers
import (
"gin_blog/global"
"github.com/gin-gonic/gin"
)
type RouterGroup struct {
*gin.RouterGroup
}
func InitRouter() *gin.Engine {
gin.SetMode(global.Config.System.Env) // 设置Gin运行等级
router := gin.Default()
// 路由分组
aipRouterGroup := router.Group("api")
routerGroupApp := &RouterGroup{aipRouterGroup}
// 系统配置API
routerGroupApp.SettingsRouters()
return router
}
routers/settings_router.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
router.GET("", api.ApiGroupApp.SettingsApi.SettingsInfoView)
}
11. 统一响应
1. 常量定义
1. 自定义状态码
models/response/error_code.go
package response
type ErrorCode int
const (
SETTINGS_ERROR ErrorCode = 1001
)
var (
ErrorMap = map[ErrorCode]string{
SETTINGS_ERROR: "系统错误",
}
)
2. 统一响应函数
models/response/response.go
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
const (
ERROR = iota
SUCCESS
)
const (
PANIC_ERROR = "未知错误"
)
type Response struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
type PageListResponse struct {
Code int `json:"code"`
Count int64 `json:"count"`
Data any `json:"data"`
Msg string `json:"msg"`
}
// 统一返回函数
func Result(code int, data any, msg string, c *gin.Context) {
c.JSON(http.StatusOK, &Response{
Code: code,
Data: data,
Msg: msg,
})
}
// 请求成功返回数据
func Ok(data any, msg string, c *gin.Context) {
Result(SUCCESS, data, msg, c)
}
// 查询数据库成功返回数据
func OkWithData(data any, c *gin.Context) {
Result(SUCCESS, data, "查询成功", c)
}
// 请求成功只显示提示信息
func OkWithMessage(msg string, c *gin.Context) {
Result(SUCCESS, map[string]any{}, msg, c)
}
// 请求失败返回数据
func Fail(data any, msg string, c *gin.Context) {
Result(ERROR, data, msg, c)
}
// 请求失败只显示提示信息
func FailWithMessage(msg string, c *gin.Context) {
Result(ERROR, map[string]any{}, msg, c)
}
// 请求成功返回分页数据
func OkWithDataPageList(count int64, data any, c *gin.Context) {
c.JSON(http.StatusOK, &PageListResponse{
Code: SUCCESS,
Count: count,
Data: data,
Msg: "请求成功",
})
}
// 请求失败,根据不同code码显示不同提示信息
func FailWithCode(code ErrorCode, c *gin.Context) {
msg, ok := ErrorMap[code]
if !ok {
Result(ERROR, map[string]any{}, PANIC_ERROR, c)
return
}
Result(int(code), map[string]any{}, msg, c)
}
3. API中调用
api/settings_api/settings_info.go
package settings_api
import (
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (s SettingsApi) SettingsInfoView(c *gin.Context) {
response.Ok(map[string]string{}, "请求成功", c)
}
2. 读Json文件
models/response/error_code.json
{
"1001": "系统错误"
}
可以在main.go 启动项目的时候将json文件中的数据读到内存中
package main
import (
"gin_blog/core"
"gin_blog/global"
"gin_blog/routers"
)
func main() {
// 读取配置文件
core.InitConf()
// 读错误码json文件
core.ReadErrorCodeFromJson()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
// 建立mysql连接
global.DB = core.InitGorm()
// 初始化路由
router := routers.InitRouter()
// 项目运行
addr := global.Config.System.Addr()
global.Log.Infof("项目启动成功,运行在: %s", addr)
router.Run(addr)
}
global/global.go
package global
import (
"gin_blog/config"
"gin_blog/core"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ErrorMap map[response.ErrorCode]string
var (
Config *config.Config
DB *gorm.DB
Log *logrus.Logger
ErrorCode ErrorMap
)
core/error_code.go
package core
import (
"encoding/json"
"fmt"
"gin_blog/global"
"gin_blog/models/response"
"github.com/sirupsen/logrus"
"os"
)
const filePath = "models/response/error_code.json"
func ReadErrorCodeFromJson() {
byteData, err := os.ReadFile(filePath)
if err != nil {
logrus.Errorf("读取错误码Json文件错误: %s", err)
return
}
var errMap global.ErrorMap
err = json.Unmarshal(byteData, errMap)
if err != nil {
logrus.Errorf("Json反序列化到Map错误: %s", err)
return
}
global.ErrorCode = errMap
fmt.Println(global.ErrorCode[response.SETTINGS_ERROR])
}
4. 表结构创建
4.0 公共表字段
models/enter.go
gorm.Model 包含了逻辑删除,但是开发过程中是不需要的,所以自定义Model
package models
import "time"
type MODEL struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedTime time.Time `json:"created_time"`
UpdateTime time.Time `json:"update_time" gorm:"autoUpdateTime"` // autoUpdateTime 设置 update 时自动更新时间
}
4.1 公共字段及方法
models/ctype/array_type.go
package ctype
import (
"database/sql/driver"
"strings"
)
type ArrayType []string
// 从数据库读该字段时调用的方法
func (a *ArrayType) Scan(value interface{}) error {
v, _ := value.([]byte)
if string(v) == "" {
*a = []string{}
return nil
}
*a = strings.Split(string(v), ";")
return nil
}
// 将数组转成字符串存在数据库中
func (a ArrayType) Value() (driver.Value, error) {
return strings.Join(a, ";"), nil
}
models/ctype/role_type.go
package ctype
import "encoding/json"
type RoleType int
const (
PermissionAdmin = iota + 1 // 管理员
PermissionUser // 普通用户
PermissionVisitor // 游客
PermissionDisableUser // 被禁用的用户
)
// 定义MarshalJSON,json反序列化的时候自动执行
func (r RoleType) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
// 重写print相关方法,调用Print的时候显示中文
func (r RoleType) String() string {
var str string
switch r {
case PermissionAdmin:
str = "管理员"
case PermissionUser:
str = "普通用户"
case PermissionVisitor:
str = "游客"
case PermissionDisableUser:
str = "被禁用的用户"
default:
str = "其他"
}
return str
}
models/ctype/sign_status_type.go
通过枚举来实现choices功能
package ctype
import "encoding/json"
type SignStatusType int
const (
SignFromQQ = iota + 1 // QQ
SignFromWeChat // 微信
SignFromGitee // Gitee
SignFromEmail // 邮箱
)
// 定义MarshalJSON,json反序列化的时候自动执行
func (s SignStatusType) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// 重写print相关方法,调用Print的时候显示中文
func (s SignStatusType) String() string {
var str string
switch s {
case SignFromQQ:
str = "QQ"
case SignFromWeChat:
str = "微信"
case SignFromGitee:
str = "Gitee"
case SignFromEmail:
str = "邮箱"
default:
str = "其他"
}
return str
}
models/tag_type.go
package ctype
import "encoding/json"
type TagType uint
const (
TagArticle = iota + 1 // 文章
TagSite // 站点
)
// 定义MarshalJSON,json反序列化的时候自动执行
func (t TagType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
// 重写print相关方法,调用Print的时候显示中文
func (t TagType) String() string {
var str string
switch t {
case TagArticle:
str = "文章"
case TagSite:
str = "站点"
default:
str = "其他"
}
return str
}
4.2 用户表
models/user_model.go
package models
import (
"gin_blog/models/ctype"
)
type UserModel struct {
MODEL
NickName string `gorm:"size:16" json:"nick_name"` // 昵称
UserName string `gorm:"size:32" json:"user_name"` // 用户名
Password string `gorm:"size:64" json:"password"` // 密码
Avatar string `gorm:"size:256" json:"avatar"` // 头像地址
Email string `gorm:"size:32" json:"email"` // 邮箱
PhoneNumber string `gorm:"size:11" json:"phone_number"` // 手机号
Address string `gorm:"size:64" json:"address"` // 联系地址
QQToken string `gorm:"size:64" json:"qq_token"` // QQ平台唯一ID
IP string `gorm:"size:20" json:"ip"` // IP地址
Role ctype.RoleType `gorm:"size:1" json:"role"` // 用户角色 1 管理员 2 普通用户 3 游客
SignStatus ctype.SignStatusType `gorm:"type=smallint(6)" json:"signStatus"` // 注册来源 1 QQ 2 微信 3 Gitee 4 邮箱
ArticleModels []ArticleModel `gorm:"foreignKey:UserID" json:"-"` // 文章列表
CollectsModels []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID" json:"-"` // 收藏的文航列表
}
4.3 文章表
models/article_model.go
package models
import "gin_blog/models/ctype"
type ArticleModel struct {
MODEL
Title string `gorm:"size:32" json:"title"` // 文章标题
Abstract string `gorm:"size:32" json:"abstract"` // 文章简介
Content string `json:"content"` // 文章内容 默认数据库字段类型为longtext
LookCount uint `json:"look_count"` // 浏览量
CommentCount uint `json:"comment_count"` // 评论量
DiggCount uint `json:"digg_count"` // 点赞量
CollectsCount uint `json:"collects_count"` // 收藏量
TagModels []TagModel `gorm:"many2many:article_tag_models" json:"tag_models"` // 文章标签
CommentModels []CommentModel `gorm:"foreignKey:ArticleID" json:"-"` // 文章的评论列表
UserModel UserModel `gorm:"foreignKey:UserID" json:"-"` // 文章作者
Category string `gorm:"size:20" json:"category"` // 文章分类
Source string `json:"source"` // 文章来源
Link string `json:"link"` // 原文链接
Banner BannerModel `gorm:"foreignKey:BannerID" json:"-"` // 文章封面
BannerID uint `json:"banner_id"` // 封面图片ID
// 冗余字段
UserID uint `json:"user_id"` // 文章作者ID
NickName string `grom:"size:16" json:"nick_name"` // 发布文章的用户昵称
BannerPath string `json:"banner_path"` // 文章的封面
Tags ctype.ArrayType `gorm:"type:string;size:64" json:"tags" ` // 文章标签,数组形式存储
}
4.4 标签表
models/tag_model.go
package models
type TagModel struct {
MODEL
Title string `gorm:"size:16" json:"title"` // 标签名称
Articles []ArticleModel `gorm:"many2many:article_tag_models" json:"-"` // 关联该标签的文章列表
}
4.5 图片表
models/banner_model.go
package models
type BannerModel struct {
MODEL
Path string `json:"path"` // 图片路径
Hash string `gorm:"column:hash_key" json:"hash" ` // 图片的hash值,用于判断重复图片
Name string `gorm:"size:32" json:"name"` //图片名称
ImageType ctype.ImageType `gorm:"default:1;" json:"image_type"` // 存储类型,本地存储 七牛云存储
NotUse bool `gorm:"default:false;comment:是否可以选择" json:"not_use"` // 是否可以展示
}
func (b *BannerModel) BeforeCreate(tx *gorm.DB) (err error) {
b.UpdateAt = time.Now() // 这里先简单的赋值更新时间,后面需要将mysql的sqlMode 改为none_in_date
return nil
}
4.6 评论表
models/comment_model.go
package models
type CommentModel struct {
MODEL
SubComments []*CommentModel `gorm:"foreignKey:ParentCommentID" json:"sub_comments"` // 子评论列表
ParentCommentModel *CommentModel `gorm:"foreignKey:ParentCommentID" json:"comment_model"` // 父级评论
ParentCommentID *uint `json:"parent_comment_id"` // 父评论ID
Content string `gorm:"size:256" json:"content"` //评论内容
DiggCount uint `gorm:"size:8;default:0" json:"digg_count"` // 点赞数
CommentCount uint `gorm:"size:8;default:0" json:"comment_count"` // 子评论数
Article ArticleModel `json:"article"` // 关联的文章
ArticleID uint `grom:"foreignKey:ArticleID" json:"article_id"` // 关联的文章ID
User UserModel `json:"user"` // 关联的用户
NickName string `grom:"size:16" json:"nick_name"` // 发布文章的用户昵称
BannerPath string `json:"banner_path"` // 文章的封面
UserID uint `json:"user_id"` // 评论的用户ID
}
4.7 用户收藏文章中间表
models/user_collect_model.go
package models
import "time"
type UserCollectModel struct {
UserID uint `gorm:"primaryKey"`
UserModel UserModel `gorm:"foreignKey:UserID"`
ArticleID uint `gorm:"primaryKey"`
ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
CreatedAt time.Time `json:"created_at"`
}
4.8 菜单表
models/menu_model.go
package models
import "gin_blog/models/ctype"
type MenuModel struct {
MODEL
MenuTitle string `gorm:"size:32" json:"menu_title"`
MenuTitleEn string `gorm:"size:32" json:"menu_title_en"` // slogan
Slogan string `gorm:"size:64" json:"slogan"` // 简介
Abstrac ctype.ArrayType `gorm:"type:string" json:"abstrac"` // 简介的切换时间
AbstractTime int `json:"abstract_time"`
Banners []BannerModel `gorm:"many2many:menu_banner_models;joinForeignKey:MenuID;JoinReferences:BannerID" json:"banners"` // 菜单的图片列表
BannerTime int `json:"banner_time"` // 菜单图片的切换时间,为0表示不切换
Sort int `grom:"size:10" json:"sort"` // 菜单的顺序
}
4.9 菜单与图片中间表
models/menu_banner_model.go
package models
type MenuBannerModel struct {
MenuID uint `gorm:"primaryKey"`
MenuModel MenuModel `gorm:"foreignKey:MenuID"`
BannerID uint `gorm:"primaryKey"`
BannerModel BannerModel `gorm:"foreignKey:BannerID"`
Sort int `gorm:"size:10" json:"sort"`
}
4.10 用户反馈表
models/fade_back_model.go
package models
type BannerModel struct {
MODEL
Path string `json:"path"` // 图片路径
Key string `gorm:"column:banner_key" json:"key" ` // 图片的hash值,用于判断重复图片
Name string `gorm:"size:32" json:"name"` //图片名称
NotUse bool `gorm:"default:false;comment:是否可以选择" json:"not_use"` // 是否可以展示
}
4.11 广告表
models/advert_model.go
package models
type AdvertModel struct {
MODEL
Title string `gorm:"size:32" json:"title"` // 显示的标题
Href string `json:"href"` // 跳转链接
Images string `json:"images"` // 图片
IsShow bool `json:"is_show"` // 是否展示
}
4.12 聊天消息表
models/message_model.go
package models
type MessageModel struct {
MODEL
SendUserID uint `gorm:"primaryKey" json:"send_user_id"` // 发送人ID
SendUserModel UserModel `gorm:"foreignKey:SendUserID" json:"-"`
SendUserNickName string `gorm:"size:42" json:"send_user_nick_name"`
SendUserAvatar string `json:"send_user_avatar"`
ReceiveUserID uint `gorm:"primaryKey" json:"receive_user_id"` // 接收人ID
ReceiveUserModel UserModel `gorm:"foreignKey:ReceiveUserID" json:"-"`
ReceiveUserNickName string `gorm:"size:42" json:"receive_user_nick_name"`
ReceiveUserAvatar string `json:"receive_user_avatar"`
IsRead bool `gorm:"default:false" json:"is_read"` // 对方是否查看
Content string `json:"content"` // 消息内容
}
4.13 登录信息表
models/login_info_model.go
package models
type LoginInfoModel struct {
MODEL
UserID uint `json:"user_id"`
UserModel UserModel `gorm:"foreignKey:UserID" json:"-"`
IP string `gorm:"size:20" json:"ip"`
NickName string `gorm:"size:32" json:"nick_name"`
Token string `gorm:"size:256" json:"token"`
Device string `gorm:"size:256" json:"device"` // 登录设备
Addr string `gorm:"size:64" json:"addr"`
}
5. 表结构迁移
通过命令行参数来执行表结构迁移
flag/enter.go
package flag
import sys_flag "flag"
type Option struct {
Version bool
DB bool
}
func Parse() Option {
//version := sys_flag.Bool("v", false, "项目版本")
db := sys_flag.Bool("db", false, "初始化数据库")
// 解析命令行参数写入注册的flag中
sys_flag.Parse()
return Option{
//Version: *version,
DB: *db,
}
}
// 是否停止web项目
func IsWebStop(option Option) bool {
if option.DB {
return true
}
//if option.Version {
// return false
//}
return false
}
// 根据不用命令执行不同函数
func SwitchOption(option Option) {
if option.DB {
MakeMigrations()
}
//if option.Version {
// Version()
//}
}
flag/db.go
package flag
import (
"gin_blog/global"
"gin_blog/models"
)
func MakeMigrations() {
// 声明绑定的中间表
global.DB.SetupJoinTable(&models.UserModel{}, "CollectsModels", &models.UserCollectModel{})
global.DB.SetupJoinTable(&models.MenuModel{}, "Banners", &models.MenuBannerModel{})
err := global.DB.Set("gorm:table_options", "Engine=InnoDB").
AutoMigrate(
&models.AdvertModel{},
&models.ArticleModel{},
&models.BannerModel{},
&models.CommentModel{},
&models.FadeBackModel{},
&models.MenuModel{},
&models.MenuBannerModel{},
&models.MessageModel{},
&models.TagModel{},
&models.UserModel{},
&models.UserCollectModel{},
&models.LoginInfoModel{},
)
if err != nil {
global.Log.Errorf("[ error ] 数据库迁移失败,%s", err.Error())
return
}
global.Log.Info("[ success ] 数据库迁移成功")
}
2. 注册到main中
main.go
package main
import (
"gin_blog/core"
"gin_blog/flag"
"gin_blog/global"
"gin_blog/routers"
)
func main() {
// 读取配置文件
core.InitConf()
// 初始化日志系统,并设置到全局变量
global.Log = core.InitLogger()
// 建立mysql连接
global.DB = core.InitGorm()
// 命令行参数解析
option := flag.Parse()
// 如果需要停止web项目,就不会执行下面的启动项目
if flag.IsWebStop(option) {
flag.SwitchOption(option)
return
}
flag.SwitchOption(option)
// 初始化路由
router := routers.InitRouter()
// 项目运行
addr := global.Config.System.Addr()
global.Log.Infof("项目启动成功,运行在: %s", addr)
router.Run(addr)
}
3. 执行命令,迁移表结构
go run main.py -db
6. 全站配置管理 API
6.1 单配置单API
6.1.1 站点信息
1. 查询站点信息
routers/settings_routers/go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
router.GET("settings_site_info", api.ApiGroupApp.SettingsApi.SettingsInfoView)
}
api/settings_api/settings_site_info.go
package settings_api
import (
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (s SettingsApi) SettingsInfoView(c *gin.Context) {
response.Ok(global.Config.SiteInfo, "请求成功", c)
}
接口测试
GET http://127.0.0.1:8089/api/settings/
2. 修改站点信息
routers/settings_routers/go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
// ......
router.PUT("settings_site_info", api.ApiGroupApp.SettingsApi.SettingsInfoUpdateView)
}
api/settings_api/settings_site_info_update.go
package settings_api
import (
"fmt"
"gin_blog/config"
"gin_blog/core"
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (SettingsApi) SettingsInfoUpdateView(c *gin.Context) {
var siteInfo config.SiteInfo
err := c.ShouldBindJSON(&siteInfo)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
global.Config.SiteInfo = siteInfo
// 修改yaml文件
err = core.SetYaml()
if err != nil {
global.Log.Error(err)
response.FailWithMessage(fmt.Sprintf("修改失败: %s", err.Error()), c)
}
response.OkWithMessage("修改成功", c)
}
core/conf.go
package core
import (
"fmt"
"gin_blog/config"
"gin_blog/global"
"gopkg.in/yaml.v2"
"io/fs"
"io/ioutil"
"log"
)
const ConfigFile = "settings.yaml"
// 读取yaml文件的配置
func InitConf() {
c := &config.Config{}
yamlConf, err := ioutil.ReadFile(ConfigFile)
if err != nil {
panic(fmt.Errorf("读取配置文件初失败: %s", err))
}
err = yaml.Unmarshal(yamlConf, c)
if err != nil {
log.Fatalf("配置文件初始化失败: %v", err)
}
log.Println("配置文件初始化成功!")
global.Config = c
}
func SetYaml() error {
byteData, err := yaml.Marshal(global.Config)
if err != nil {
return err
}
err = ioutil.WriteFile(ConfigFile, byteData, fs.ModePerm)
if err != nil {
return err
}
global.Log.Info("配置文件修改成功")
return nil
}
接口测试
PUT http://127.0.0.1:8089/api/settings/
请求 json
{
"created_at": "2023-12-06",
"bei_an": "渝ICP备20231206654号-1",
"title": "个人博客系统",
"qq_image": "http://www.aicx365.com/static/img/footer/qq.png",
"version": "0.1",
"email": "992838239@qq.com",
"wechat_image": "http://www.aicx365.com/static/img/footer/we_chat.png",
"name": "个人博客",
"job": "后端开发",
"addr": "重庆市",
"slogan": "Golang个人博客",
"slogan_En": "",
"web": "http://www.aicx365.com/",
"giteeUrl": ""
}
6.2.2 QQ
package config
type QQ struct {
AppID string `yaml:"app-id" json:"app_id"`
Key string `yaml:"key" json:"key"`
RedirectUrl string `yaml:"redirect-url" json:"redirect_url"` // 登录后的回调地址
}
6.2.3 Jwt
package config
type Jwt struct {
Secret string `yaml:"secret" json:"secret"` // 秘钥
Expires uint `yaml:"expires" json:"expires"` // 过期时间 单位是小时
IsSuer string `yaml:"is-suer" json:"iss_user"` // 颁发人
}
6.2.4 Email
package config
type Email struct {
Host string `yaml:"host" json:"host"` // 邮件的服务器地址
Port string `yaml:"port" json:"port"`
SendEmail string `yaml:"send-email" json:"send_email"` // 默认的发件人邮箱
Password string `yaml:"password" json:"password"` // 默认的发件人密码
SenderName string `yaml:"sender-name" json:"sender_namel"` // 默认的发件人名字
UseSSL bool `yaml:"use-ssl" json:"use_ssl"` // 是否使用ssl
UserTls string `yaml:"user-tls" json:"user_tls"`
}
1. 查询 Email 配置信息
1. 路由
routers/settings_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
// ......
router.GET("settings_email", api.ApiGroupApp.SettingsApi.SettingsEmailView)
}
2. API
api/settings_api/settings_email.go
package settings_api
import (
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (SettingsApi) SettingsEmailView(c *gin.Context) {
response.OkWithData(global.Config.Email, c)
}
2. 修改 Email 配置信息
1. 路由
routers/settings_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
// ......
router.PUT("settings_email_update", api.ApiGroupApp.SettingsApi.SettingsEmailUpdate)
}
2. API
api/settings_api/settings_email_update.go
package settings_api
import (
"fmt"
"gin_blog/config"
"gin_blog/core"
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (SettingsApi) SettingsEmailUpdate(c *gin.Context) {
var email config.Email
err := c.ShouldBindJSON(&email)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
global.Config.Email = email
// 修改yaml文件
err = core.SetYaml()
if err != nil {
global.Log.Error(err)
response.FailWithMessage(fmt.Sprintf("修改失败: %s", err.Error()), c)
}
response.OkWithMessage("修改成功", c)
}
6.2.5 Qiniu
package config
type QiNiu struct {
AccessKey string `yaml:"access-key" json:"access_key"`
SecretKey string `yaml:"secret-key" json:"secret-key"`
Bucket string `yaml:"bucket" json:"bucket"` // 存储桶名称
CDN string `yaml:"cdn" json:"cdn"` // 访问图片的地址前缀
Zone string `yaml:"zone" json:"zone"` // 存储的地区 华东 华北
Size uint `yaml:"size" json:"size"` // 存储大小限制 MB
IsUse bool `yaml:"is-use" json:"is_use"` // 是否启用七牛缓存图片
}
6.2.6 配置到Config结构体中
config/enter.go
package config
type Config struct {
// ......
QQ QQ `yaml:"qq"`
Email Email `yaml:"email"`
QiNiu QiNiu `yaml:"qi-niu"`
Jwt Jwt `yaml:"jwt"`
}
6.2.7 全局yaml文件
mysql:
host: 62.234.221.57
port: 33060
config: "charset=utf8mb4&parseTime=True&loc=Local"
db: blog
user: root
password: "123456"
max-idle-conns: 10
max-open-conns: 100
log_level: dev
logger:
level: info
prefix: '[blog]'
director: log
show-line: false
log-in-console: false
system:
host: 0.0.0.0
port: 8089
env: release
site-info:
created_at: "2023-12-06"
bei_an: 渝ICP备20231206654号-1
title: 个人博客系统
qq_image: http://www.aicx365.com/static/img/footer/qq.png
version: "0.1"
email: 992838239@qq.com
wechat_image: http://www.aicx365.com/static/img/footer/we_chat.png
name: 个人博客
job: 后端开发
addr: 重庆市
slogan: Golang个人博客
slogan_En: ""
web: http://www.aicx365.com/
gitee_Url: ""
jwt:
secret: xx
expires: 48
is-suer: xx
qq:
app-id: "xx"
key: xx
redirect-url: xx
email:
host: xx
port: xx
send-email: xx
password: xx
sender-name: xx
use-ssl: true
user-tls: xx
qi-niu:
is-use: false
access-key: xx
secret-key: xx
bucket: xx
cdn: xx
zone: xx
size: 5
6.2 多配置统一API
6.2.1 多配置查询
这样有个问题就是后面输出 API 文档的时候不好输出
1. 路由
router/settings_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
router.GET("settings/:name", api.ApiGroupApp.SettingsApi.SettingsInfoView)
}
2. API
api/settings_api/settings_info.go
package settings_api
import (
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
type SettingsUri struct {
Name string `uri:"name"`
}
func (SettingsApi) SettingsInfoView(c *gin.Context) {
var su SettingsUri
err := c.ShouldBindUri(&su)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
switch su.Name {
case "site":
response.OkWithData(global.Config.SiteInfo, c)
case "qq":
response.OkWithData(global.Config.QQ, c)
case "jwt":
response.OkWithData(global.Config.Jwt, c)
case "email":
response.OkWithData(global.Config.Email, c)
case "qi_niu":
response.OkWithData(global.Config.QiNiu, c)
default:
response.FailWithMessage("未匹配到正确的配置信息", c)
}
}
3. 根据不同的 url 执行不同的函数
api/settings/site/
api/settings/qq/
api/settings/jwt/
api/settings/email/
api/settings/qi_niu/
6.2.2 多配置修改
1. 路由
router/settings_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) SettingsRouters() {
// .....
router.PUT("settings/:name", api.ApiGroupApp.SettingsApi.SettingsInfoUpdateView)
}
2. API
api/settings_api/settings_info_update.go
package settings_api
import (
"fmt"
"gin_blog/config"
"gin_blog/core"
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func SiteInfoUpdate(c *gin.Context) error {
var siteInfo config.SiteInfo
err := c.ShouldBindJSON(&siteInfo)
if err != nil {
return err
}
global.Config.SiteInfo = siteInfo
return nil
}
func EmailUpdate(c *gin.Context) error {
var email config.Email
err := c.ShouldBindJSON(&email)
if err != nil {
return err
}
global.Config.Email = email
return nil
}
func JWTUpdate(c *gin.Context) error {
var jwt config.Jwt
err := c.ShouldBindJSON(&jwt)
if err != nil {
return err
}
global.Config.Jwt = jwt
return nil
}
func QQUpdate(c *gin.Context) error {
var qq config.QQ
err := c.ShouldBindJSON(&qq)
if err != nil {
return err
}
global.Config.QQ = qq
return nil
}
func QiNiuUpdate(c *gin.Context) error {
var qiNiu config.QiNiu
err := c.ShouldBindJSON(&qiNiu)
if err != nil {
return err
}
global.Config.QiNiu = qiNiu
return nil
}
func (SettingsApi) SettingsInfoUpdateView(c *gin.Context) {
var su SettingsUri
err := c.ShouldBindUri(&su)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
switch su.Name {
case "site":
err = SiteInfoUpdate(c)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
case "qq":
err = QQUpdate(c)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
case "jwt":
err = JWTUpdate(c)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
case "email":
err = EmailUpdate(c)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
case "qi_niu":
err = QiNiuUpdate(c)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
default:
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
// 修改yaml文件
err = core.SetYaml()
if err != nil {
global.Log.Error(err)
response.FailWithMessage(fmt.Sprintf("修改失败: %s", err.Error()), c)
return
}
response.OkWithMessage("修改成功", c)
}
3. 根据不同的 url 执行不同的函数
api/settings/site/
api/settings/qq/
api/settings/jwt/
api/settings/email/
api/settings/qi_niu/
7. 图片管理 API
6.3 图片上传API
6.3.1 上传单个图片
项目目录下创建/static/img/文件夹,用来保存用户上传的图片
1. API文件结构创建
api/images_api/enter.py
package images_api
type ImagesApi struct {
}
api/enter.go
package api
import (
"gin_blog/api/images_api"
"gin_blog/api/settings_api"
)
type ApiGroup struct {
SettingsApi settings_api.SettingsApi
ImageApi images_api.ImageUploadApi
}
var ApiGroupApp = new(ApiGroup)
api/images_api/image_upload.go
package images_api
import (
"fmt"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
// 上传单个图片,返回图片的URL
func (ImagesApi) ImageUploadView(c *gin.Context) {
fileHeader, err := c.FormFile("image")
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
fmt.Println(fileHeader.Header)
fmt.Println(fileHeader.Size)
fmt.Println(fileHeader.Filename)
}
// 打印结果
map[Content-Disposition:[form-data; name = "image"; filename = "加减组件 加号.png"; filename*=UTF-8''%E5%8A%A0%E5%87%8F%E7%BB%84%E4%BB%B6%20%E5%8A%A0%E5%8F%B7.png] Content-Type:[image/png]] // Header
3211 // Size 单位为kb
加减组件 加号.png // Filename
2. 配置路由
routers/images_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) ImagesRouters() {
router.POST("image/", api.ApiGroupApp.ImagesApi.ImageUploadView)
}
routers/enter.go
package routers
// ......
func InitRouter() *gin.Engine {
// ......
// 上传文件
routerGroupApp.ImagesRouters()
return router
}
3. 视图逻辑编写
api/images_api/image_upload.go
package images_api
import (
"fmt"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
// 上传单个图片,返回图片的URL
func (ImagesApi) ImageUploadView(c *gin.Context) {
fileHeader, err := c.FormFile("image")
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
fmt.Println(fileHeader.Header)
fmt.Println(fileHeader.Size)
fmt.Println(fileHeader.Filename)
}
4. API测试

6.3.2 上传多个图片
此接口同样兼容上传单个图片
1. API 编写
api/images_api/image_upload.go
// 接收并保存多个图片
func (ImagesApi) ImagesUploadView(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
fileList, ok := form.File["images"]
if !ok {
response.FailWithMessage("文件不存在", c)
return
}
for _, file := range fileList {
// 保存文件到指定目录
file_path := path.Join("static", "img", file.Filename)
err = c.SaveUploadedFile(file, file_path)
if err != nil {
global.Log.Error(err)
continue
}
}
response.OkWithMessage("图片上传成功", c)
}
2. 路由注册
routers/images_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) ImagesRouters() {
// ......
router.POST("images/", api.ApiGroupApp.ImagesApi.ImagesUploadView)
}
3. 测试接口

6.3.3 文件大小和保存路径配置及自动创建
config/image_upload.go
package config
type ImageUpload struct {
Path string `yaml:"path" json:"path"` // 文件保存路径
Size uint `yaml:"size" json:"size"` // 文件大小
}
config/enter.go
package config
type Config struct {
// ......
ImageUpload ImageUpload `yaml:"image-upload"`
}
settings.yaml
image-upload:
path: static/img
size: 2
api/images_api/image_upload.go
package images_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
"io/fs"
"os"
"path"
)
type ImageUploadResponse struct {
FileName string `json:"file_name"`
IsSuccess bool `json:"is_success"`
ErrorMsg string `json:"error_msg"`
}
// 上传多个图片
func (ImagesApi) ImagesUploadView(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
fileList, ok := form.File["images"]
if !ok {
response.FailWithMessage("文件不存在", c)
return
}
// 创建图片存放路径
basePath := global.Config.ImageUpload.Path
// 循环手动创建图片上传目录
//pathList := strings.Split(basePath, "/")
//for _, cuPath := range pathList {
// _, err = os.Stat(cuPath)
// if err != nil {
// err = os.Mkdir(cuPath, fs.ModePerm)
// if err != nil {
// global.Log.Errorf("创建文件失败:,%s", err.Error())
// }
// os.Chdir(cuPath)
// }
//}
// 递归创建图片上传目录
_, err = os.ReadDir(basePath)
if err != nil {
err = os.MkdirAll(global.Config.ImageUpload.Path, fs.ModePerm)
if err != nil {
global.Log.Errorf("文件创建失败:%s", err.Error())
return
}
}
var iurList []ImageUploadResponse
baseSize := global.Config.ImageUpload.Size
for _, file := range fileList {
// 校验文件大小,不允许超过配置文件的大小,单位为M
fileSize := float64(file.Size) / float64(1024*1024)
filePath := path.Join(basePath, file.Filename)
if fileSize >= float64(baseSize) {
iurList = append(iurList, ImageUploadResponse{
FileName: file.Filename,
IsSuccess: false,
ErrorMsg: fmt.Sprintf("文件大小为:%.2fMB,限制大小为:%dMB", fileSize, baseSize),
})
continue
}
iurList = append(iurList, ImageUploadResponse{
FileName: filePath, // 上传成功,返回文件的保存路径
IsSuccess: true,
ErrorMsg: "文件上传成功",
})
// 保存文件到配置文件中的指定目录
err = c.SaveUploadedFile(file, filePath)
if err != nil {
global.Log.Error(err)
continue
}
}
response.OkWithData(iurList, c)
}
6.3.4 文件上传白名单
判断文件后缀,如果与白名单中的后缀符合,则上传,否则拒绝保存
只能上传白名单中后缀的文件
utils/utils.go
package utils
func InList(key string, list []string) bool {
for _, k := range list {
if k == key {
return true
}
}
return false
}
api/images_api/image_upload.go
// 定义文件后缀白名单
var (
WhiteImageList = []string{ // 文件上传的白名单
"jpg",
"png",
"jpeg",
"ico",
"git",
"tif",
"svg",
"webp",
}
)
// 上传多个图片
func (ImagesApi) ImagesUploadView(c *gin.Context) {
// .......
for _, file := range fileList {
// 先判断文件后缀
nameList := strings.Split(file.Filename, ".")
fileSuffix := strings.ToLower(nameList[len(nameList)-1]) // 变成小写
if !utils.InList(fileSuffix, WhiteImageList) {
// 如果当前文件后缀不在在白名单中,则跳过该文件
iurList = append(iurList, ImageUploadResponse{
FileName: file.Filename,
IsSuccess: false,
ErrorMsg: fmt.Sprintf("文件格式不合法,请检查后重新上传"),
})
continue
}
// 校验文件大小,不允许超过配置文件的大小,单位为M
}
}
6.3.5 图片哈希校验并入库
utils/m_md5.go
package utils
import (
"crypto/md5"
"encoding/hex"
)
func Md5(src []byte) string {
m := md5.New()
m.Write(src)
res := hex.EncodeToString(m.Sum(nil))
return res
}
api/images_api/image_upload.go
// 上传多个图片
func (ImagesApi) ImagesUploadView(c *gin.Context) {
for _, file := range fileList {
// 先判断文件后缀
// 校验文件大小,不允许超过配置文件的大小,单位为M
// 图片哈希,
fileObj, err := file.Open()
if err != nil {
global.Log.Error(err.Error())
return
}
byteData, err := io.ReadAll(fileObj)
imageHash := utils.Md5(byteData)
// 从数据库中查询图片是否存在,如果有则直接返回,避免保存多个一样的文件
var bannerModel models.BannerModel
row := global.DB.Take(&bannerModel, "hash_key = ?", imageHash).RowsAffected
if row > 0 {
// 找到了
iurList = append(iurList, ImageUploadResponse{
FileName: bannerModel.Path, // 上传成功,返回文件的保存路径
IsSuccess: true,
ErrorMsg: "文件上传成功",
})
continue
}
// 保存文件到配置文件中的指定目录
err = c.SaveUploadedFile(file, filePath)
if err != nil {
global.Log.Error(err)
continue
}
// 保存图片到数据库中
global.DB.Create(&models.BannerModel{
Path: filePath,
Hash: imageHash,
Name: file.Filename,
})
}
response.OkWithData(iurList, c)
}
6.4 图片列表API
6.4.1 查询API
api/images_api/image_list.go
package images_api
import (
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (ImagesApi) ImageListView(c *gin.Context) {
var params models.PageParams
err := c.ShouldBindQuery(¶ms)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
var imageList models.BannerModel
// 数据总条数
count := global.DB.Select("id").Find(&imageList).RowsAffected
offSet := (params.Page - 1) * params.Limit
if offSet < 0 {
offSet = 0
}
global.DB.Limit(params.Limit).Offset(offSet).Find(&imageList)
response.OkWithPageList(count, &imageList, c)
return
}
2. 注册路由
routers/images_routers.go
package routers
import (
"gin_blog/api"
)
func (router *RouterGroup) ImagesRouters() {
router.GET("images/", api.ApiGroupApp.ImagesApi.ImageListView)
// ......
}
3. API测试

6.4.2 带分页功能
models/enter.go
type PageParams struct {
Page int `form:"page"`
Key string `form:"key"`
Limit int `form:"limit"`
Sort string `form:"sort"`
}
serive/common/page_list.go
package common
import (
"gin_blog/global"
"gin_blog/models"
"gorm.io/gorm"
)
type Option struct {
models.PageParams
Debug bool
}
// PageList 分页查询公共函数
func PageList[T any](model T, option Option) (count int64, list []T, err error) { // T为定义泛型
DB := global.DB
if option.Debug {
DB = global.DB.Session(&gorm.Session{ // 如果option.Debug 为True 则打印日志
Logger: global.MysqlLog,
})
}
query := DB.Model(model)
// 替换DB对象为上面返回的DB对象
count = query.Select("id").Find(&list).RowsAffected // 数据总条数,只查ID
offSet := (option.Page - 1) * option.Limit
if offSet < 0 {
offSet = 0
}
query := DB.Model(model) // 重置查询条件
// 替换DB对象为上面返回的DB对象
err = query.Limit(option.Limit).Offset(offSet).Find(&list).Error
return count, list, err
}
api/images_api/image_list.go
package images_api
import (
"gin_blog/models"
"gin_blog/models/response"
"gin_blog/serive/common"
"github.com/gin-gonic/gin"
)
func (ImagesApi) ImagesListView(c *gin.Context) {
var params models.PageParams
err := c.ShouldBindQuery(¶ms)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
count, list, err := common.PageList(&models.BannerModel{}, common.Option{
params,
true,
})
response.OkWithPageList(count, list, c)
return
}
6.4.3 部分展示日志
core/gorm.go
func InitGorm() *gorm.DB {
// ......
var mysqlLogger logger.Interface
if global.Config.System.Env == "debug" {
// 开发环境显示所有的sql语句
mysqlLogger = logger.Default.LogMode(logger.Info)
} else {
mysqlLogger = logger.Default.LogMode(logger.Error)
}
// 保存到全局变量中
global.MysqlLog = logger.Default.LogMode(logger.Info)
// ......
}
global/global.go
package global
import (
"gin_blog/config"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
Config *config.Config
DB *gorm.DB
Log *logrus.Logger
MysqlLog logger.Interface
)
serive/common/page_list.go
package common
import (
"gin_blog/global"
"gin_blog/models"
"gorm.io/gorm"
)
type Option struct {
models.PageParams
Debug bool
}
// PageList 分页查询公共函数
func PageList[T any](model T, option Option) (count int64, list T, err error) { // T为定义泛型
DB := global.DB
if option.Debug {
DB = global.DB.Session(&gorm.Session{ // 如果option.Debug 为True 则打印日志
Logger: global.MysqlLog,
})
}
// 替换DB对象为上面返回的DB对象
count = DB.Select("id").Find(&list).RowsAffected // 数据总条数,只查ID
offSet := (option.Page - 1) * option.Limit
if offSet < 0 {
offSet = 0
}
// 替换DB对象为上面返回的DB对象
err = DB.Limit(option.Limit).Offset(offSet).Error
return count, list, err
}
6.4.4 排序
sevice/common/page_list.go
// PageList 分页查询公共函数
func PageList[T any](model T, option Option) (count int64, list []T, err error) { // T为定义泛型
// ......
// 如果没有传排序条件,则按照创建时间倒序排列
if option.Sort == "" {
option.Sort = "created_at desc"
}
// .......
// 使用Order() 进行排序
err = DB.Limit(option.Limit).Offset(offSet).Order(option.Sort).Find(&list).Error
return count, list, err
}
6.5 图片批量删除API
1. 钩子函数删文件
models/banner_model.go
func (b *BannerModel) BeforeDelete(tx *gorm.DB) (err error) { // 先删除文件后删除数据库
if b.ImageType == ctype.Local {
// 如果是本地存储,除了删除数据库还要删除本地图片
err = os.Remove(b.Path)
if err != nil {
return err
}
}
// 七牛云存储
return nil
}
2. 删除数据库记录
models/enter.go
type RemoveParams struct {
IDList []uint `json:"id_list"`
}
api/images_api/image_remove.go
package images_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (ImagesApi) ImageRemoveView(c *gin.Context) {
var params models.RemoveParams
err := c.ShouldBindJSON(¶ms)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
var imageList []models.BannerModel
count := global.DB.Debug().Find(&imageList, params.IDList).RowsAffected
if count == 0 {
response.FailWithMessage("文件不存在", c)
return
}
deleteCount := global.DB.Debug().Delete(&imageList).RowsAffected
response.OkWithMessage(fmt.Sprintf("删除成功,共删除 %d 个", deleteCount), c)
return
}
3. 注册路由
routers/images_routers.go
package routers
import "gin_blog/api"
func (router *RouterGroup) ImageUploadRouters() {
// ......
router.DELETE("images/", api.ApiGroupApp.ImagesApi.ImageRemoveView)
}
4. API测试

6.6 图片名称修改
1. API
utils/valid.go
package utils
import (
"github.com/go-playground/validator/v10"
"reflect"
)
// GetValidMsg 返回结构体中的msg参数
func GetValidMsg(err error, obj any) string {
getObj := reflect.TypeOf(obj)
if errs, ok := err.(validator.ValidationErrors); ok {
// 断言成功
for _, e := range errs {
// 循环每一个错误信息,根据报错字段名,获取结构体的具体字段
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
return msg
}
}
}
return err.Error()
}
api/images_api/image_update.go
package images_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
"strings"
)
type ImageUpdateParams struct {
ID uint `json:"id" binding:"required" msg:"请选择文件"` // 参数校验,msg是校验未通过的提示信息
Name string `json:"name" binding:"required" msg:"请输入文件名称"`
}
func (ImagesApi) ImageUpdateView(c *gin.Context) {
var params ImageUpdateParams
err := c.ShouldBindJSON(¶ms)
if err != nil {
response.FailWithError(err, ¶ms, c)
return
}
var imageModel models.BannerModel
err = global.DB.Take(&imageModel, params.ID).Error
if err != nil {
response.FailWithMessage("文件不存在", c)
return
}
// 获取原名称的后缀
oldNameList := strings.Split(imageModel.Name, ".")
oldNameLast := oldNameList[len(oldNameList)-1]
fmt.Println(oldNameLast)
err = global.DB.Model(&imageModel).Update("name", params.Name+"."+oldNameLast).Error
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("图片名称修改成功", c)
return
}
2. 响应错误信息
models/response.go
func FailWithError(err error, obj any, c *gin.Context) {
msg := utils.GetValidMsg(err, obj)
FailWithMessage(msg, c)
}
3. 注册路由
package routers
import "gin_blog/api"
func (router *RouterGroup) ImageUploadRouters() {
// ......
router.PUT("images/", api.ApiGroupApp.ImagesApi.ImageUpdateView)
}
6.7 配置七牛云
1. 进入个人中心

2. 秘钥管理

3. AK和SK

4. 写入yaml文件中
qi-niu:
is-use: false
access-key: nrNLoQPykA56Hmq58qyJZh4H47heOB256_oXi6D6
secret-key: GSyLdL2n1kGb2qNKxBhzbJ-pNfWqh2pxCIuHpDkc
5. 首页的存储空间

6. 空间名称

如果想外部访问到需要改为公开空间

7. 配置到yaml文件
qi-niu:
is-use: false
access-key: nrNLoQPykA56Hmq58qyJZh4H47heOB256_oXi6D6
secret-key: GSyLdL2n1kGb2qNKxBhzbJ-pNfWqh2pxCIuHpDkc
bucket: hetu
8. 存储区域


8. 配置到yaml文件
七牛云的源码中定义了这些常量来表示机房
const (
// region code
RIDHuadong = RegionID("z0")
RIDHuadongZheJiang2 = RegionID("cn-east-2")
RIDHuabei = RegionID("z1")
RIDHuanan = RegionID("z2")
RIDNorthAmerica = RegionID("na0")
RIDSingapore = RegionID("as0")
)
qi-niu:
is-use: false
access-key: nrNLoQPykA56Hmq58qyJZh4H47heOB256_oXi6D6
secret-key: GSyLdL2n1kGb2qNKxBhzbJ-pNfWqh2pxCIuHpDkc
bucket: hetu
zone: z2
size: 5
9.配置加速域名



10.腾讯云配置加速域名


11. 配置到yaml文件
qi-niu:
is-use: true
access-key: nrNLoQPykA56Hmq58qyJZh4H47heOB256_oXi6D6
secret-key: GSyLdL2n1kGb2qNKxBhzbJ-pNfWqh2pxCIuHpDkc
bucket: hetu
cdn: image.aicx365.com/
zone: z2
size: 5
12. 文档和Go SDK
https://developer.qiniu.com/kodo/1238/go
13. 下载七牛云的开发包
go get github.com/qiniu/go-sdk/v7
6.8 使用七牛云操作图片
6.8.1 上传图片
plugins.qiniu/enter.go
package qiniu
import (
"bytes"
"context"
"errors"
"fmt"
"gin_blog/config"
"gin_blog/global"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"time"
)
// getToken 获取上传的Token
func getToken(q config.QiNiu) string {
accessKey := q.AccessKey
secertKey := q.SecretKey
bucket := q.Bucket
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secertKey)
upToken := putPolicy.UploadToken(mac)
return upToken
}
// getCfg 获取上传的配置
func getCfg(q config.QiNiu) storage.Config {
cfg := storage.Config{}
// 空间对应的机房
zone, _ := storage.GetRegionByID(storage.RegionID(q.Zone))
cfg.Zone = &zone
// 是否使用https 域名
cfg.UseHTTPS = false
// 上传是否使用CDN加速
cfg.UseCdnDomains = false
return cfg
}
func UploadImage(data []byte, imageName, prefix string) (filePath string, err error) {
if !global.Config.QiNiu.IsUse {
return "", errors.New("请启用七牛云存储")
}
// 文件名不能重复
q := global.Config.QiNiu
if q.AccessKey == "" || q.SecretKey == "" {
return "", errors.New("请配置accessKey及secretKey")
}
if float64(len(data))/1024/1024 > float64(q.Size) {
return "", errors.New("文件超过设定大小")
}
upToken := getToken(q)
cfg := getCfg(q)
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{
Params: map[string]string{},
}
dataLen := int64(len(data))
// 获取当前时间
now := time.Now().Format("20060102150405")
key := fmt.Sprintf("%s/%s__%s", prefix, now, imageName)
err = formUploader.Put(context.Background(), &ret, upToken, key, bytes.NewReader(data), dataLen, &putExtra)
if err != nil {
return "", err
}
return fmt.Sprintf("%s%s", q.CDN, ret.Key), nil
}
api/images_api/image_upload.go
package images_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/ctype"
"gin_blog/models/response"
"gin_blog/plugins/qiniu"
"gin_blog/utils"
"github.com/gin-gonic/gin"
"io"
"io/fs"
"os"
"path"
"strings"
)
type ImageUploadResponse struct {
FileName string `json:"file_name"`
IsSuccess bool `json:"is_success"`
ErrorMsg string `json:"error_msg"`
}
// 定义文件后缀白名单
var (
WhiteImageList = []string{ // 文件上传的白名单
"jpg",
"png",
"jpeg",
"ico",
"git",
"tif",
"svg",
"webp",
}
)
// 上传多个图片
func (ImagesApi) ImagesUploadView(c *gin.Context) {
// 创建图片存放路径
// 递归创建图片上传目录
for _, file := range fileList {
// 先判断文件后缀
// 校验文件大小,不允许超过配置文件的大小,单位为M
// 图片哈希,
// 从数据库中查询图片是否存在,如果有则直接返回,避免保存多个一样的文件
// 限制大小改为七牛云的限制大小
baseSize := global.Config.ImageUpload.Size
if global.Config.QiNiu.IsUse {
baseSize = global.Config.QiNiu.Size
}
// 保存文件到七牛云
if global.Config.QiNiu.IsUse {
filePath, err = qiniu.UploadImage(byteData, file.Filename, "gvb")
if err != nil {
global.Log.Error(err.Error())
continue
}
global.DB.Create(&models.BannerModel{
Path: filePath,
Hash: imageHash,
Name: file.Filename,
ImageType: ctype.QiNiu,
})
iurList = append(iurList, ImageUploadResponse{
FileName: filePath, // 上传成功,返回文件的保存路径
IsSuccess: true,
ErrorMsg: "文件上传到七牛云成功",
})
continue
}
// 保存文件到配置文件中的指定目录
// 保存图片到数据库中
}
response.OkWithData(iurList, c)
}
6.8.1 公有空间查看七牛云图片
直接在访问图片地址即可访问

6.8.2 私有空间查看七牛云图片
需要秘钥进行访问私有空间

8. 广告管理 API
8.1 新增广告
1. API
api/advert_api/enter.go
package advert_api
type AdvertApi struct {
}
api/advert_api/advert_create.go
package advert_api
import (
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
type AdvertParams struct {
Title string `json:"title" binding:"required" msg:"请输入标题"` // 显示的标题
Href string `json:"href" binding:"required,url" msg:"跳转链接格式错误"` // 跳转链接
Images string `json:"images" binding:"required,url" msg:"图片地址格式错误"` // 图片
IsShow bool `json:"is_show" binding:"required" msg:"请选择是否展示"` // 是否展示
}
// AdvertCreateView 创建广告
// @Tags 广告管理
// @Summary 创建广告
// @Description 描述,可以有多个
// @Param limit query string false "表示URL的query查询参数,false表示没有,true表示必填"
// @Param data body AdvertParams true "表示body的查询参数,false表示没有,true表示必填"
// @Router /api/adverts [post]
// @Produce json
// @Success 200 {object} response.Response{}
func (AdvertApi) AdvertCreateView(c *gin.Context) {
var ap AdvertParams
err := c.ShouldBindJSON(&ap)
if err != nil {
response.FailWithError(err,&ap, c) // 自动返回 请求参数结构体报错的msg信息
return
}
// 重复判断
var advert models.AdvertModel
err = global.DB.Take(&advert, "title = ?").Error
if err != nil {
response.FailWithMessage("广告已存在", c)
return
}
err = global.DB.Create(&models.AdvertModel{
Title: ap.Title,
Href: ap.Href,
Images: ap.Images,
IsShow: ap.IsShow,
}).Error
if err != nil {
global.Log.Error(err.Error())
response.FailWithMessage("添加广告失败", c)
return
}
response.OkWithMessage("添加广告成功", c)
}
api/enter.go
type ApiGroup struct {
// ......
AdvertApi advert_api.AdvertApi
}
2. 注册路由
routers/adverts_routers.go
package routers
import (
"gin_blog/api"
)
func (router RouterGroup) AdvertRouters() {
router.POST("adverts", api.ApiGroupApp.AdvertApi.AdvertCreateView)
}
routers/enter.go
package routers
func InitRouter() *gin.Engine {
// .......
// 广告相关API
routerGroupApp.AdvertRouters()
}
8.2 广告列表
1. API
api/advert_api/advert_list.go
package advert_api
import (
"gin_blog/models"
"gin_blog/models/response"
"gin_blog/service/common"
"github.com/gin-gonic/gin"
)
// AdvertListView 广告列表 生成API文档的配置
// @Tags 广告列表
// @Summary 广告列表
// @Description 描述,可以有多个
// @Param data query models.PageParams true "如果有body参数,则加上,没有则不加,body表示请求体查询参数,false表示没有,true表示必填"
// @Router /api/adverts [get] "表示get请求"
// @Produce json
// @Success 200 {object} response.PageListResponse{}
func (AdvertApi) AdvertListView(c *gin.Context) {
var params models.PageParams
err := c.ShouldBindQuery(¶ms)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
// 查询is_show 为true 的广告
count, list, err := common.PageList(&models.AdvertModel{IsShow:true}, common.Option{
params,
false,
})
response.OkWithPageList(count, list, c)
return
}
2. 注册路由
routers/adverts_routers.go
package routers
import (
"gin_blog/api"
)
func (router RouterGroup) AdvertRouters() {
router.GET("adverts", api.ApiGroupApp.AdvertApi.AdvertListView)
// ......
}
8.3 广告编辑
1. API
api/advert_api/advert_update.go
package advert_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (AdvertApi) AdvertUpdateView(c *gin.Context) {
id := c.Param("id")
var aup AdvertParams
err := c.ShouldBindJSON(&aup)
if err != nil {
fmt.Println(err.Error())
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
// 判断广告是否存在
var advert models.AdvertModel
err = global.DB.Take(&advert, id).Error
if err != nil {
response.FailWithMessage("广告不存在", c)
return
}
// 这里如果使用接收的参数对象,取修改is_show=false,是不会生效的,所以需要使用map来修改
err = global.DB.Debug().Model(&advert).Updates(map[string]any{
"title": aup.Title,
"href": aup.Href,
"images": aup.Images,
"is_show": aup.IsShow,
}).Error
if err != nil {
global.Log.Error(err.Error())
response.FailWithMessage("修改广告失败", c)
return
}
response.OkWithMessage("修改广告成功", c)
}
2. 注册路由
routers/adverts_routers.go
package routers
import (
"gin_blog/api"
)
func (router RouterGroup) AdvertRouters() {
// ......
router.PUT("adverts/:id", api.ApiGroupApp.AdvertApi.AdvertUpdateView)
}
3. 请求URL
http://127.0.0.1:8080/api/adverts/1/
4. 注意点
1. 参数传递报:required错误
type AdvertParams struct {
Title string `json:"title" binding:"required" msg:"请输入标题"` // 显示的标题
Href string `json:"href" binding:"required,url" msg:"跳转链接格式错误"` // 跳转链接
Images string `json:"images" binding:"required,url" msg:"图片地址格式错误"` // 图片
IsShow bool `json:"is_show" binding:"required" msg:"请选择是否展示"` // 是否展示
}
// 当ShouldBindJson 检测传的参数时,如果 IsShow 有 binding:"required" 的限制,当传is_show=false时,是无法绑定参数的,所以需要将 binding:"required" 删掉,不做校验
2. 将is_show 在数据库中存的值 修改为 false
// 这里使用参数对象直接修改,当is_show=false时,是不会真正取数据库修改字段的
err = global.DB.Debug().Model(&advert).Updates(&aup).Error
// 需要改成map来修改
err = global.DB.Debug().Model(&advert).Updates(map[string]any{
"title": aup.Title,
"href": aup.Href,
"images": aup.Images,
"is_show": aup.IsShow,
}).Error
5. structs 使用
3. 如果请求参数很多的话,一个一个写就很不方便了,所以用到了struct转map的第三方包:structs
go get github.com/fatih/structs
1. 需要转某字段
// 需要转哪个字段 就在后面写structs 标签, 如果不想转哪个字段,则 写 structs:"-" 来忽略某字段
type AdvertParams struct {
models.Model `structs:"-"` // 忽略这个字段
Title string `json:"title" binding:"required" msg:"请输入标题" structs:"title"` // 显示的标题
Href string `json:"href" binding:"required,url" msg:"跳转链接格式错误" structs:"href"` // 跳转链接
Images string `json:"images" binding:"required,url" msg:"图片地址格式错误" structs:"images"` // 图片
IsShow bool `json:"is_show" msg:"请选择是否展示" structs:"is_show"` // 是否展示
}
2. 调用方法
var advertP AdvertParams
c.ShouldBindJSON(&ap)
err = global.DB.Debug().Model(&advert).Updates(structs.Map(&aup)).Error // 结构体转成map
6. 修改原广告编辑代码
package advert_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/fatih/structs"
"github.com/gin-gonic/gin"
)
func (AdvertApi) AdvertUpdateView(c *gin.Context) {
id := c.Param("id")
var aup AdvertParams
err := c.ShouldBindJSON(&aup)
if err != nil {
response.FailWithError(err,&ap, c) // 自动返回 请求参数结构体报错的msg信息
return
}
// 判断广告是否存在
var advert models.AdvertModel
err = global.DB.Take(&advert, id).Error
if err != nil {
response.FailWithMessage("广告不存在", c)
return
}
// 这里使用structs 来自动转成map
err = global.DB.Debug().Model(&advert).Updates(structs.Map(&aup)).Error
//err = global.DB.Debug().Model(&advert).Updates(map[string]any{
// "title": aup.Title,
// "href": aup.Href,
// "images": aup.Images,
// "is_show": aup.IsShow,
//}).Error
if err != nil {
global.Log.Error(err.Error())
response.FailWithMessage("修改广告失败", c)
return
}
response.OkWithMessage("修改广告成功", c)
}
8.4 广告批量删除
1. API
api/advert_api/advert_update.go
package advert_api
import (
"fmt"
"gin_blog/global"
"gin_blog/models"
"gin_blog/models/response"
"github.com/gin-gonic/gin"
)
func (AdvertApi) AdvertRemoveView(c *gin.Context) {
var params models.RemoveParams
err := c.ShouldBindJSON(¶ms)
if err != nil {
response.FailWithCode(response.PARAMS_ERROR, c)
return
}
var advertList []models.AdvertModel
count := global.DB.Find(&advertList, params.IDList).RowsAffected
if count == 0 {
response.FailWithMessage("广告不存在", c)
return
}
deleteCount := global.DB.Delete(&advertList).RowsAffected
response.OkWithMessage(fmt.Sprintf("删除成功,共删除 %d 个", deleteCount), c)
return
}
2. 注册路由
routers/adverts_routers.go
package routers
import (
"gin_blog/api"
)
func (router RouterGroup) AdvertRouters() {
// ......
router.DELETE("adverts", api.ApiGroupApp.AdvertApi.AdvertRemoveView)
}
3. 请求URL
http://127.0.0.1:8080/api/adverts/
99. 自动生成API接口文档
1. 下载swag
go install github.com/swaggo/swag/cmd/swag@latest
# golang版本在1.19以上
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files
2. main文件加代码
2.1 将注释内容写在 main 文件的main函数上
...... 省略代码 .....
// @title gin_blog API文档
// @version 1.0
// @description gin_blog API文档
// @host 127.0.0.1:8089
// @BasePath /
func main() {
}
2.2 执行命令生成API文档
# 生成 api 文档
swag init
2.3 main中引入docs文件
package main
import (
_ "gin_blog/docs" // 忽略导入 swagger 生成文档目录
)
3. 配置访问路由
routers/enter.go
import (
swaggerFiles "github.com/swaggo/files"
gs "github.com/swaggo/gin-swagger"
)
func InitRouter() *gin.Engine {
.... 省略代码 ....
router := gin.Default()
router.GET("/swagger/*any", gs.WrapHandler(swaggerFiles.Handler)) // 添加路由
}
再次执行命令
# 生成 api 文档
swag init
4. 视图加注释
如上已加在新增广告和广告列表两个接口中了
// @Tags 广告列表
// @Summary 广告列表
// @Description 描述,可以有多个
// @Param limit query string false "如果有query参数,则加上,没有则不加,query 表示query查询参数,,false表示没有,true表示必填"
// @Param data query models.PageParams true "如果有body参数,则加上,没有则不加,body表示请求体查询参数,false表示没有,true表示必填"
// @Router /api/adverts [get] "表示get请求"
// @Produce json
// @Success 200 {object} response.Response{}
func (a *Api) UserApi(c *gin.Context) {}
5. 访问网站
# 浏览器访问
http://ip:host/swagger/index.html
6. 项目中的应用
1. 创建广告
// AdvertCreateView 创建广告
// @Tags 广告管理
// @Summary 创建广告
// @Description 描述,可以有多个
// @Param data body AdvertParams true "表示body的查询参数,false表示没有,true表示必填"
// @Router /api/adverts [post]
// @Produce json
// @Success 200 {object} response.Response{}
func (AdvertApi) AdvertCreateView(c *gin.Context) {}
2. 广告列表
// AdvertListView 广告列表
// @Tags 广告列表
// @Summary 广告列表
// @Description 描述,可以有多个
// @Param data query models.PageParams true "如果有body参数,则加上,没有则不加,body表示请求体查询参数,false表示没有,true表示必填"
// @Router /api/adverts [get] "表示get请求"
// @Produce json
// @Success 200 {object} response.PageListResponse{}
func (AdvertApi) AdvertListView(c *gin.Context) {}
3. 广告更新
// AdvertUpdateView 广告更新
// @Tags 广告管理
// @Summary 广告更新
// @Description 描述,可以有多个
// @Param data body AdvertParams true "广告参数"
// @Router /api/adverts/:id [put]
// @Produce json
// @Success 200 {object} response.Response{data=string}
func (AdvertApi) AdvertUpdateView(c *gin.Context) {}
4. 广告批量删除
// AdvertRemoveView 广告批量删除
// @Tags 广告管理
// @Summary 广告批量删除
// @Description 描述,可以有多个
// @Param data body models.RemoveParams true "广告ID列表"
// @Router /api/adverts [delete]
// @Produce json
// @Success 200 {object} response.Response{data=string}
func (AdvertApi) AdvertRemoveView(c *gin.Context) {}

浙公网安备 33010602011771号