博客Gin框架后端开发日志

1 创建数据库

表结构如下图
在这里插入图片描述

2 创建 GitGub 项目仓库

在 GitHub 上创建 GinCoBlog仓库,并将其clone到本地

3 初始化项目

clone下来的目录下创建:

  • config // 配置文件所在目录
  • controller // 控制器所在目录
  • entity // 数据库实体类所在目录
  • logs // 运行日志所在目录
  • middleware // 中间件所在目录
  • model // 数据库连接所在目录
  • request // 参数实体类所在目录
  • router // 路由所在目录
  • temp // 缓存文件
  • utils // 工具所在目录
  • .gitignore // git 不上传的文件列表
  • Application.yaml // 项目常量定义列表
  • main.go // 项目主入口

3.1 加载依赖

## 加载 Gin 框架
go get -u github.com/gin-gonic/gin
## 加载 bcrypt 依赖
go get golang.org/x/crypto/bcrypt
## token依赖
go get github.com/dgrijalva/jwt-go
## 跨域依赖
go get github.com/gin-contrib/cors
## 日志依赖
go get github.com/sirupsen/logrus
## 邮箱依赖
go get gopkg.in/gomail.v2
## yaml依赖
go get gopkg.in/yaml.v2
## mysql驱动
go get gorm.io/driver/mysql
## 数据库依赖
go get gorm.io/gorm
## redis 驱动
go get github.com/redis/go-redis/v9
## UUID 生成器
go get -u github.com/satori/go.uuid

3.2 编写项目主入口

main.go中编写

/**  
 * @projectName:    GinCoBlog 
 * @package:        GinCoBlog 
 * @className:      main 
 * @author:     张杰  
 * @description:  TODO  
 * @date:    2023/10/4 9:23 
 * @version:    1.0 
 */
package main  
  
import (  
   "github.com/gin-gonic/gin"  
   "net/http")  
  
func main() {  
   router := gin.Default()  
   router.GET("/", func(c *gin.Context) {  
      c.JSON(http.StatusOK, "HelloWorld!")  
   })  
   err := router.Run(":4001")  
   if err != nil {  
      panic("项目启动失败!")  
      return  
   }  
}

运行:

go run main.go

测试:

在浏览器输入 127.0.0.1:4001
在这里插入图片描述

3.3 编写项目配置文件

Application.yaml

# 项目端口号  
port: 4001

config 目录下创建 ConfigDefault.go

package config  
  
import (  
   "gopkg.in/yaml.v2"  
   "os")  
  
// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
}  
  
// Default /** 获取 yaml 配置  
func Default() Config {  
   // 实例化配置对象  
   var configObj Config  
   // 读取配置文件  
   yamlFil, err := os.ReadFile("Application.yaml")  
   // 读取失败  
   if err != nil {  
      panic(err)  
   }  
   // 将读到的文件解析为配置对象  
   err = yaml.Unmarshal(yamlFil, &configObj)  
   // 解析失败  
   if err != nil {  
      panic(err)  
   }  
   // 抛出配置文件对象  
   return configObj  
}

main.go 下:

/**  
 * @projectName:    GinCoBlog * 
 * @package:        GinCoBlog * 
 * @className:      main * @author:     张杰  
 * @description:  TODO  
 * @date:    2023/10/4 9:23 * 
 * @version:    1.0 *
 */
package main  
// 引入相关依赖  
import (  
   "GinCoBlog/config"  
   "github.com/gin-gonic/gin"   "net/http")  
// 项目主方法  
func main() {  
   // 获取项目路由  
   router := gin.Default()  
   // 编写项目基础接口  
   router.GET("/", func(c *gin.Context) {  
      c.JSON(http.StatusOK, "HelloWorld!")  
   })  
   // 开启端口监听  
   err := router.Run(":" + config.Default().Port)  
   // 开启监听失败  
   if err != nil {  
      // 写入日志  
      panic("项目启动失败!")  
      return  
   }  
}

运行:

go run main.go

测试:

在浏览器输入 127.0.0.1:4001
![[gin_co_blog_assets/Pasted image 20231008150332.png]]

3.4 配置基础中间件

3.4.1 配置日志中间件

Application.yaml 中添加

# 日志配置  
log_config:  
  # 日志存储位置  
  path: 'logs/'  
  # 日志名称  
  name: 'gin.log'

config/ConfigDefalult.go中添加

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"` // 日志存放路径  
      Name string `yaml:"name"` // 日志名称  
   } `yaml:"log_config"`  
}

// LogFilePath /**日志存放地址  
var LogFilePath = Default().LogConfig.Path  
  
// LogFileName /**日志文件名  
var LogFileName = Default().LogConfig.Name

middleware 目录下创建 LoggerMiddleware.go

package middleware  
  
import (  
   "GinCoBlog/config"  
   "fmt"   "github.com/gin-gonic/gin"   "github.com/sirupsen/logrus"   "os"   "path"   "time")  
  
// LoggerToFile 日志记录到文件  
func LoggerToFile() gin.HandlerFunc {  
  
   logFilePath := config.LogFilePath  
   logFileName := config.LogFileName  
  
   //日志文件  
   fileName := path.Join(logFilePath, logFileName)  
  
   //写入文件  
   src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModeAppend)  
   if err != nil {  
      logrus.Error("err", err)
   }  
  
   //实例化  
   logger := logrus.New()  
  
   //设置输出  
   logger.Out = src  
  
   //设置日志级别  
   logger.SetLevel(logrus.DebugLevel)  
  
   //设置日志格式  
   logger.SetFormatter(&logrus.TextFormatter{})  
  
   return func(c *gin.Context) {  
      // 开始时间  
      startTime := time.Now()  
  
      // 处理请求  
      c.Next()  
  
      // 结束时间  
      endTime := time.Now()  
  
      // 执行时间  
      latencyTime := endTime.Sub(startTime)  
  
      // 请求方式  
      reqMethod := c.Request.Method  
  
      // 请求路由  
      reqUri := c.Request.RequestURI  
  
      // 状态码  
      statusCode := c.Writer.Status()  
  
      // 请求IP  
      clientIP := c.ClientIP()  
  
      // 日志格式  
      logger.Infof("| %3d | %13v | %15s | %s | %s |",  
         statusCode,  
         latencyTime,  
         clientIP,  
         reqMethod,  
         reqUri,  
      )  
   }  
}  
  
// LoggerToMongo 日志记录到 MongoDBfunc LoggerToMongo() gin.HandlerFunc {  
   return func(c *gin.Context) {  
  
   }}  
  
// LoggerToES 日志记录到 ESfunc LoggerToES() gin.HandlerFunc {  
   return func(c *gin.Context) {  
  
   }}  
  
// LoggerToMQ 日志记录到 MQfunc LoggerToMQ() gin.HandlerFunc {  
   return func(c *gin.Context) {  
  
   }
}

main.go 中挂载此中间件

// 挂载中间件  
router.Use(gin.Logger(), gin.Recovery(), middleware.LoggerToFile())

3.4.2 挂载代理中间件

main.go 中添加

// 加载代理中间件  
err := router.SetTrustedProxies([]string{"192.168.1.0/24"})  
if err != nil {  
   fmt.Println("代理失败!")  
   return  
}

3.4.3 配置跨域中间件

Application.yaml

# 跨域IP  
cors:  
  ip:  
    - 'http://127.0.0.1'  
    - 'http://39.101.72.168'  
    - 'http://localhost'  
  methods:  
    - 'GET'  
    - 'POST'  
    - 'PUT'  
    - 'DELETE'

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
   // redis 配置  
   RedisConfig struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      Password string `yaml:"password"`  
      DB       int    `yaml:"db"`  
   } `yaml:"redis_config"`  
   // 加密配置  
   Encryption struct {  
      // 私钥  
      PrivateKey struct {  
         Password string `yaml:"password"`  
      } `yaml:"private_key"`  
      // 盐值  
      Salt struct {  
         Password int `yaml:"password"`  
      } `yaml:"salt"`  
   } `yaml:"encryption"`  
   // token 配置  
   Token struct {  
      PrivateKey string `yaml:"private_key"`  
   } `yaml:"token"`  
   // 绕过中间件验证的地址  
   NotVerifyUrl []string `yaml:"not_verify_url"`  
   // 跨域中间件验证地址  
   Cors struct {  
      Ip      []string `yaml:"ip"`  
      Methods []string `yaml:"methods"`  
   } `yaml:"cors"`  
}

// Cors 跨域  
var Cors = Default().Cors

middleware 目录下创建 CorsMiddleware.go

package middleware  
  
import (  
   "GinCoBlog/config"  
   "github.com/gin-contrib/cors"   "github.com/gin-gonic/gin")  
  
// CorsMiddleware 跨域中间件  
func CorsMiddleware() gin.HandlerFunc {  
   corsConfig := cors.DefaultConfig()  
   allowAccess := config.Cors.Ip  
   corsConfig.AllowOrigins = allowAccess // 允许访问的域名  
   corsConfig.AllowMethods = config.Cors.Methods  
  
   return cors.New(corsConfig)  
}

main.go

// 挂载中间件  
router.Use(gin.Logger(), gin.Recovery(), middleware.CorsMiddleware(), middleware.LoggerToFile(), middleware.JwtVerifyMiddle())

3.4.4 编写Token验证中间件

Application.yaml

# 不验证的接口地址  
not_verify_url:  
  - '/api/user/phoneOccupy'  
  - '/api/user/emailOccupy'  
  - '/api/user/register'  
  - '/api/user/phoneLogin'  
  - '/api/user/emailLogin'  
  - '/api/user/sendEmailCode'  
  - '/api/user/forgotPassword'

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
   // redis 配置  
   RedisConfig struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      Password string `yaml:"password"`  
      DB       int    `yaml:"db"`  
   } `yaml:"redis_config"`  
   // 加密配置  
   Encryption struct {  
      // 私钥  
      PrivateKey struct {  
         Password string `yaml:"password"`  
      } `yaml:"private_key"`  
      // 盐值  
      Salt struct {  
         Password int `yaml:"password"`  
      } `yaml:"salt"`  
   } `yaml:"encryption"`  
   // token 配置  
   Token struct {  
      PrivateKey string `yaml:"private_key"`  
   } `yaml:"token"`  
   // 绕过中间件验证的地址  
   NotVerifyUrl []string `yaml:"not_verify_url"`  
}

utils/index.go

// IsContainArr /**Token URL过滤  
func IsContainArr(noVerify []string, requestUrl string) bool {  
   for _, str := range noVerify {  
      if str == requestUrl {  
         return true  
      }  
   }  
   return false  
}

service/UserService.go

// VerUserByToken 通过Token查找用户  
func VerUserByToken(token string) (string, error) {  
   // 验证数据库中是否存有此token  
   user, err := RedisClient().Get(ctx, token).Result()  
   return user, err  
}

middleware 目录下创建 TokenMiddleware.go

package middleware  
  
import (  
   "GinCoBlog/config"  
   "GinCoBlog/request"   "GinCoBlog/service"   "GinCoBlog/utils"   "github.com/dgrijalva/jwt-go"   "github.com/gin-gonic/gin"   "log")  
  
// JwtVerifyMiddle /**验证token  
func JwtVerifyMiddle() gin.HandlerFunc {  
   return func(c *gin.Context) {  
      //过滤是否验证token  
      if utils.IsContainArr(config.NotVerifyUrl, c.Request.URL.Path) {  
         return  
      }  
      token := c.GetHeader("Authorization")  
      if token == "" {  
         log.Println("")  
         utils.AuthorizationResult(c, "权限验证失败,无法访问系统资源!")  
         // 终止请求  
         c.Abort()  
         return  
      }  
      // 判断是否错误  
      claims := parseToken(c, token)  
      //验证token,并存储在请求中  
      c.Set("user", claims)  
   }  
}  
  
// 解析Token  
func parseToken(c *gin.Context, tokenString string) *request.TokenParams {  
   // 验证数据库中是否存有此token  
   user, err := service.VerUserByToken(tokenString)  
   // 没查到  
   if err != nil || user == "" {  
      utils.AuthorizationResult(c, "权限验证失败,无法访问系统资源!")  
      c.Abort()  
      return nil  
   }  
   //解析token  
   token, err := jwt.ParseWithClaims(tokenString, &request.TokenParams{}, func(token *jwt.Token) (interface{}, error) {  
      return []byte(config.TokenPrivateKey), nil  
   })  
   // 解析成功  
   if err != nil {  
      utils.AuthorizationResult(c, "权限验证失败,无法访问系统资源!")  
      c.Abort()  
      return nil  
   }  
   // 将Token内存的数据转化为 token.Claims   claims, ok := token.Claims.(*request.TokenParams)  
   // 转化失败  
   if !ok {  
      utils.AuthorizationResult(c, "登录失效!")  
      c.Abort()  
      return nil  
   }  
   // 抛出数据  
   return claims  
}

4 正式编写

4.1 封装路由

router 目录下新建 UserRouter.go

package router  
  
import (  
   "github.com/gin-gonic/gin"  
   "net/http")  
  
// UserRouter 定义用户路由(二级)
func UserRouter(router *gin.RouterGroup) {  
   router.GET("/", func(c *gin.Context) {  
      c.JSON(http.StatusOK, "HelloUser!")  
   })  
}

router 目录下新建 index.go

package router  
  
import "github.com/gin-gonic/gin"  
  
// SetupRouterGroup 项目主路由(一级)  
func SetupRouterGroup(router *gin.RouterGroup) {  
   // 调取用户路由
   UserRouter(router.Group("/user"))  
}

main.go

/**  
 * @projectName:    GinCoBlog 
 * @package:        GinCoBlog 
 * @className:      main 
 * @author:     张杰  
 * @description:  TODO  
 * @date:    2023/10/4 9:23 * @version:    1.0 
 */
package main  
  
// 引入相关依赖  
import (  
   "GinCoBlog/config"  
   routes "GinCoBlog/router"  
   "github.com/gin-gonic/gin"   "net/http")  
  
// 项目主方法  
func main() {  
   // 获取项目路由  
   router := gin.Default()  
   // 编写项目基础接口  
   router.GET("/", func(c *gin.Context) {  
      c.JSON(http.StatusOK, "HelloWorld!")  
   })  
   // 调用项目主路由  
   routes.SetupRouterGroup(router.Group("/api"))  
   // 开启端口监听  
   err := router.Run(":" + config.Default().Port)  
   // 开启监听失败  
   if err != nil {  
      // 写入日志  
      panic("项目启动失败!")  
      return  
   }  
}

4.2 提取控制器

controller 目录下创建 UserController.go

package controller  
  
import (  
   "github.com/gin-gonic/gin"  
   "net/http")  
  
// HelloUser 测试  
func HelloUser(c *gin.Context) {  
   // 返回数据  
   c.JSON(http.StatusOK, gin.H{"msg": "HelloUser!"})  
}

修改 router/UserRouter.go

package routes  
  
import (  
   "GinCoBlog/controller"  
   "github.com/gin-gonic/gin")  
  
// UserRouter 定义用户路由(二级)  
func UserRouter(router *gin.RouterGroup) {  
   // 用户相关接口  
   router.GET("/", controller.HelloUser)  
}

运行

go run main.go

测试

在浏览器输入 http://localhost:4001/api/user
在这里插入图片描述

4.3 编写数据库实体类

  1. SysUser
  2. ArticleType
  3. Article
  4. ArticleComments
  5. FeedBack

entity/SysUser.go

package entity  
  
// SysUser 用户表结构  
type SysUser struct {  
   UID           uint32 `json:"uid" gorm:"primaryKey;autoIncrement;not null"`                           // 主键 用户ID  
   UserName      string `json:"user_name"`                                                              // 用户名  
   Phone         string `json:"phone" gorm:"not null;unique;size:11"`                                   // 电话号码 不允许为空 唯一  
   HeadSculpture string `json:"head_sculpture" gorm:"default:'http://39.101.72.168:81/image/icon.jpg'"` // 头像 默认值  
   Password      string `json:"password" gorm:"type:text;not null"`                                     // 密码 默认值  
   Email         string `json:"email" gorm:"not null;unique"`                                           // 邮箱 不允许为空 唯一  
   Available     bool   `json:"available" gorm:"not null;default:0"`                                    // 是否注销 不允许为空 默认值  
   Limit         uint8  `json:"limit" gorm:"not null;default:2"`                                        // 权限 不允许为空 默认值  
   Integral      uint32 `json:"integral" gorm:"not null;default:0"`                                     //用户积分  
   Member        uint8  `json:"member" gorm:"not null;default:2"`                                       // 会员: 0 超级会员、1 会员、2 非会员  
   UUID          string `json:"uuid" gorm:"not null;type:uuid;default:uuid_generate_v4()"`              // 用户身份  
}  
  
// TableName /** 复写默认方法,设置表名  
func (SysUser) TableName() string {  
   return "sys_user"  
}

entity/ArticleType.go

package entity  
  
// ArticleType 文章类型表  
type ArticleType struct {  
   ID          uint32 `json:"id" gorm:"primaryKey;autoIncrement;not null"` // 文章类型ID  
   RootID      uint32 `json:"root_id" gorm:"not null"`                     // 文章类型根ID  
   TypeName    string `json:"type_name" gorm:"not null;unique"`            // 文章类型名称  
   TypeVisible bool   `json:"type_visible" gorm:"not null;default:1"`      // 类型是否显示  
   Order       uint8  `json:"order" gorm:"not null;unique"`                // 排序  
   Picture     string `json:"picture" gorm:"not null"`                     // 类型图片  
   AddStatus   bool   `json:"edit_status" gorm:"not null; default:1"`      // 是否可加入文章  
}  
  
// TableName /** 复写默认方法,设置表名  
func (ArticleType) TableName() string {  
   return "article_type"  
}

entity/Article.go

package entity  
  
// Article 用户表结构  
type Article struct {  
   ID             int32  `json:"id" gorm:"primaryKey;autoIncrement;not null"` // 文章ID  
   TypeId         int32  `json:"typeId" gorm:"not null"`                      // 文章类型ID  
   UserId         int32  `json:"userId" gorm:"not null"`                      // 用户ID  
   Title          string `json:"title" gorm:"not null;unique"`                // 文章标题  
   Context        string `json:"context" gorm:"not null"`                     // 文章内容  
   ArticleVisible bool   `json:"article_visible" gorm:"not null;default:1"`   // 文章是否显示  
   Read           int32  `json:"read" gorm:"not null;default:0"`              // 阅读量  
   Date           string `json:"date" gorm:"not null"`                        // 上传时间  
   Icon           string `json:"icon" gorm:"not null"`                        // 文章图像  
}  
  
// TableName /** 复写默认方法,设置表名  
func (Article) TableName() string {  
   return "article"  
}

entity/ArticleCommits.go

package entity  
  
// ArticleComments 文章评论表  
type ArticleComments struct {  
   ID        uint32 `json:"id" gorm:"primaryKey;autoIncrement;not null"` // 评论ID  
   ArticleID uint32 `json:"article" gorm:"not null"`                     // 文章ID  
   UserID    uint32 `json:"user_id" gorm:"not null"`                     // 用户ID  
   Context   string `json:"context" gorm:"not null"`                     // 评论内容  
}  
  
// TableName /** 复写默认方法,设置表名  
func (ArticleComments) TableName() string {  
   return "article_comments"  
}

entity/FeedBack.go

package entity  
  
// FeedBack 反馈表  
type FeedBack struct {  
   ID      uint32 `json:"id" gorm:"primaryKey;autoIncrement;not null"` // 评论ID  
   UserID  uint32 `json:"user_id" gorm:"not null"`                     // 用户ID  
   Context string `json:"context" gorm:"not null"`                     // 反馈内容  
}  
  
// TableName /** 复写默认方法,设置表名  
func (FeedBack) TableName() string {  
   return "feed_back"  
}

4.4 编写数据库配置

Application.yaml下添加

# 服务器配置  
dataBase:  
  # 服务器地址  
  host: 'localhost'  
  # 服务器端口  
  port: '3306'  
  # 用户名  
  userName: 'root'  
  # 密码  
  password: '555555'  
  # 数据库名  
  schema: 'gin_co_blog'
# redis  
redis_config:  
  host: '127.0.0.1'  
  port: '6379'  
  password: ''  
  db: 0

config/ConfigDefault.go 下修改

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port     string `yaml:"port"`  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`
   // redis 配置  
	RedisConfig struct {  
	   Host     string `yaml:"host"`  
	   Port     string `yaml:"port"`  
	   Password string `yaml:"password"`  
	   DB       int    `yaml:"db"`  
	} `yaml:"redis_config"`
}

4.5 创建数据库连接并自动生成数据库表

service 目录下创建 index.go

package service  
  
import (  
   "GinCoBlog/config"  
   "GinCoBlog/entity"   "fmt"   "gorm.io/driver/mysql"   "gorm.io/gorm")  
  
// Pool /** 创建数据库连接池  
func Pool() *gorm.DB {  
   // 获取数据库配置  
   dbConfig := config.Default().DataBase  
   fmt.Println(dbConfig.Host)  
   // 创建数据库连接  
   db, err := gorm.Open(mysql.Open(dbConfig.UserName+":"+dbConfig.Password+"@tcp("+dbConfig.Host+":"+dbConfig.Port+")/"+dbConfig.Schema+"?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})  
   // 连接失败  
   if err != nil {  
      panic("连接数据库失败")  
   }  
   // 抛出数据库连接池  
   return db  
}  
  
// VerDataBase /** 验证数据库/表是否已经创建  
func VerDataBase() {  
   // 获取数据库连接池  
   pool := Pool()  
   // 验证sys_user表是否存在  
   err := pool.AutoMigrate(&entity.SysUser{})
   err = pool.AutoMigrate(&entity.ArticleType{})  
   err = pool.AutoMigrate(&entity.Article{})  
   err = pool.AutoMigrate(&entity.ArticleComments{})  
   err = pool.AutoMigrate(&entity.FeedBack{})  
   // 表创建失败  
   if err != nil {  
      panic(err)  
   }  
}

// RedisClient Redis 数据库连接  
func RedisClient() *redis.Client {  
   // 拿到 Redis 数据库配置  
   redisConfig := config.Default().RedisConfig  
   rdb := redis.NewClient(&redis.Options{  
      Addr:     redisConfig.Host + ":" + redisConfig.Port,  
      Password: redisConfig.Password,  
      DB:       redisConfig.DB,  
   })  
   ctx := context.Background()  
   if _, err := rdb.Ping(ctx).Result(); err != nil {  
      log.Fatalln(err.Error())  
   }  
   return rdb  
}

main.go

/**  
 * @projectName:    GinCoBlog 
 * @package:        GinCoBlog 
 * @className:      main 
 * @author:     张杰  
 * @description:  TODO  
 * @date:    2023/10/4 9:23 
 * @version:    1.0 */
package main  
  
// 引入相关依赖  
import (  
   "GinCoBlog/config"  
   "GinCoBlog/middleware"   routes "GinCoBlog/router"  
   "GinCoBlog/service"   "fmt"   "github.com/gin-gonic/gin"   "net/http")  
  
// 项目主方法  
func main() {  
   // 获取项目路由  
   router := gin.Default()  
   // 挂载中间件  
   router.Use(gin.Logger(), gin.Recovery(), middleware.LoggerToFile())  
   // 加载代理中间件  
   err := router.SetTrustedProxies([]string{"192.168.1.0/24"})  
   if err != nil {  
      fmt.Println("代理失败!")  
      return  
   }  
   // 验证数据表是否存在  
   service.VerDataBase()  
   // 编写项目基础接口  
   router.GET("/", func(c *gin.Context) {  
      c.JSON(http.StatusOK, "HelloWorld!")  
   })  
   // 调用项目主路由  
   routes.SetupRouterGroup(router.Group("/api"))  
   // 开启端口监听  
   err = router.Run(":" + config.Default().Port)  
   // 开启监听失败  
   if err != nil {  
      // 写入日志  
      panic("项目启动失败!")  
      return  
   }  
}

4.6 定义返回值类型

config/ConfigDefault.go下加入

// Response 返回值类型  
type Response struct {  
   Code    int         `json:"code"` // 响应值  
   Message string      `json:"msg"`  // 提示信息  
   Data    interface{} `json:"data"` // 数据  
}

utils 下创建 index.go

// resultType 返回值方法  
func resultType(code int, msg string, data interface{}) config.Response {  
   return config.Response{  
      Code:    code,  
      Message: msg,  
      Data:    data,  
   }  
}  
  
// SuccessResult 成功响应  
func SuccessResult(c *gin.Context, msg string, data interface{}) {  
   c.JSON(http.StatusOK, resultType(http.StatusOK, msg, data))  
}  
  
// FailResult 错误响应  
func FailResult(c *gin.Context, msg string) {  
   c.JSON(http.StatusBadRequest, resultType(http.StatusBadRequest, msg, nil))  
}  
  
// ServerErrorResult 服务器错误响应  
func ServerErrorResult(c *gin.Context) {  
   c.JSON(http.StatusInternalServerError, resultType(http.StatusInternalServerError, "服务器错误!", nil))  
}  
  
// AuthorizationResult 权限错误响应  
func AuthorizationResult(c *gin.Context, msg string) {  
   c.JSON(http.StatusUnauthorized, resultType(http.StatusUnauthorized, msg, nil))  
}

4.7 编写用户名查重接口

接口地址:/api/user/userNameOccupy

service 目录下创建 UserService.go

package service  
  
import (  
   "GinCoBlog/entity"  
   "GinCoBlog/utils"   "errors")  
  
// 获取数据库连接池  
var pool = Pool()  
  
// UserNameOccupyService 用户名查重  
func UserNameOccupyService(userName string) (error, bool) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证用户名参数  
   // 验证是否为空  
   if utils.StrIsEmpty(userName) {  
      return errors.New("参数为空"), false  
   }  
   // 判断长度是否超出  
   if len(userName) > 15 {  
      return errors.New("参数超出 15 个字符"), false  
   }  
   // 判断用户名中是否含有非法字符串  
   // 通过调用 gorm api 进行查找数据库  
   pool.Where("user_name=?", userName).Find(&user)  
   // 判断查询的数组长度是否大于1  
   if len(user) > 1 {  
      return errors.New("数据库存储异常!"), false  
   }  
   // 返回用户是否存在  
   return nil, len(user) == 0  
}

requset/UserRequest.go

package request  
  
// UserNameOccupyParams 用户名查重参数  
type UserNameOccupyParams struct {  
   UserName string `json:"user_name" binding:"required,min=3,max=15"`  
}

controller/UserController.go下添加

// UserNameOccupy 用户名查重  
func UserNameOccupy(c *gin.Context) {  
    // 获取参数接口实例  
	var param request.UserNameOccupyParams  
	// 绑定参数  
	err := c.ShouldBindJSON(&param)  
	// 参数绑定失败  
	if err != nil {  
	   utils.FailResult(c, "参数错误!")  
	   return  
	}  
	// 调用接口查找用户名是否已存在  
	err, isUserName := service.UserNameOccupyService(param.UserName)  
	// 如果发生错误  
	if err != nil {  
	   utils.FailResult(c, err.Error())  
	   return  
	}  
	// 判断用户名是否存在  
	if isUserName {  
	   utils.FailResult(c, "用户名已存在!")  
	   return  
	}  
	// 如果用户名不存在  
	utils.SuccessResult(c, "可以使用", nil)
}

router/UserRouter.go下添加

// 用户名查重  
router.GET("/userNameOccupy", controller.UserNameOccupy)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.8 编写电话号码查重接口

接口地址:/api/user/phoneOccupy

Application.yaml 下添加:

# 正则  
regular:  
  # 电话号码正则  
  phone: '^(13[0-9]|14[5-9]|15[0-35-9]|16[6]|17[0-8]|18[0-9]|19[0-9]|147|166|17[0-1]|162)\d{8}$'  

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"` // 日志存放路径  
      Name string `yaml:"name"` // 日志名称  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"` // 电话号码正则
   } `yaml:"regular"`  
}
// PhoneReg /** 定义电话号码正则  
var PhoneReg = Default().Regular.Phone  
  
// EmailReg /** 定义邮箱正则  
var EmailReg = Default().Regular.Email

utils/index.go

// VerPhoneReg /** 验证电话号码格式  
func VerPhoneReg(phone string) bool {  
   phoneReg := regexp.MustCompile(config.PhoneReg)  
   return !phoneReg.MatchString(phone)  
}  

service 目录下创建 UserService.go

// PhoneOccupyService 电话号码查重  
func PhoneOccupyService(phone string) (error, bool) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证用户名参数  
   // 验证是否为空  
   if utils.StrIsEmpty(phone) {  
      return errors.New("参数为空"), false  
   }  
   // 判断长度是否超出  
   if len(phone) != 11 {  
      return errors.New("电话号码格式错误"), false  
   }  
   // 判断用户名中是否含有非法字符串  
   // 通过调用 gorm api 进行查找数据库  
   pool.Where("phone=?", phone).Find(&user)  
   // 判断查询的数组长度是否大于 1   if len(user) > 1 {  
      // 将错误写入日志  
      logrus.Error("数据库内存有多个相同的电话号码")  
      // 将信息返回  
      return errors.New("电话号码已存在"), false  
   }  
   // 返回用户是否存在  
   return nil, len(user) == 1  
}

requset/UserRequest.go

// PhoneOccupyParams 电话号码查重参数  
type PhoneOccupyParams struct {  
   Phone string `json:"phone" binding:"required,len=11"`  
}

controller/UserController.go

// PhoneOccupy 电话号码查重  
func PhoneOccupy(c *gin.Context) {  
   // 获取参数接口实例  
   var param request.PhoneOccupyParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 调用接口查找用户名是否已存在  
   err, isUserName := service.PhoneOccupyService(param.Phone)  
   // 如果发生错误  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 判断用户名是否存在  
   if isUserName {  
      utils.FailResult(c, "电话号码已存在!")  
      return  
   }  
   // 如果用户名不存在  
   utils.SuccessResult(c, "可以使用", nil)  
}

router/UserRouter.go

// 电话号码查重  
router.GET("/phoneOccupy", controller.PhoneOccupy)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.9 编写邮箱查重接口

接口地址:/api/user/emailOccupy

Application.yaml

# 正则  
regular:  
  # 电话号码正则  
  phone: '^(13[0-9]|14[5-9]|15[0-35-9]|16[6]|17[0-8]|18[0-9]|19[0-9]|147|166|17[0-1]|162)\d{8}$'  
  # 邮箱正则  
  email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"` // 日志存放路径  
      Name string `yaml:"name"` // 日志名称  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"` // 电话号码正则  
      Email string `yaml:"email"` // 邮箱正则  
   } `yaml:"regular"`  
}
// EmailReg /** 定义邮箱正则  
var EmailReg = Default().Regular.Email

utils/index.go

// VerEmailReg /** 验证电话号码格式  
func VerEmailReg(email string) bool {  
   emailReg := regexp.MustCompile(config.EmailReg)  
   return !emailReg.MatchString(email)  
}

service 目录下创建 UserService.go

// EmailOccupyService 邮箱查重  
func EmailOccupyService(email string) (error, bool) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证用户名参数  
   // 验证是否为空  
   if utils.StrIsEmpty(email) {  
      return errors.New("参数为空"), false  
   }  
   // 判断邮箱格式是否正确  
   if utils.VerEmailReg(email) {  
      return errors.New("邮箱格式错误"), false  
   }  
   // 判断用户名中是否含有非法字符串  
   // 通过调用 gorm api 进行查找数据库  
   pool.Where("email=?", email).Find(&user)  
   // 判断查询的数组长度是否大于 1   if len(user) > 1 {  
      // 将错误写入日志  
      logrus.Error("数据库内存有多个相同的邮箱")  
      // 将信息返回  
      return errors.New("邮箱已存在"), false  
   }  
   // 返回用户是否存在  
   return nil, len(user) == 1  
}

requset/UserRequest.go

// EmailOccupyParams 邮箱查重参数  
type EmailOccupyParams struct {  
   Email string `json:"email" binding:"required"`  
}

controller/UserController.go

// EmailOccupy 邮箱查重  
func EmailOccupy(c *gin.Context) {  
   // 获取参数接口实例  
   var param request.EmailOccupyParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 调用接口查找用户名是否已存在  
   err, isUserName := service.EmailOccupyService(param.Email)  
   // 如果发生错误  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 判断用户名是否存在  
   if isUserName {  
      utils.FailResult(c, "邮箱已存在!")  
      return  
   }  
   // 如果用户名不存在  
   utils.SuccessResult(c, "可以使用", nil)  
}

router/UserRouter.go

// 邮箱查重  
router.GET("/emailOccupy", controller.EmailOccupy)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.10 编写邮箱验证码接口

接口地址:/api/user/sendEmailCode

Application.yaml 下添加:

# 邮箱配置  
emailConfig:  
  emailAddress: 'co-blog@qq.com'  
  emailName: 'CoBlog站长'  
  password: 'qgiegwujfquqbjfd'  
  smtpServer: 'smtp.qq.com'  
  smtpPort: 587

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
}
// EmailConfig /** 邮箱发送配置  
var EmailConfig = Default().EmailConfig

utils/index.go

// generateVerificationCode /** 生成验证码  
func generateVerificationCode() string {  
   source := rand.NewSource(time.Now().UnixNano())  
   r := rand.New(source)  
   code := ""  
   for i := 0; i < 6; i++ {  
      code += fmt.Sprintf("%d", r.Intn(10))  
   }  
   return code  
}  
  
func SendEmail(email string) string {  
   // 生成验证码  
   code := generateVerificationCode()  
   // 创建消息  
   m := gomail.NewMessage()  
   // 设置发件地址和发件人  
   m.SetAddressHeader("From", config.EmailConfig.EmailAddress, config.EmailConfig.EmailName)  
   // 发送地址  
   m.SetHeader("To", email)  
   // 设置标题  
   m.SetHeader("Subject", "验证码")  
   // 设置内容  
   m.SetBody("text/html", `  
            <p>您好!</p>  
            <p>您的验证码是:<strong style="color:orangered;">`+code+`</strong></p>  
         <p>此验证码在 5 分钟内有效</p>  
            <p>如果不是您本人操作,请无视此邮件</p>  
        `)  
   // 使用 smtp发送邮件  
   s := gomail.NewDialer(config.EmailConfig.SmtpServer, config.EmailConfig.SmtpPort, config.EmailConfig.EmailAddress, config.EmailConfig.Password)  
  
   if err := s.DialAndSend(m); err != nil {  
      panic("发送失败!")  
   }  
   return code  
}

service 目录下创建 UserService.go

// SendMsgCodeService 发送邮箱验证码  
func SendMsgCodeService(email string) (error, bool) {  
   // 验证用户名参数  
   // 验证是否为空  
   if utils.StrIsEmpty(email) {  
      return errors.New("参数为空"), false  
   }  
   // 判断邮箱格式是否正确  
   if utils.VerEmailReg(email) {  
      return errors.New("邮箱格式错误"), false  
   }  
   // 判断用户名中是否含有非法字符串  
   // 发送验证码  
   code := utils.SendEmail(email)  
   // 判断生成的验证码格式是否正确  
   if len(code) != 6 {  
      return errors.New("验证码发送失败"), false  
   }  
   // 将 code 存入 redis 5 分钟有效期  
   if _, err := RedisClient().Set(ctx, email, code, 5*time.Minute).Result(); err != nil {  
      return errors.New("发送失败"), false  
   }  
   // 返回  
   return nil, true  
}

requset/UserRequest.go

// SendEmailParams 邮箱查重参数  
type SendEmailParams struct {  
   Email string `json:"email" binding:"required"`  
}

controller/UserController.go

// SendEmailCode 获取邮箱验证码  
func SendEmailCode(c *gin.Context) {  
   // 获取参数接口实例  
   var param request.SendEmailParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 调用接口发送验证码  
   err, sendStatus := service.SendMsgCodeService(param.Email)  
   // 如果发生错误  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 发送失败  
   if !sendStatus {  
      utils.FailResult(c, "发送失败!")  
      return  
   }  
   // 发送成功  
   utils.SuccessResult(c, "发送成功!", nil)  
}

router/UserRouter.go

// 获取邮箱验证码  
router.POST("/sendEmailCode", controller.SendEmailCode)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

邮箱截图
在这里插入图片描述

4.11 编写注册接口

接口地址:/api/user/register

Application.yaml 下添加:

# 加密  
encryption:  
  # 私钥  
  private_key:  
    password: '1b6b260b-67d1-4b5a-917e-10ec233329ca'  
  # 盐值  
  salt:  
    password: 10

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
   // redis 配置  
   RedisConfig struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      Password string `yaml:"password"`  
      DB       int    `yaml:"db"`  
   } `yaml:"redis_config"`  
   // 加密配置  
   Encryption struct {  
      // 私钥  
      PrivateKey struct {  
         Password string `yaml:"password"`  
      } `yaml:"private_key"`  
      // 盐值  
      Salt struct {  
         Password int `yaml:"password"`  
      } `yaml:"salt"`  
   } `yaml:"encryption"`  
}
// Encryption 加密配置  
var Encryption = Default().Encryption

utils/index.go

// CreateUUID 生成 uuid
func CreateUUID() string {  
   uCode := uuid.NewV4()  
   return uCode.String()  
}  
  
// EncryptionPassword 密码加密  
func EncryptionPassword(pwd string) string {  
   password, err := bcrypt.GenerateFromPassword([]byte(pwd+config.Encryption.PrivateKey.Password), config.Encryption.Salt.Password)  
   if err != nil {  
      return ""  
   }  
   return string(password)  
}

service 目录下创建 UserService.go

// RegisterService 注册  
func RegisterService(param *request.RegisterParams) (error, bool) {  
   // 判断数据是否为空  
   if utils.StrIsEmpty(param.UserName) || utils.StrIsEmpty(param.Phone) || utils.StrIsEmpty(param.Email) || utils.StrIsEmpty(param.Password) {  
      return errors.New("参数为空"), false  
   }  
   // 验证电话号码格式  
   if utils.VerPhoneReg(param.Phone) {  
      return errors.New("电话号码格式错误"), false  
   }  
   // 验证邮箱格式  
   if utils.VerEmailReg(param.Email) {  
      return errors.New("邮箱格式错误"), false  
   }  
   // 验证电话号码是否已存在  
   if err, _ := PhoneOccupyService(param.Phone); err != nil {  
      return err, false  
   }  
   // 验证邮箱是否已存在  
   if err, _ := EmailOccupyService(param.Email); err != nil {  
      return err, false  
   }  
   // 判断密码格式  
   if len(param.Password) != 32 || len(param.VerPassword) != 32 {  
      return errors.New("密码为空"), false  
   }  
   // 判断两次密码是否一致  
   if param.Password != param.VerPassword {  
      return errors.New("两次密码不一致"), false  
   }  
   // 判断验证码是否正确  
   result, err := RedisClient().Get(ctx, param.Email).Result()  
   if err != nil {  
      return errors.New("验证码失效"), false  
   }  
   if result != param.EmailCode {  
      return errors.New("验证码错误!"), false  
   }  
   // 将密码进行二次加密  
   encryptionPwd := utils.EncryptionPassword(param.Password)  
   // 将用户数据存入数据库  
   if err := pool.Create(&entity.SysUser{  
      UserName: param.UserName,  
      Phone:    param.Phone,  
      Password: encryptionPwd,  
      Email:    param.Email,  
      UUID:     utils.CreateUUID(),  
   }).Error; err != nil {  
      return errors.New("注册失败!"), false  
   }  
   // 判断  
   return nil, true  
}

requset/UserRequest.go

// RegisterParams 注册参数  
type RegisterParams struct {  
   UserName    string `json:"user_name" binding:"required,min=3,max=15"`  
   Phone       string `json:"phone" binding:"required,len=11"`  
   Email       string `json:"email" binding:"required"`  
   EmailCode   string `json:"email_code" binding:"required,len=6"`  
   Password    string `json:"password" binding:"required,len=32"`  
   VerPassword string `json:"ver_password" binding:"required,len=32"`  
}

controller/UserController.go

// Register 注册  
func Register(c *gin.Context) {  
   // 获取参数接口实例  
   var param request.RegisterParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 验证完成后  
   err, status := service.RegisterService(&param)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 注册失败  
   if !status {  
      utils.FailResult(c, "注册失败")  
      return  
   }  
   // 注册成功  
   utils.SuccessResult(c, "注册成功!", nil)  
}

router/UserRouter.go

// 注册  
router.POST("/register", controller.Register)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.12 编写电话号码登录接口

接口地址:/api/user/phoneLogin

Application.yaml 下添加:

# token 配置  
token:  
  private_key: 'af28c9fe-9426-41dd-8ea7-cba762ec0f1e'

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
   // redis 配置  
   RedisConfig struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      Password string `yaml:"password"`  
      DB       int    `yaml:"db"`  
   } `yaml:"redis_config"`  
   // 加密配置  
   Encryption struct {  
      // 私钥  
      PrivateKey struct {  
         Password string `yaml:"password"`  
      } `yaml:"private_key"`  
      // 盐值  
      Salt struct {  
         Password int `yaml:"password"`  
      } `yaml:"salt"`  
   } `yaml:"encryption"`  
   // token 配置  
   Token struct {  
      PrivateKey string `yaml:"private_key"`  
   } `yaml:"token"`
}

// TokenPrivateKey Token 私钥配置  
var TokenPrivateKey = Default().Token.PrivateKey  
  
// TokenEffectAge Token 生命周期配置  
const TokenEffectAge = 1 * 24 * time.Hour

utils/index.go

// ComparePassword 密码验证  
func ComparePassword(hashPwd string, pwd string) bool {  
   err := bcrypt.CompareHashAndPassword([]byte(hashPwd), []byte(pwd+config.Encryption.PrivateKey.Password))  
   if err != nil {  
      return false  
   }  
   return true  
}  
  
// GenerateToken 生成 Token
func GenerateToken(claims *request.TokenParams) string {  
   //设置token有效期,也可不设置有效期,采用redis的方式  
   //   1)将token存储在redis中,设置过期时间,token如没过期,则自动刷新redis过期时间,  
   //   2)通过这种方式,可以很方便的为token续期,而且也可以实现长时间不登录的话,强制登录  
   claims.ExpiresAt = time.Now().Add(config.TokenEffectAge).Unix()  
   //生成token  
   sign, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(config.TokenPrivateKey))  
   if err != nil {  
      //这里因为项目接入了统一异常处理,所以使用panic并不会使程序终止,如不接入,可使用原始方式处理错误  
      log.Panicln("Token生成异常")  
      return ""  
   }  
   return sign  
}

service 目录下创建 UserService.go

// PhoneLoginService 电话号码登录  
func PhoneLoginService(param *request.PhoneLoginParams) (error, bool, string) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证参数为空  
   if utils.StrIsEmpty(param.Phone) || utils.StrIsEmpty(param.Password) {  
      return errors.New("参数为空"), false, ""  
   }  
   // 验证电话号码格式  
   if utils.VerPhoneReg(param.Phone) {  
      return errors.New("电话号码格式错误"), false, ""  
   }  
   // 验证密码格式  
   if len(param.Password) != 32 {  
      return errors.New("密码格式错误"), false, ""  
   }  
   // 判断此用户是否存在  
   // 通过电话号码查询用户  
   err := pool.Where("phone=?", param.Phone).Find(&user).Error  
   if err != nil {  
      return errors.New("登录失败"), false, ""  
   }  
   // 通过判断用户列表长度,判断是否有此用户  
   if len(user) < 1 {  
      return errors.New("此用户暂未注册!"), false, ""  
   }  
   // 验证密码正确性  
   if !utils.ComparePassword(user[0].Password, param.Password) {  
      return errors.New("账号/密码错误!"), false, ""  
   }  
   // 生成 Token   token := utils.GenerateToken(&request.TokenParams{  
      UserInfo:       user[0],  
      StandardClaims: jwt.StandardClaims{},  
   })  
   if utils.StrIsEmpty(token) {  
      return errors.New("登录失败"), false, ""  
   }  
   // 将 Token 存入 Redis   if _, err := RedisClient().Set(ctx, token, param.Phone, config.TokenEffectAge).Result(); err != nil {  
      return errors.New("登录失败"), false, ""  
   }  
  
   return nil, true, token  
}

requset/UserRequest.go

// PhoneLoginParams 电话号码登录参数  
type PhoneLoginParams struct {  
   Phone    string `json:"phone" binding:"required,len=11"`  
   Password string `json:"password" binding:"required,len=32"`  
}  
  
// TokenParams 定义 Token 类型  
type TokenParams struct {  
   UserInfo           entity.SysUser // 用户信息  
   jwt.StandardClaims                // token 配置  
}

controller/UserController.go

// PhoneLogin 电话号码登录  
func PhoneLogin(c *gin.Context) {  
   // 拿到电话号码登录参数实体类  
   var param request.PhoneLoginParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 验证完成后  
   err, status, token := service.PhoneLoginService(&param)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 注册失败  
   if !status {  
      utils.FailResult(c, "注册失败")  
      return  
   }  
   // 注册成功  
   utils.SuccessResult(c, "登录成功!", map[string]interface{}{"token": token})  
}

router/UserRouter.go

// 电话号码登录  
router.POST("/phoneLogin", controller.PhoneLogin)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.13 编写邮箱登录接口

接口地址:/api/user/emailLogin

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go

// EmailLoginService 邮箱登录  
func EmailLoginService(param *request.EmailLoginParams) (error, bool, string) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证参数为空  
   if utils.StrIsEmpty(param.Email) || utils.StrIsEmpty(param.Password) {  
      return errors.New("参数为空"), false, ""  
   }  
   // 验证电话号码格式  
   if utils.VerEmailReg(param.Email) {  
      return errors.New("电话号码格式错误"), false, ""  
   }  
   // 验证密码格式  
   if len(param.Password) != 32 {  
      return errors.New("密码格式错误"), false, ""  
   }  
   // 判断此用户是否存在  
   // 通过电话号码查询用户  
   err := pool.Where("email=?", param.Email).Find(&user).Error  
   if err != nil {  
      return errors.New("登录失败"), false, ""  
   }  
   // 通过判断用户列表长度,判断是否有此用户  
   if len(user) < 1 {  
      return errors.New("此用户暂未注册!"), false, ""  
   }  
   // 验证密码正确性  
   if !utils.ComparePassword(user[0].Password, param.Password) {  
      return errors.New("账号/密码错误!"), false, ""  
   }  
   // 生成 Token   token := utils.GenerateToken(&request.TokenParams{  
      UserInfo:       user[0],  
      StandardClaims: jwt.StandardClaims{},  
   })  
   if utils.StrIsEmpty(token) {  
      return errors.New("登录失败"), false, ""  
   }  
   // 将 Token 存入 Redis   if _, err := RedisClient().Set(ctx, token, param.Email, config.TokenEffectAge).Result(); err != nil {  
      return errors.New("登录失败"), false, ""  
   }  
  
   return nil, true, token  
}

requset/UserRequest.go

// EmailLoginParams 邮箱登录参数  
type EmailLoginParams struct {  
   Email    string `json:"email" binding:"required"`  
   Password string `json:"password" binding:"required,len=32"`  
}

controller/UserController.go

// EmailLogin 邮箱登录  
func EmailLogin(c *gin.Context) {  
   // 拿到电话号码登录参数实体类  
   var param request.EmailLoginParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 验证完成后  
   err, status, token := service.EmailLoginService(&param)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 注册失败  
   if !status {  
      utils.FailResult(c, "注册失败")  
      return  
   }  
   // 注册成功  
   utils.SuccessResult(c, "登录成功!", map[string]interface{}{"token": token})  
}

router/UserRouter.go

// 邮箱登录  
router.POST("/emailLogin", controller.EmailLogin)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.14 编写忘记密码接口

接口地址:/api/user/forgotPassword

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go

// ForgotPassword 忘记密码  
func ForgotPassword(param *request.ForgotPasswordParams) (error, bool) {  
   // 创建数据库数组对象  
   var user []entity.SysUser  
   // 验证参数为空  
   if utils.StrIsEmpty(param.Email) || utils.StrIsEmpty(param.EmailCode) || utils.StrIsEmpty(param.NewPassword) || utils.StrIsEmpty(param.VerPassword) {  
      return errors.New("参数为空"), false  
   }  
   // 验证邮箱格式  
   if utils.VerEmailReg(param.Email) {  
      return errors.New("邮箱格式错误"), false  
   }  
   // 验证密码格式  
   if len(param.NewPassword) != 32 || len(param.VerPassword) != 32 {  
      return errors.New("密码格式错误"), false  
   }  
   // 验证新密码和验证密码是否一致  
   if param.NewPassword != param.VerPassword {  
      return errors.New("两次密码不一致"), false  
   }  
   // 验证验证码格式  
   if len(param.EmailCode) != 6 {  
      return errors.New("验证码格式错误"), false  
   }  
   // 验证验证码是否正确  
   if result, err := RedisClient().Get(ctx, param.Email).Result(); err != nil || result != param.EmailCode {  
      return errors.New("验证码错误"), false  
   }  
   // 删除 redis 中的验证码  
   if _, err := RedisClient().Del(ctx, param.Email).Result(); err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 判断此用户是否存在  
   // 通过邮箱查询用户  
   err := pool.Where("email=?", param.Email).Find(&user).Error  
   if err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 通过判断用户列表长度,判断是否有此用户  
   if len(user) < 1 {  
      return errors.New("账号不存在"), false  
   }  
   // 对密码进行加密  
   encryptionPassword := utils.EncryptionPassword(param.NewPassword)  
   // 修改密码 SQL   err = pool.Model(&entity.SysUser{}).Where("uid", user[0].UID).Update("password", encryptionPassword).Error  
   if err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 修改成功  
   return nil, true  
}

requset/UserRequest.go

// ForgotPasswordParams 忘记密码参数  
type ForgotPasswordParams struct {  
   Email       string `json:"email" binding:"required"`  
   EmailCode   string `json:"email_code" binding:"required,len=6"`  
   NewPassword string `json:"new_password" binding:"required,len=32"`  
   VerPassword string `json:"ver_password" binding:"required,len=32"`  
}

controller/UserController.go

func ForgotPassword(c *gin.Context) {  
   // 获取参数接口实例  
   var param request.ForgotPasswordParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   // 参数绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误!")  
      return  
   }  
   // 验证完成后  
   err, status := service.ForgotPassword(&param)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 注册失败  
   if !status {  
      utils.FailResult(c, "修改失败")  
      return  
   }  
   // 注册成功  
   utils.SuccessResult(c, "修改成功!", nil)  
}

router/UserRouter.go

// 忘记密码  
router.POST("/forgotPassword", controller.ForgotPassword)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.15 编写查询用户信息接口

接口地址:/api/user/userInfo

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go


requset/UserRequest.go


controller/UserController.go

// UserInfo 用户信息  
func UserInfo(c *gin.Context) {  
   // 获取用户信息  
   user, _ := c.Get("user")  
   // 判断用户信息是否存在  
   if user == nil {  
      utils.FailResult(c, "登陆失效,请重新登录!")  
      return  
   }  
   // 将user转化为 TokenParam类型  
   tokenParam, ok := user.(*request.TokenParams)  
   // 判断是否转化正确  
   if !ok {  
      utils.FailResult(c, "登陆失效,请重新登录!")  
      return  
   }  
   // 返回用户信息  
   utils.SuccessResult(c, "获取成功!", request.UserResponse{  
      Name:          tokenParam.UserInfo.UserName,  
      Phone:         tokenParam.UserInfo.Phone,  
      HeadSculpture: tokenParam.UserInfo.HeadSculpture,  
      Email:         tokenParam.UserInfo.Email,  
      Limit:         tokenParam.UserInfo.Limit,  
      Integral:      tokenParam.UserInfo.Integral,  
      Member:        tokenParam.UserInfo.Member,  
      CreateTime:    tokenParam.UserInfo.CreateTime.String(),  
      UpdateTime:    tokenParam.UserInfo.UpdateTime.String(),  
      UUID:          tokenParam.UserInfo.UUID,  
   })  
}

router/UserRouter.go

// 获取用户信息  
router.GET("/userInfo", controller.UserInfo)

运行

go run main.go

测试

打开 postman/apiPost进行测试
![[gin_co_blog_assets/Pasted image 20231012144124.png]]

4.16 编写修改用户信息接口

接口地址:/api/user/userNameOccupy

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go

// GetCacheUser 获取缓存里的token信息  
func GetCacheUser(c *gin.Context) (error, *request.TokenParams) {  
   // 获取用户信息  
   user, _ := c.Get("user")  
   // 判断用户信息是否存在  
   if user == nil {  
      return errors.New("登录状态错误,请重新登录"), nil  
   }  
   // 将user转化为 TokenParam类型  
   tokenParam, ok := user.(*request.TokenParams)  
   // 判断是否转化正确  
   if !ok {  
      return errors.New("登录状态错误,请重新登录"), nil  
   }  
   return nil, tokenParam  
}

service 目录下创建 UserService.go

// ModifyUserInfoService ModifyUserInfo 修改用户信息  
func ModifyUserInfoService(param *request.ModifyUserInfoParams, userInfo *entity.SysUser) (error, bool) {  
   // 判断参数是否为空  
   if utils.StrIsEmpty(param.UserName) || utils.StrIsEmpty(param.Email) || utils.StrIsEmpty(param.Phone) || utils.StrIsEmpty(param.HeadSculpture) {  
      return errors.New("参数为空"), false  
   }  
   // 验证电话号码格式  
   if utils.VerPhoneReg(param.Phone) {  
      return errors.New("电话号码格式错误"), false  
   }  
   // 验证验证码格式  
   if len(param.EmailCode) != 6 {  
      return errors.New("验证码格式错误"), false  
   }  
   // 验证验证码是否正确  
   if result, err := RedisClient().Get(ctx, userInfo.Email).Result(); err != nil || result != param.EmailCode {  
      return errors.New("验证码错误"), false  
   }  
   // 修改数据  
   userInfo.UserName = param.UserName  
   userInfo.Phone = param.Phone  
   userInfo.Email = param.Email  
   userInfo.HeadSculpture = param.HeadSculpture  
   // 修改数据库  
   err := pool.Model(&entity.SysUser{}).Where("uid", userInfo.UID).Updates(userInfo).Error  
   // 修改失败  
   if err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 修改成功  
   return nil, true  
}

requset/UserRequest.go

// ModifyUserInfoParams 修改用户信息参数  
type ModifyUserInfoParams struct {  
   UserName      string `json:"user_name" binding:"required,min=3,max=15"`  
   Phone         string `json:"phone" binding:"required,len=11"`  
   Email         string `json:"email" binding:"required"`  
   HeadSculpture string `json:"head_sculpture" binding:"required"`  
   EmailCode     string `json:"email_code" binding:"required,len=6"`  
}

controller/UserController.go

// ModifyUserInfo 修改用户信息  
func ModifyUserInfo(c *gin.Context) {  
   var params request.ModifyUserInfoParams  
   err := c.ShouldBindJSON(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   err, tokenInfo := utils.GetCacheUser(c)  
   if err != nil {  
      utils.AuthorizationResult(c, err.Error())  
      return  
   }  
   err, status := service.ModifyUserInfoService(&params, &tokenInfo.UserInfo)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   if !status {  
      utils.FailResult(c, "修改失败")  
      return  
   }  
   utils.SuccessResult(c, "修改成功", nil)  
}

router/UserRouter.go

// 修改用户信息  
router.POST("/modifyUserInfo", controller.ModifyUserInfo)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.17 编写修改用户密码接口

接口地址:/api/user/userNameOccupy

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go

// ModifyPasswordService 修改密码  
func ModifyPasswordService(params *request.ModifyPasswordParams, userInfo *entity.SysUser) (error, bool) {  
   // 判断参数为空  
   if utils.StrIsEmpty(params.OldPassword) || utils.StrIsEmpty(params.NewPassword) {  
      return errors.New("参数为空"), false  
   }  
   // 判断新老密码是否一样  
   if params.OldPassword == params.NewPassword {  
      return errors.New("两次输入的密码一样"), false  
   }  
   // 验证密码正确性  
   if !utils.ComparePassword(userInfo.Password, params.OldPassword) {  
      return errors.New("旧密码错误!"), false  
   }  
   if params.NewPassword != params.VerifiedPassword {  
      return errors.New("新密码与验证密码不一致!"), false  
   }  
   // 将密码进行二次加密  
   encryptionPwd := utils.EncryptionPassword(params.NewPassword)  
   // 修改数据库  
   err := pool.Model(&entity.SysUser{}).Where("uid", userInfo.UID).Update("password", encryptionPwd).Error  
   if err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 修改成功  
   return nil, true  
}

requset/UserRequest.go

// ModifyPasswordParams 修改密码参数  
type ModifyPasswordParams struct {  
   OldPassword      string `json:"old_password" binding:"required,len=32"`  
   NewPassword      string `json:"new_password" binding:"required,len=32"`  
   VerifiedPassword string `json:"verified_password" binding:"required,len=32"`  
}

controller/UserController.go

// ModifyPassword 修改密码  
func ModifyPassword(c *gin.Context) {  
   // 获取参数类型对象  
   var params request.ModifyPasswordParams  
   // 绑定参数  
   err := c.ShouldBindJSON(&params)  
   // 绑定失败  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   // 获取Token信息  
   err, tokenInfo := utils.GetCacheUser(c)  
   if err != nil {  
      utils.AuthorizationResult(c, "登录失效")  
      return  
   }  
   err, status := service.ModifyPasswordService(&params, &tokenInfo.UserInfo)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   if !status {  
      utils.FailResult(c, "修改失败")  
      return  
   }  
   utils.SuccessResult(c, "修改成功", nil)  
}

router/UserRouter.go

// 修改密码  
router.POST("/modifyPassword", controller.ModifyPassword)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.18 编写退出登录接口

接口地址:/api/user/logout

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go

// LogoutService 退出登录  
func LogoutService(param *request.TokenParams, token string) (error, bool) {  
   // 判断参数是否为空  
   if param == nil {  
      return errors.New("退出失败"), false  
   }  
   // 删除 Redis   
   _, err := RedisClient().Del(ctx, token).Result()  
   if err != nil {  
      return err, false  
   }  
   return nil, true  
}

controller/UserController.go

// Logout 退出登录  
func Logout(c *gin.Context) {  
   // 获取用户信息  
   user, _ := c.Get("user")  
   // 获取Token  
   header := c.GetHeader("Authorization")  
   // 判断用户信息是否存在  
   if user == nil {  
      utils.FailResult(c, "登陆失效,请重新登录!")  
      return  
   }  
   // 将user转化为 TokenParam类型  
   tokenParam, ok := user.(*request.TokenParams)  
   // 判断是否转化正确  
   if !ok {  
      utils.FailResult(c, "登陆失效,请重新登录!")  
      return  
   }  
   err, logout := service.LogoutService(tokenParam, header)  
   if err != nil {  
      utils.FailResult(c, "退出失败")  
      return  
   }  
   if !logout {  
      utils.FailResult(c, "退出失败")  
      return  
   }  
   // 退出成功  
   utils.SuccessResult(c, "退出成功", nil)  
}

router/UserRouter.go

// 退出登录  
router.POST("/logout", controller.Logout)

运行

go run main.go

测试

打开 postman/apiPost进行测试

4.19 编写文件上传接口

接口地址:/api/upload/userAvatar/api/upload/articleIcon

Application.yaml 下添加:

# 上传文件配置  
upload:  
  # 基础返回地址  
  host: 'http://39.101.72.168:81/image/'  
  # 存储位置  
  img_load:  
    user: 'uploads/' # 用户头像存储目录  
    article: 'uploads/article/' # 文章图片存储目录  
  # 文件大小  
  max_size:  
    # 图片  
    img: 10  
  # 可上传的类型  
  img_type:  
    - '.jpg'  
    - '.jpeg'  
    - '.png'  
    - '.bmp'  
    - '.gif'

config/ConfigDefault.go

// Config 配置 yaml 结构  
type Config struct {  
   // 端口号  
   Port string `yaml:"port"`  
   // 数据库配置  
   DataBase struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      UserName string `yaml:"user_name"`  
      Password string `yaml:"password"`  
      Schema   string `yaml:"schema"`  
   } `yaml:"data_base"`  
   // 日志配置  
   LogConfig struct {  
      Path string `yaml:"path"`  
      Name string `yaml:"name"`  
   } `yaml:"log_config"`  
   // 正则配置  
   Regular struct {  
      Phone string `yaml:"phone"`  
      Email string `yaml:"email"`  
   } `yaml:"regular"`  
   // 邮箱配置  
   EmailConfig struct {  
      EmailAddress string `yaml:"email_address"`  
      EmailName    string `yaml:"email_name"`  
      Password     string `yaml:"password"`  
      SmtpServer   string `yaml:"smtp_server"`  
      SmtpPort     int    `yaml:"smtp_port"`  
   } `yaml:"email_config"`  
   // redis 配置  
   RedisConfig struct {  
      Host     string `yaml:"host"`  
      Port     string `yaml:"port"`  
      Password string `yaml:"password"`  
      DB       int    `yaml:"db"`  
   } `yaml:"redis_config"`  
   // 加密配置  
   Encryption struct {  
      // 私钥  
      PrivateKey struct {  
         Password string `yaml:"password"`  
      } `yaml:"private_key"`  
      // 盐值  
      Salt struct {  
         Password int `yaml:"password"`  
      } `yaml:"salt"`  
   } `yaml:"encryption"`  
   // token 配置  
   Token struct {  
      PrivateKey string `yaml:"private_key"`  
   } `yaml:"token"`  
   // 绕过中间件验证的地址  
   NotVerifyUrl []string `yaml:"not_verify_url"`  
   // 跨域中间件验证地址  
   Cors struct {  
      Ip      []string `yaml:"ip"`  
      Methods []string `yaml:"methods"`  
   } `yaml:"cors"`  
   // 上传配置  
   Upload struct {  
      Host    string `yaml:"host"`  
      ImgLoad struct {  
         User    string `yaml:"user"`  
         Article string `yaml:"article"`  
      } `yaml:"img_load"`  
      MaxSize struct {  
         Img int64 `yaml:"img"`  
      } `yaml:"max_size"`  
      ImgType []string `yaml:"img_type"`  
   } `yaml:"upload"`  
}

utils/index.go

// GenerateFileName 生成文件名  
func GenerateFileName(originalName string) string {  
   // 提取文件后缀  
   extension := filepath.Ext(originalName)  
   // 生成 UUID 字符串  
   uuidFilename := uuid.NewV4().String()  
   // 返回拼接字符串  
   return uuidFilename + extension  
}  
  
// IsAllowedImageType 定义允许上传的文件类型  
func IsAllowedImageType(extension string) bool {  
   // 获取允许的类型  
   allowedImageTypes := config.Upload.ImgType  
   // 判断是否符合该类型  
   return contains(allowedImageTypes, extension)  
}  
  
// 判断是否允许上传  
func contains(slice []string, val string) bool {  
   for _, item := range slice {  
      if item == val {  
         return true  
      }  
   }  
   return false  
}

controller下创建UploadController.go

package controller  
  
import (  
   "GinCoBlog/config"  
   "GinCoBlog/utils"   "fmt"   "github.com/gin-gonic/gin"   "path/filepath")  
  
func UserAvatar(c *gin.Context) {  
   file, err := c.FormFile("image")  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
  
   // 生成 UUID 作为文件名的一部分  
   newName := utils.GenerateFileName(file.Filename)  
  
   // 检查文件类型是否被允许上传  
   ext := filepath.Ext(file.Filename)  
   if !utils.IsAllowedImageType(ext) {  
      utils.FailResult(c, "只允许上传图片文件")  
      return  
   }  
  
   // 检查文件大小  
   if file.Size > (config.Upload.MaxSize.Img << 20) { // 限制文件大小为 5MB      utils.FailResult(c, "文件大小超过限制")  
      return  
   }  
  
   // 保存上传的文件到本地  
   dst := fmt.Sprintf(config.Upload.ImgLoad.User+"%s", newName)  
   if err := c.SaveUploadedFile(file, dst); err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
  
   utils.SuccessResult(c, "上传成功", map[string]interface{}{"fileName": config.Upload.Host + newName})  
}  
  
func ArticleIcon(c *gin.Context) {  
   file, err := c.FormFile("image")  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
  
   // 生成 UUID 作为文件名的一部分  
   newName := utils.GenerateFileName(file.Filename)  
  
   // 检查文件类型是否被允许上传  
   ext := filepath.Ext(file.Filename)  
   if !utils.IsAllowedImageType(ext) {  
      utils.FailResult(c, "只允许上传图片文件")  
      return  
   }  
  
   // 检查文件大小  
   if file.Size > (config.Upload.MaxSize.Img << 20) { // 限制文件大小为 5MB      utils.FailResult(c, "文件大小超过限制")  
      return  
   }  
  
   // 保存上传的文件到本地  
   dst := fmt.Sprintf(config.Upload.ImgLoad.Article+"%s", newName)  
   if err := c.SaveUploadedFile(file, dst); err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
  
   utils.SuccessResult(c, "上传成功", map[string]interface{}{"fileName": config.Upload.Host + newName})  
}

router下创建 UploadRouter.go

package routes  
  
import (  
   "GinCoBlog/controller"  
   "github.com/gin-gonic/gin")  
  
func UploadRouter(router *gin.RouterGroup) {  
   // 用户头像  
   router.PUT("/userAvatar", controller.UserAvatar)  
   // 文章图片  
   router.PUT("/articleIcon", controller.ArticleIcon)  
}

router/index.go中添加

package routes  
  
import "github.com/gin-gonic/gin"  
  
// SetupRouterGroup 项目主路由(一级)  
func SetupRouterGroup(router *gin.RouterGroup) {  
   // 调取用户路由  
   UserRouter(router.Group("/user"))  
   // 调取上传路由  
   UploadRouter(router.Group("/upload"))  
}

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

在这里插入图片描述

4.20 编写获取文章类型接口

接口地址:/api/article/articleType

Application.yaml 下添加:

# 不验证的接口地址  
not_verify_url:
  - '/api/article/articleType'

service 目录下创建 ArticleService.go

// ArticleTypeService 文章类型  
func ArticleTypeService(list *[]entity.ArticleType) error {  
   // 获取数据库内的文章类型数据  
   if err := pool.Model(&entity.ArticleType{}).Where("type_visible", 1).Find(&list).Error; err != nil {  
      return errors.New("获取失败")  
   }  
   return nil  
}

controller下创建ArticleController.go

// ArticleType 文章类型  
func ArticleType(c *gin.Context) {  
   // 定义类型  
   var typeList []entity.ArticleType  
   // 获取数据  
   if err := service.ArticleTypeService(&typeList); err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   utils.SuccessResult(c, "获取成功", map[string][]entity.ArticleType{"articleType": typeList})  
}

router下创建ArticleRouter.go

package routes  
  
import (  
   "GinCoBlog/controller"  
   "github.com/gin-gonic/gin")  
  
// ArticleRouter  定义文章路由(二级)  
func ArticleRouter(router *gin.RouterGroup) {  
   // 获取文章类型列表  
   router.GET("/articleType", controller.ArticleType)  
}

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.21 编写获取文章列表接口

接口地址:/api/article/articleList

Application.yaml 下添加:

# 不验证的接口地址  
not_verify_url:  
  - '/api/user/phoneOccupy'  
  - '/api/user/emailOccupy'  
  - '/api/user/register'  
  - '/api/user/phoneLogin'  
  - '/api/user/emailLogin'  
  - '/api/user/sendEmailCode'  
  - '/api/user/forgotPassword'  
  - '/api/article/articleType'  
  - '/api/article/articleList'

service/ArticleService.go

// ArticleListByTypeId 根据文章类型查询文章列表  
func ArticleListByTypeId(list *[]entity.Article, typeId int32) error {  
   // 判断参数格式  
   if typeId == 0 {  
      return errors.New("参数为空")  
   }  
   // 获取数据库内的文章类型数据  
   if err := pool.Model(&entity.Article{}).Where("type_id", typeId).Find(&list).Error; err != nil {  
      return errors.New("获取失败")  
   }  
   return nil  
}

requset/ArticleRequest.go

// ArticleListByTypeParam 根据类型 ID 查询的参数  
type ArticleListByTypeParam struct {  
   TypeId int32 `form:"type_id" binding:"required"`  
}

controller/ArticleController.go

// ArticleListByType 文章列表  
func ArticleListByType(c *gin.Context) {  
   var params request.ArticleListByTypeParam  
   var articleList []entity.Article  
   // 绑定参数  
   err := c.ShouldBindQuery(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   // 查询数据库  
   if err := service.ArticleListByTypeId(&articleList, params.TypeId); err != nil {  
      utils.FailResult(c, "获取失败")  
      return  
   }  
   utils.SuccessResult(c, "获取成功", map[string][]entity.Article{"rows": articleList})  
}

router/ArticleRouter.go

// 通过类型 ID 获取文章列表  
router.GET("/articleList", controller.ArticleListByType)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.22 编写获取文章详情接口

接口地址:/api/article/articleInfo

Application.yaml 下添加:

# 不验证的接口地址  
not_verify_url:  
  - '/api/user/phoneOccupy'  
  - '/api/user/emailOccupy'  
  - '/api/user/register'  
  - '/api/user/phoneLogin'  
  - '/api/user/emailLogin'  
  - '/api/user/sendEmailCode'  
  - '/api/user/forgotPassword'  
  - '/api/article/articleType'  
  - '/api/article/articleList'  
  - '/api/article/articleInfo'

service/ArticleService.go

// ArticleInfoByIdService 根据 ID 获取文章详情  
func ArticleInfoByIdService(info *entity.Article, articleId int32) error {  
   if articleId == 0 {  
      return errors.New("参数为空")  
   }  
   // 获取数据库信息  
   if err := pool.Model(&entity.Article{}).Where("id=? AND article_visible=?", articleId, 1).First(&info).Error; err != nil {  
      return errors.New("获取失败")  
   }  
   return nil  
}

requset/ArticleRequest.go

// ArticleInfoByIdParam 根据 ID 获取文章详情  
type ArticleInfoByIdParam struct {  
   ArticleId int32 `form:"article_id" binding:"required"`  
}

controller/ArticleController.go

// ArticleInfoById 文章详情  
func ArticleInfoById(c *gin.Context) {  
   var params request.ArticleInfoByIdParam  
   var info entity.Article  
   // 绑定参数  
   err := c.ShouldBindQuery(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   // 查询数据库  
   if err := service.ArticleInfoByIdService(&info, params.ArticleId); err != nil {  
      utils.FailResult(c, "获取失败")  
      return  
   }  
   utils.SuccessResult(c, "获取成功", map[string]entity.Article{"data": info})  
}

router/ArticleRouter.go

// 通过 ID 获取文章详情  
router.GET("/articleInfo", controller.ArticleInfoById)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.23 编写添加文章接口

接口地址:/api/article/addArticle

service/ArticleService.go

// AddArticleService 添加文章  
func AddArticleService(param *request.AddArticleParam, userInfo entity.SysUser) (error, bool) {  
   // 判断参数是否为空  
   if param.TypeId == 0 || utils.StrIsEmpty(param.Title) || utils.StrIsEmpty(param.Context) || utils.StrIsEmpty(param.Icon) {  
      return errors.New("参数为空"), false  
   }  
   // 判断参数是否合法  
   // 将数据存入数据库  
   err := pool.Model(&entity.Article{}).Create(&entity.Article{  
      TypeId:  param.TypeId,  
      UserId:  userInfo.UID,  
      Title:   param.Title,  
      Context: param.Context,  
      Icon:    param.Icon,  
   }).Error  
   if err != nil {  
      return errors.New("添加失败"), false  
   }  
   return nil, true  
}

requset/ArticleRequest.go

// AddArticleParam 添加文章参数  
type AddArticleParam struct {  
   TypeId  uint32 `json:"typeId" binding:"required"`  
   Title   string `json:"title" binding:"required"`  
   Context string `json:"context" binding:"required"`  
   Icon    string `json:"icon" binding:"required"`  
}

controller/ArticleController.go

// AddArticle 添加文章  
func AddArticle(c *gin.Context) {  
   // 获取参数实例  
   var params request.AddArticleParam  
   // 绑定参数  
   err := c.ShouldBindJSON(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   err, tokenInfo := utils.GetCacheUser(c)  
   if err != nil {  
      utils.AuthorizationResult(c, "登录状态失效")  
      return  
   }  
   // 将数据添加到数据库  
   err, status := service.AddArticleService(&params, tokenInfo.UserInfo)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   if !status {  
      utils.FailResult(c, "添加失败")  
   }  
   utils.SuccessResult(c, "添加成功", nil)  
}

router/ArticleRouter.go

// 添加文章  
router.POST("/addArticle", controller.AddArticle)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.24 编写更新阅读量接口

接口地址:/api/article/updateRead

Application.yaml 下添加:

not_verify_url:  
  - '/api/user/phoneOccupy'  
  - '/api/user/emailOccupy'  
  - '/api/user/register'  
  - '/api/user/phoneLogin'  
  - '/api/user/emailLogin'  
  - '/api/user/sendEmailCode'  
  - '/api/user/forgotPassword'  
  - '/api/article/articleType'  
  - '/api/article/articleList'  
  - '/api/article/articleInfo'  
  - '/api/article/updateRead'

config/ConfigDefault.go


utils/index.go


service/ArticleService.go

// UpdateReadService 更新阅读量  
func UpdateReadService(param *request.UpdateReadParam) (error, bool) {  
   // 判断参数为空  
   if param.ArticleId == 0 {  
      return errors.New("参数为空"), false  
   }  
   // 修改数据库  
   if err := pool.Model(&entity.Article{}).Where("id", param.ArticleId).UpdateColumn("read", gorm.Expr("`read` + ?", 1)).Error; err != nil {  
      return errors.New("修改失败"), false  
   }  
   // 修改成功  
   return nil, true  
}

requset/ArticleRequest.go

// UpdateReadParam 更新阅读量  
type UpdateReadParam struct {  
   ArticleId int32 `json:"article_id" binding:"required"`  
}

controller/ArticleController.go

// UpdateRead 更新阅读量  
func UpdateRead(c *gin.Context) {  
   var params request.UpdateReadParam  
   // 绑定参数  
   err := c.ShouldBindJSON(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   // 修改数据库  
   err, status := service.UpdateReadService(&params)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 判断状态  
   if !status {  
      utils.FailResult(c, "设置失败")  
      return  
   }  
   // 设置成功  
   utils.SuccessResult(c, "更新成功", nil)  
}

router/ArticleRouter.go

// 更新阅读量  
router.POST("/updateRead", controller.UpdateRead)

运行

go run main.go

测试

打开 postman/apiPost进行测试

4.25 编写查询评论列表接口

接口地址:/api/article/articleComment

Application.yaml 下添加:

not_verify_url:  
  - '/api/user/phoneOccupy'  
  - '/api/user/emailOccupy'  
  - '/api/user/register'  
  - '/api/user/phoneLogin'  
  - '/api/user/emailLogin'  
  - '/api/user/sendEmailCode'  
  - '/api/user/forgotPassword'  
  - '/api/article/articleType'  
  - '/api/article/articleList'  
  - '/api/article/articleInfo'  
  - '/api/article/updateRead'  
  - '/api/article/articleComment'

config/ConfigDefault.go


utils/index.go


service/ArticleService.go

// ArticleCommentService 获取文章评论  
func ArticleCommentService(param *request.ArticleCommentParam, comments *[]entity.ArticleComments) error {  
   if param.ArticleId == 0 {  
      return errors.New("参数为空")  
   }  
   // 获取数据库信息  
if err := pool.Table("article_comments").  
   Where("article_id", param.ArticleId).  
   Joins("INNER JOIN sys_user ON article_comments.user_id = sys_user.uid").  
   Select("article_comments.id, article_comments.article_id, article_comments.context, sys_user.user_name, sys_user.head_sculpture, sys_user.integral, sys_user.member").  
   Find(&comments).Error; err != nil {  
   return errors.New("获取失败")  
}
   // 获取成功  
   return nil  
}

requset下创建 ArticleResponse.go

// ArticleCommentResponse 文章评论返回参数  
type ArticleCommentResponse struct {  
   ID            uint32 `json:"id"`             // 评论ID  
   ArticleID     uint32 `json:"article_id"`     // 文章ID  
   Context       string `json:"context"`        // 评论内容  
   UserName      string `json:"user_name"`      // 用户名  
   HeadSculpture string `json:"head_sculpture"` // 头像 默认值  
   Integral      uint32 `json:"integral"`       // 积分  
   Member        uint8  `json:"member"`         // 会员  
}

requset/ArticleRequest.go

// ArticleCommentParam 文章评论参数  
type ArticleCommentParam struct {  
   ArticleId int32 `form:"article_id" binding:"required"`  
}

controller/ArticleController.go

// ArticleComment 获取文章评论  
func ArticleComment(c *gin.Context) {  
   var params request.ArticleCommentParam  
   var commentList []entity.ArticleComments  
  
   // 绑定参数  
   err := c.ShouldBindQuery(&params)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   // 查询数据库  
   if err := service.ArticleCommentService(&params, &commentList); err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 获取成功  
   utils.SuccessResult(c, "获取成功", map[string][]entity.ArticleComments{"rows": commentList})  
}

router/ArticleRouter.go

// 获取文章评论  
router.GET("/articleComment", controller.ArticleComment)

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.26 编写上传评论接口

接口地址:/api/article/addComment

service/ArticleService.go

// AddArticleCommentService 添加文章评论  
func AddArticleCommentService(param *request.AddArticleCommentParam, userInfo entity.SysUser) (error, bool) {  
   // 判断参数是否为空  
   if param.ArticleId == 0 || utils.StrIsEmpty(param.Context) {  
      return errors.New("参数为空"), false  
   }  
   // 判断参数格式  
   // 将数据存入数据库  
   if err := pool.Model(&entity.ArticleComments{}).Create(&entity.ArticleComments{  
      ArticleID: param.ArticleId,  
      UserID:    userInfo.UID,  
      Context:   param.Context,  
   }).Error; err != nil {  
      return errors.New("上传失败"), false  
   }  
   // 添加成功  
   return nil, true  
}

requset/ArticleRequest.go

// AddArticleCommentParam 添加文章评论  
type AddArticleCommentParam struct {  
   ArticleId uint32 `json:"article_id" binding:"required"`  
   Context   string `json:"context" binding:"required"`  
}

controller/ArticleController.go

// AddArticleComment 添加文章评论  
func AddArticleComment(c *gin.Context) {  
   // 获取参数类型  
   var param request.AddArticleCommentParam  
   // 绑定参数  
   err := c.ShouldBindJSON(&param)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   err, tokenInfo := utils.GetCacheUser(c)  
   if err != nil {  
      utils.AuthorizationResult(c, "登录状态失效")  
      return  
   }  
   // 将数据添加到数据库  
   err, status := service.AddArticleCommentService(&param, tokenInfo.UserInfo)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 判断是否添加成功  
   if !status {  
      utils.FailResult(c, "上传失败")  
      return  
   }  
   utils.SuccessResult(c, "上传成功", nil)  
}

router/ArticleRouter.go

// 添加文章评论  
router.POST("/addComment", controller.AddArticleComment)

运行

go run main.go

测试

打开 postman/apiPost进行测试
![[gin_co_blog_assets/Pasted image 20231018142319.png]]

4.27 编写用户反馈接口

接口地址:/api/user/userNameOccupy

service 目录下创建 FeedBackService.go

// AddFeedBackService 添加用户反馈  
func AddFeedBackService(param request.AddFeedBackParam, userInfo entity.SysUser) (error, bool) {  
   // 验证数据  
   if utils.StrIsEmpty(param.Context) {  
      return errors.New("参数为空"), false  
   }  
   // 验证数据  
   // 向数据库内添加数据  
   if err := pool.Model(&entity.FeedBack{}).Create(&entity.FeedBack{  
      UserID:  userInfo.UID,  
      Context: param.Context,  
   }).Error; err != nil {  
      return errors.New("上传失败"), false  
   }  
   return nil, true  
}

requset 下创建FeedBackRequest.go

// AddFeedBackParam 添加反馈信息参数  
type AddFeedBackParam struct {  
   Context string `json:"context" binding:"required"`  
}

controller下创建FeedBackController.go

// AddFeedBack 添加用户反馈  
func AddFeedBack(c *gin.Context) {  
   var param request.AddFeedBackParam  
   // 绑定数据  
   err := c.ShouldBindJSON(&param)  
   if err != nil {  
      utils.FailResult(c, "参数错误")  
      return  
   }  
   err, tokenInfo := utils.GetCacheUser(c)  
   if err != nil {  
      utils.AuthorizationResult(c, "登录状态失效")  
      return  
   }  
   // 向数据库内添加  
   err, status := service.AddFeedBackService(param, tokenInfo.UserInfo)  
   if err != nil {  
      utils.FailResult(c, err.Error())  
      return  
   }  
   // 判断是否成功  
   if !status {  
      utils.FailResult(c, "反馈失败")  
      return  
   }  
   // 反馈成功  
   utils.SuccessResult(c, "反馈成功", nil)  
}

router下创建FeedBackRouter.go

// FeedBackRouter 定义反馈路由(二级)  
func FeedBackRouter(router *gin.RouterGroup) {  
   // 添加反馈  
   router.POST("/addFeedback", controller.AddFeedBack)  
}

router/index.go

// SetupRouterGroup 项目主路由(一级)  
func SetupRouterGroup(router *gin.RouterGroup) {  
   // 调取用户路由  
   UserRouter(router.Group("/user"))  
   // 调取上传路由  
   UploadRouter(router.Group("/upload"))  
   // 调取文章路由  
   ArticleRouter(router.Group("/article"))  
   // 调取反馈路由  
   FeedBackRouter(router.Group("/feedback"))  
}

运行

go run main.go

测试

打开 postman/apiPost进行测试
在这里插入图片描述

4.28 编写查询VIP列表接口

接口地址:/api/user/userNameOccupy

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go


requset/UserRequest.go


controller/UserController.go

router/UserRouter.go

运行

go run main.go

测试

打开 postman/apiPost进行测试

4.29 编写升级VIP接口

接口地址:/api/user/userNameOccupy

Application.yaml 下添加:


config/ConfigDefault.go


utils/index.go


service 目录下创建 UserService.go


requset/UserRequest.go


controller/UserController.go

router/UserRouter.go

运行

go run main.go

测试

打开 postman/apiPost进行测试

5 打包

5.1 生成接口文档

在 apifox 上点击项目三个点->导出
![[gin_co_blog_assets/Pasted image 20231018155343.png]]
选择导出 MarkDown 导出就行

5.2 exe 程序

将项目右键,找到 Open In --> Terminal
![[gin_co_blog_assets/Pasted image 20231018160106.png]]

## 生成与项目同名的 exe 程序
go build .

5.3 linux 执行程序

也是打开 Terminal

输入以下命令:

## 变回windows
go env -w CGO_ENABLED=1 GOOS=windows GOARCH=amd64
## linux
go env -w CGO_ENABLED=0 GOOS=linux GOARCH=amd64
go build .

5.4 Docker 镜像

5.4.1 编写 Dockerfile

在项目根目录下创建 Dockerfile

# 使用 golang 的官方镜像作为基础镜像  
FROM golang:latest  
  
# 设置工作目录  
WORKDIR /app  
  
# 将代码复制到容器中的工作目录  
COPY . .  
  
# 构建 Go 语言程序  
RUN go build -o CoBlogInterface .  
  
# 暴露需要监听的端口  
EXPOSE 4001  
  
# 运行 Go 语言程序  
CMD ["./CoBlogInterface"]

5.4.2 生成 Docker Image

在 terminal打开,并输入:

docker build -t co_blog_interface:1.0 .

![[gin_co_blog_assets/Pasted image 20231018162907.png]]

6 发布

6.1 linux 发布

6.2 Docker 发布

安装 Docker
拉取

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值