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(&params)
	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(&params)
	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(&params)
	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(&params)
	if err != nil {
		response.FailWithError(err, &params, 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(&params)
	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(&params)
	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) {}
posted @ 2023-12-03 00:26  河图s  阅读(355)  评论(0)    收藏  举报