Go语言Web项目搭建

Web论坛搭建(后端部分)

需求罗列

根据前端提供的基础,实现业务逻辑的开发,主要包括:

  • 用户的登陆注册功能
  • 登陆后,用户具有撰写帖、修改帖子、阅读帖子以及删除帖子的需求
  • 提供一个相册,用户可以上传图片、展示图片
  • 实现一个分页机制,使得可以限定每页展示的帖子数量(即上一页,下一页)
  • 提供一个社区阅读排行榜,统计文章点击的数量,并且进行显示(类似于微博热搜)

注意:

  • 项目需要提供一个配置组件,提供一个配置文件,减少后期代码的改动
  • 项目需要提供一个日志组件,便于记录/调试
  • 项目需要提供一个Session,便于后期扩展
  • 密码用MD5加密

需求分析

  • 用户的登陆注册功能

      1、划分好不同的URL访问,如:login、Register,分别实现它们的Get和Post请求
      2、注册的时候,先在数据库中查询,是否已经存在,存在则注册失败,转至重新注册的页面
         注册成功则根据用户提交的UserName 和 PassWord插入到数据库中,转到登陆页面
      3、登陆的时候,去数据库中查询,验证用户名是否存在,密码是否正确,用MD5加密,成功成功,转到主页
    
  • 登陆后,用户具有撰写帖、修改帖子、阅读帖子以及删除帖子的需求

      1、每次访问AddArticle/DeleteArticle等等的时候,都应该验证用户的登陆状态,这些部分可以抽象出一个中间件
       基础的认证校验:只要cookie中带了login_user标识就认为是登录用户
       2、帖子的增删改查,全部用MYSQL实现
    
  • 提供一个相册,用户可以上传图片、展示图片

      1、同样用MYSQL实现,需求完成同上
    
  • 实现一个分页机制,使得可以限定每页展示的帖子数量(即上一页,下一页)

      1、首先查询全部文章,然后根据前端传入的N,对查询到的文章做处理,注意判断,上一下和下一页是否存在
    
  • 提供一个社区阅读排行榜,统计文章点击的数量,并且进行显示(类似于微博热搜)

      1、用Redis数据库的zincrby 实现, 即每一个文章,都有一个分数,点一下,分数+1,
      // zincrby code_language 1 golang
      2、 根据分数进行排序,获得前10的文章ID,在MySql数据库中,按照这些ID查询,返回这些文章的标题
    

架构

  • 项目架构基于Gin框架
  • MySQl使用Sqlx
_ "github.com/go-sql-driver/mysql"  
sql "github.com/jmoiron/sqlx"
  • Redis使用go-redis
"github.com/go-redis/redis"
  • 日志组件使用Zap
"go.uber.org/zap"
  • 配置组件使用无闻大佬的go-ini
"github.com/go-ini/ini"

业务分层

  • conf

      存放配置文件,支持多种格式,jason、ini
    
  • config

      配置模块,存放各个组件的初始化函数
    
  • conreollers

      处理器模块,按照不同的对象,进行划分,Example:跟文章相关的,位于article.go
    
  • dao

      数据库模块,如创建数据库的表,数据库增删改查
    
  • logger

      日志模块,创建GinLogger、GinRecovery接管Gin框架日志和恢复
    
  • logic

      逻辑模块,主要是相关逻辑算法,如排行榜等
    
  • middlewares

      中间件模块,提供认证校验功能
    
  • models

      模型模块,每个模型自身的结构定义,以及函数,例如帖子,则有帖子的增删改查函数,
    
  • routers

      路由模块,根据不同的URL访问不同的处理器模块
    
  • static

      静态数据模块,存放静态文件
    
  • views

      视图模块,存放模板html
    
  • utils

      工具模块,存放工具函数
    

源码实现

config

  • config.go
package config

import (
	"encoding/json"
	"github.com/go-ini/ini"
	"io/ioutil"
)

//结构体标签的多个键值对之间,必须用空格分割。,不能用逗号!!!,不能用逗号!!!,不能用逗号!!!

// 应用的配置结构体
type AppConfig struct {
	*ServerConfig `json:"server" ini:"server"`
	*MySQLConfig  `json:"mysql" ini:"mysql"`
	*RedisConfig  `json:"redis" ini:"redis"`
	*LogConfig    `json:"log" ini:"log"`
}

// web server配置
type ServerConfig struct {
	Port int `json:"port" ini:"port"`
}

// MySQL数据库配置
type MySQLConfig struct {
	Host     string `json:"host" ini:"host"`
	Username string `json:"username" ini:"username"`
	Password string `json:"password" ini:"password"`
	Port     int    `json:"port" ini:"port"`
	DB       string `json:"db" ini:"db"`
}

// redis配置
type RedisConfig struct {
	Host     string `json:"host" ini:"host"`
	Password string `json:"password" ini:"password"`
	Port     int    `json:"port" ini:"port"`
	DB       int    `json:"db" ini:"db"`
}

// Log配置
type LogConfig struct {
	Level      string `json:"level" ini:"level"`
	Filename   string `json:"filename" ini:"filename"`
	MaxSize    int    `json:"maxsize" ini:"maxsize"`
	MaxAge     int    `json:"max_age" ini:"max_age"`
	MaxBackups int    `json:"max_backups" ini:"max_backups"`
}

var Conf = new(AppConfig) // 定义了全局的配置文件实例

// Init 初始化
func Init(file string) error {
	jsonData, err := ioutil.ReadFile(file)
	if err != nil {
		return err
	}
	if err := json.Unmarshal(jsonData, Conf); err != nil {
		return err
	}
	return nil
}

func InitFromStr(str string) error {
	if err := json.Unmarshal([]byte(str), Conf); err != nil {
		return err
	}
	return nil
}

func InitFromIni(filename string) error {
	err := ini.MapTo(Conf, filename)
	if err != nil {
		panic(err)
	}
	return err
}

controllers

  • album.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"fmt"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"net/http"
	"os"
	"path"
	"time"
)

// 文件上传
func UploadPost(c *gin.Context) {
	fh, err := c.FormFile("upload")
	if err != nil {
		logger.Warn("UploadPost", zap.Any("error", err))
		c.JSON(http.StatusOK, gin.H{"msg": "无效的参数"})
		return
	}
	logger.Debug("UploadPost", zap.String("filename", fh.Filename), zap.Int64("fileSize", fh.Size))

	now := time.Now()
	fileType := "other"
	// 判断后缀为图片的文件,如果是图片我们才存入到数据库中
	fileExt := path.Ext(fh.Filename)
	if fileExt == ".jpg" || fileExt == ".png" || fileExt == ".gif" || fileExt == ".jpeg" {
		fileType = "img"
	}

	// 准备好要创建的文件夹路径
	fileDir := fmt.Sprintf("static/upload/%s/%d/%d/%d", fileType, now.Year(), now.Month(), now.Day())

	// ModePerm是0777,这样拥有该文件夹路径的执行权限
	// 创建文件夹
	err = os.MkdirAll(fileDir, os.ModePerm)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"msg": "服务繁忙,稍后再试。"})
		return
	}

	// 文件路径
	timeStamp := time.Now().Unix()
	fileName := fmt.Sprintf("%d-%s", timeStamp, fh.Filename)

	// 文件路径+文件名  拼接好
	filePathStr := path.Join(fileDir, fileName)

	// 将浏览器客户端上传的文件拷贝到本地路径的文件里面,此处也可以使用io操作
	c.SaveUploadedFile(fh, filePathStr)

	if fileType == "img" {
		album := &models.Album{Filepath: filePathStr, Filename: fileName, CreateTime: timeStamp}
		models.AddAlbum(album)
	}

	c.JSON(http.StatusOK, gin.H{"code": 1, "message": "上传成功"})
}

// 获取所有的图片
func AlbumGet(c *gin.Context) {
	isLogin := c.GetBool("is_login")
	albums, err := models.QueryAlbum()
	if err != nil {
		logger.Error("AlbumGet", zap.Any("error", err))
		c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "服务繁忙,请稍后再试。"})
		return
	}

	c.HTML(http.StatusOK, "album.html", gin.H{"isLogin": isLogin, "albums": albums})
}

  • article.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/logic"
	"blogweb_gin/models"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"net/http"
	"strconv"
	"time"
)

/*
当访问/add路径的时候回触发AddArticleGet方法
响应的页面是通过HTML
*/

// 获取写文章
func AddArticleGet(c *gin.Context) {
	//获取session
	isLogin := c.MustGet("is_login").(bool)
	c.HTML(http.StatusOK, "write_article.html", gin.H{"isLogin": isLogin})
}

// 提交写好的文章
func AddArticlePost(c *gin.Context) {
	//获取浏览器传输的数据,通过表单的name属性获取值
	//获取表单信息
	title := c.PostForm("title")
	tags := c.PostForm("tags")
	short := c.PostForm("short")
	content := c.PostForm("content")
	currentUser := c.MustGet("login_user").(string)

	logger.Debug("AddArticlePost", zap.String("title", title), zap.String("tags", tags))

	//实例化model,将它出入到数据库中
	art := &models.Article{
		Title:      title,
		Tags:       tags,
		Short:      short,
		Content:    content,
		Author:     currentUser,
		CreateTime: time.Now().Unix(),
	}
	_, err := models.AddArticle(art)

	//返回数据给浏览器
	response := gin.H{}

	if err == nil {
		//无误
		response = gin.H{"code": 1, "message": "ok"}
	} else {
		logger.Error("AddArticlePost failed", zap.Any("error", err))
		response = gin.H{"code": 0, "message": "error"}
	}
	c.JSON(http.StatusOK, response)
}

// 展示文章
func ShowArticleGet(c *gin.Context) {
	isLogin := c.MustGet("is_login")
	idStr := c.Param("id")

	// 查询文章
	article, err := models.QueryArticleWithId(idStr)
	if err != nil {
		logger.Error("QueryArticleWithId failed", zap.Any("error", err))
		c.String(http.StatusOK, "bad id")
		return
	}
	if article == nil {
		c.String(http.StatusOK, "bad id")
		return
	}

	// 增加文章的阅读数
	err = logic.IncArticleReadCount(idStr)
	if err != nil {
		logger.Error("ArticleReadCountIncr failed", zap.Any("error", err))
	}

	c.HTML(http.StatusOK, "show_article.html", gin.H{"isLogin": isLogin, "Title": article.Title, "Content": article.Content})
}

// UpdateArticleGet 更新文章
func UpdateArticleGet(c *gin.Context) {
	isLogin := c.MustGet("is_login")
	idStr := c.Query("id")

	//获取id所对应的文章信息
	article, err := models.QueryArticleWithId(idStr)
	if err != nil {
		logger.Error("QueryArticleWithId failed", zap.Any("error", err))
		c.String(http.StatusOK, "bad id")
		return
	}
	if article == nil {
		c.String(http.StatusOK, "bad id")
		return
	}

	c.HTML(http.StatusOK, "write_article.html", gin.H{"isLogin": isLogin, "article": article})
}

// 更新文章
func UpdateArticlePost(c *gin.Context) {
	// 获取浏览器传输的数据,通过表单的name属性获取值
	idStr := c.PostForm("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(http.StatusOK, "bad id")
	}
	title := c.PostForm("title")
	tags := c.PostForm("tags")
	short := c.PostForm("short")
	content := c.PostForm("content")

	// 实例化model,修改数据库
	art := &models.Article{
		Id:      id,
		Title:   title,
		Tags:    tags,
		Short:   short,
		Content: content,
	}

	logger.Debug("UpdateArticlePost", zap.Any("article", *art))

	_, err = models.UpdateArticle(art)

	//返回数据给浏览器
	response := gin.H{}

	if err == nil {
		//无误
		response = gin.H{"code": 1, "message": "更新成功"}
	} else {
		response = gin.H{"code": 0, "message": "更新失败"}
	}

	c.JSON(http.StatusOK, response)
}

// 删除文章
func DeleteArticle(c *gin.Context) {
	idStr := c.Query("id")
	_, err := models.DeleteArticle(idStr)
	if err != nil {
		logger.Error("DeleteArticle failed", zap.Any("error", err))
	}
	c.Redirect(http.StatusFound, "/home")
}

// 按照阅读数排行返回前n篇文章的id和title
func ArticleTopN(c *gin.Context) {
	nStr := c.Param("n")
	n, err := strconv.ParseInt(nStr, 0, 16)
	if err != nil {
		logger.Error("ArticleTopN", zap.Any("error", err))
		n = 5
	}

	// 调用业务逻辑层 获取返回数据结果
	articleList := logic.GetArticleReadCountTopN(n)

	// 3. 返回
	c.JSON(http.StatusOK, gin.H{
		"code": 2000,
		"msg":  "success",
		"data": articleList,
	})
	return
}

  • code_msg.go
package controllers

const (
	CodeSuccess = 2000
	CodeBadRequest = 2001
	CodeInvalidParam = 2002
	CodeFailed = 5000
	CodeError = 5001
)

var MsgMap = map[int]string{
	CodeSuccess: "success",
	CodeBadRequest: "bad request",
	CodeInvalidParam: "无效的参数",
	CodeFailed: "请求失败",
	CodeError:"啊哦,服务器走丢了",
}

func ShowMsg(code int)string{
	v, ok:= MsgMap[code]
	if !ok {
		return MsgMap[CodeError]
	}
	return v
}
  • home.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"net/http"
	"strconv"
)

// 获取首页
func HomeGet(c *gin.Context) {
	//获取session,判断用户是否登录
	isLogin := c.MustGet("is_login").(bool)
	username := c.MustGet("login_user").(string)
	page, _ := strconv.Atoi(c.Query("page"))
	if page <= 0 {
		page = 1
	}

	logger.Debug("HomeGet", zap.Int("page", page))

	articleList, err := models.QueryCurrUserArticleWithPage(username, page)
	if err != nil {
		logger.Error("models.QueryCurrUserArticleWithPage failed", zap.Any("error", err))
	}

	logger.Debug("models.QueryCurrUserArticleWithPage", zap.Any("articleList", articleList))

	data := models.GenHomeBlocks(articleList, isLogin)
	pageData := models.GenHomePagination(page)

	logger.Debug("models.GenHomeBlocks", zap.Any("data", data))

	c.HTML(http.StatusOK, "home.html", gin.H{"isLogin": isLogin, "data": data, "pageData": pageData})
}
  • index.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"net/http"
)

// 索引页
func IndexGet(c *gin.Context) {
	articleList, err := models.QueryAllArticle()
	if err != nil {
		logger.Error("models.QueryCurrUserArticleWithPage failed", zap.Any("error", err))
	}
	logger.Debug("models.QueryCurrUserArticleWithPage", zap.Any("articleList", articleList))
	c.HTML(http.StatusOK, "index.html", gin.H{"articleList": articleList})
}

  • login.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"blogweb_gin/utils"
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"net/http"
)

// get登陆页
func LoginGet(c *gin.Context) {
	//返回html
	c.HTML(http.StatusOK, "login.html", gin.H{"title": "登录页"})
}

// 提交登陆
func LoginPost(c *gin.Context) {
	// 取出请求数据
	// 校验用户名密码是否正确
	// 返回响应
	username := c.PostForm("username")
	password := c.PostForm("password")
	logger.Debug("login", zap.String("username", username), zap.String("password", password))

	// 去数据库查,注意查找的时候,密码是MD5之后的密码查找
	id := models.QueryUserWithParam(username, utils.MD5(password))

	fmt.Println("id:", id)

	// 登陆成功
	if id > 0 {
		// 给响应种上Cookie
		session := sessions.Default(c)
		session.Set("login_user", username) // 在session中保存k-v,然后写入cookie
		session.Save()

		c.Redirect(http.StatusFound, "/home") // 浏览器收到这个就会跳转到我指定的页面
		c.JSON(http.StatusOK, gin.H{"code": 200, "message": "登录成功"})
	} else {
		c.JSON(http.StatusOK, gin.H{"code": 0, "message": "登录失败"})
	}
}

// 登出
func LogoutHandler(c *gin.Context) {
	//清除该用户登录状态的数据
	session := sessions.Default(c)
	session.Delete("login_user")
	session.Save()

	c.Redirect(http.StatusFound, "/login")
}

  • register.go
package controllers

import (
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"blogweb_gin/utils"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

// 获取注册
func RegisterGet(c *gin.Context) {
	// 返回html
	c.HTML(http.StatusOK, "register.html", gin.H{"title": "注册页"})
}

// 注册提交
func RegisterPost(c *gin.Context) {
	// 取出请求的数据
	// 判断注册是否重复  --> 拿着用户名去数据库查一下有没有
	// 写入数据库
	// 获取表单信息
	username := c.PostForm("username")
	password := c.PostForm("password")
	repassword := c.PostForm("repassword")
	logger.Debug(fmt.Sprintf("%s %s %s", username, password, repassword))

	// 注册之前先判断该用户名是否已经被注册,如果已经注册,返回错误
	id := models.QueryUserWithUsername(username)
	fmt.Println("id:", id)
	if id > 0 {
		c.JSON(http.StatusOK, gin.H{"code": 0, "message": "用户名已经存在"})
		return
	}

	// 注册用户名和密码
	// 存储的密码是md5后的数据,那么在登录的验证的时候,也是需要将用户的密码md5之后和数据库里面的密码进行判断
	password = utils.MD5(password)
	logger.Debug(fmt.Sprintf("password after md5:%s", password))

	user := models.User{
		Username:   username,
		Password:   password,
		Status:     0,
		CreateTime: time.Now().Unix(),
	}
	_, err := models.InsertUser(&user)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"code": 0, "message": "注册失败"})
	} else {
		c.JSON(http.StatusOK, gin.H{"code": 1, "message": "注册成功"})
	}
}

dao

  • mysql.go
package dao

import (
	"blogweb_gin/config"
	"blogweb_gin/logger"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	sql "github.com/jmoiron/sqlx"
)

var db *sql.DB

func InitMySQL(cfg *config.MySQLConfig) (err error) {
	logger.Info("InitMySQL....")
	if db == nil {
		dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DB)
		db, err = sql.Connect("mysql", dsn)
		if err != nil {
			return
		}
	}

	err = CreateTableWithUser() // 创建用户表
	if err != nil {
		return
	}

	err = CreateTableWithArticle() // 创建文章表
	if err != nil {
		return
	}

	err = CreateTableWithAlbum() // 创建图片表
	if err != nil {
		return
	}

	return
}

//创建用户表
func CreateTableWithUser() (err error) {
	sqlStr := `CREATE TABLE IF NOT EXISTS users(
        id INT(4) PRIMARY KEY AUTO_INCREMENT NOT NULL,
        username VARCHAR(64),
        password VARCHAR(64),
        status INT(4),
        create_time INT(10)
        );`

	_, err = ModifyDB(sqlStr)
	return
}

// 创建文章表
func CreateTableWithArticle() (err error) {
	sqlStr := `create table if not exists article(
        id int(4) primary key auto_increment not null,
        title varchar(30),
        author varchar(20),
        tags varchar(30),
        short varchar(255),
        content longtext,
        create_time int(10),
        status int(4)
        );`
	_, err = ModifyDB(sqlStr)
	return
}

// 创建图片表
func CreateTableWithAlbum() (err error) {
	sqlStr := `create table if not exists album(
        id int(4) primary key auto_increment not null,
        filepath varchar(255),
        filename varchar(64),
        status int(4),
        create_time int(10)
        );`
	_, err = ModifyDB(sqlStr)
	return
}

// 操作数据库
func ModifyDB(sql string, args ...interface{}) (int64, error) {
	result, err := db.Exec(sql, args...)
	if err != nil {
		fmt.Println(err)
		return 0, err
	}
	count, err := result.RowsAffected()
	if err != nil {
		fmt.Println(err)
		return 0, err
	}
	return count, nil
}

// 查询
func QueryRowDB(dest interface{}, sql string, args ...interface{}) error {
	return db.Get(dest, sql, args...)
}

// 查询多条
func QueryRows(dest interface{}, sql string, args ...interface{}) error {
	return db.Select(dest, sql, args...)
}

  • redis.go
package dao

import (
	"blogweb_gin/config"
	"fmt"
	"github.com/go-redis/redis"
)

var (
	Client *redis.Client
)

// 初始化连接
func InitRedis(cfg *config.RedisConfig) (err error) {
	Client = redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
		Password: cfg.Password, // no password set
		DB:       cfg.DB,       // use default DB
	})

	_, err = Client.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

  • redis_key.go
package dao

const (
	KeyArticleCount = "blog:article:read:count:%s" // 24小时文章阅读数key eq:blog:article:count:20200315
)

logger

  • logger.go
package logger

import (
	"blogweb_gin/config"
	"github.com/gin-gonic/gin"
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

var Logger *zap.Logger // 我们在项目用都使用这个日志对象

// 初始化Logger
func InitLogger(cfg *config.LogConfig) (err error) {
	ws := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge) // 做日志切割第三方包
	encoder := getEncoder()                                                   // 日志输出的格式
	var level = new(zapcore.Level)
	err = level.UnmarshalText([]byte(cfg.Level))
	if err != nil {
		return
	}
	core := zapcore.NewCore(encoder, ws, level)
	Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
	return
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间字符串
	encoderConfig.TimeKey = "time"
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
	encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder // 函数调用
	return zapcore.NewJSONEncoder(encoderConfig)            // JSON格式
}

func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    maxSize,
		MaxBackups: maxBackup,
		MaxAge:     maxAge,
	}
	return zapcore.AddSync(lumberJackLogger)
}

// 参考:gin-zap 这个库

// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

func Debug(msg string, fields ...zap.Field) {
	Logger.Debug(msg, fields...) // logger.go
}

func Info(msg string, fields ...zap.Field) {
	Logger.Info(msg, fields...)
}

func Warn(msg string, fields ...zap.Field) {
	Logger.Warn(msg, fields...)
}

func Error(msg string, fields ...zap.Field) {
	Logger.Error(msg, fields...)
}

logic

  • logic.go
package logic

import (
	"blogweb_gin/dao"
	"blogweb_gin/logger"
	"blogweb_gin/models"
	"fmt"
	"go.uber.org/zap"
	"strconv"
	"time"
)

// 点击文章,阅读数加 1
// 每次请求`/article/show/:id`URL的时候 执行redis命令 zincrby code_language 1 golang

// 给指定文章的阅读数+1
func IncArticleReadCount(articleId string) error {
	// zincrby code_language 1 golang
	todayStr := time.Now().Format("20060102")
	key := fmt.Sprintf(dao.KeyArticleCount, todayStr)

	return dao.Client.ZIncrBy(key, 1, articleId).Err()
}

// 获取阅读排行榜排名前N的文章
func GetArticleReadCountTopN(n int64) []*models.Article {
	// 1. zrevrange Key 0 n-1 从redis取出前n位的文章id
	todayStr := time.Now().Format("20060102")
	key := fmt.Sprintf(dao.KeyArticleCount, todayStr)
	idStrs, err := dao.Client.ZRevRange(key, 0, n-1).Result()
	if err != nil {
		logger.Error("ZRevRange", zap.Any("error", err))
	}

	// 2. 根据上一步获取的文章id查询数据库取文章标题  ["3" "1" "5"]
	// select id, title from article where id in (3, 1, 5);  // 文章的顺序对吗? 不对
	// 		1. 让MySQL排序
	// select id, title from article where id in (3, 1, 5) order by FIND_IN_SET(id, (3, 1, 5));
	// 		2. 查询出来自己排序
	// 先准备好要查询的ID Slice
	var ids = make([]int64, len(idStrs))
	for _, idStr := range idStrs {
		id, err := strconv.ParseInt(idStr, 0, 16)
		if err != nil {
			logger.Warn("ArticleTopN:strconv.ParseInt failed", zap.Any("error", err))
			continue
		}

		ids = append(ids, id)
	}

	articleList, err := models.QueryArticlesByIds(ids, idStrs)
	if err != nil {
		logger.Error("queryArticlesByIds", zap.Any("error", err))
	}
	return articleList
}

middlewares

  • auth.go
package middlewares

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"net/http"
)

// 最基础的认证校验 只要cookie中带了login_user标识就认为是登录用户
func BasicAuth() func(c *gin.Context) {
	return func(c *gin.Context) {
		// c代表了请求相关的所有内容,获取当前请求对应的session数据
		session := sessions.Default(c)

		loginUser := session.Get("login_user")
		// 请求对应的session中找不到我想要的数据,说明不是登录的用户
		if loginUser == nil {
			c.Redirect(http.StatusFound, "/login")
			c.Abort() // 终止当前请求的处理函数调用链
			return    // 终止当前处理函数
		}

		// 根据loginUser 去数据库里用户对象取出来  gob是go语言里面二进制的数据格式
		// 如果是一个登录的用户,我就在c上设置两个自定义的键值对!!!
		c.Set("is_login", true)
		c.Set("login_user", loginUser)
		c.Next()
	}
}

models

  • album.go
package models

import "blogweb_gin/dao"

type Album struct {
	Id         int
	Filepath   string
	Filename   string
	Status     int
	CreateTime int64 `db:"create_time"`
}

// 增加图片
func AddAlbum(album *Album) (int64, error) {
	return dao.ModifyDB("insert into album(filepath,filename,status,create_time)values(?,?,?,?)",
		album.Filepath, album.Filename, album.Status, album.CreateTime)
}

// 获取图片
func QueryAlbum() (dest []*Album, err error) {
	sqlStr := "select id,filepath,filename,status,create_time from album"
	err = dao.QueryRows(&dest, sqlStr)
	return
}

  • article.go
package models

import (
	"blogweb_gin/dao"
	"blogweb_gin/logger"
	sql "github.com/jmoiron/sqlx"
	"go.uber.org/zap"
	"strings"
)

const (
	pageSize = 4
)

type Article struct {
	Id         int    `json:"id",form:"id"`
	Title      string `json:"title",form:"title"`
	Tags       string `json:"tags",form:"tags"`
	Short      string `json:"short",form:"short"`
	Content    string `json:"content",form:"content"`
	Author     string
	CreateTime int64 `db:"create_time"`
	Status     int   // Status=0为正常,1为删除,2为冻结
}

//-----------数据库操作---------------

// 增加文章
func AddArticle(article *Article) (int64, error) {
	return dao.ModifyDB("insert into article(title,tags,short,content,author,create_time,status) values(?,?,?,?,?,?,?)",
		article.Title, article.Tags, article.Short, article.Content, article.Author, article.CreateTime, article.Status)
}

// 更新文章
func UpdateArticle(article *Article) (int64, error) {
	sqlStr := "update article set title=?,tags=?,short=?,content=? where id=?"
	return dao.ModifyDB(sqlStr, article.Title, article.Tags, article.Short, article.Content, article.Id)
}

// 删除文章
func DeleteArticle(id string) (int64, error) {
	sqlStr := "delete from article where id=?"
	return dao.ModifyDB(sqlStr, id)
}

// 查询所有文章

/**
分页查询数据库
limit分页查询语句,
    语法:limit m,n

    m代表从多少位开始获取,与id值无关
    n代表获取多少条数据

	总共有10条数据,每页显示4条。  --> 总共需要(10-1)/4+1 页。
	问第2页数据是哪些?           --> 5,6,7,8  (2-1)*4,4

*/
// 查询数据库文章
func QueryAllArticle() ([]*Article, error) {
	sqlStr := "select id,title,tags,short,content,author,create_time from article"
	var articleList []*Article
	err := dao.QueryRows(&articleList, sqlStr)
	if err != nil {
		return nil, err
	}
	return articleList, nil
}

// 根据Page查询文章
func QueryCurrUserArticleWithPage(username string, pageNum int) (articleList []*Article, err error) {
	sqlStr := "select id,title,tags,short,content,author,create_time from article where author=? limit ?,?"

	articleList, err = queryArticleWithCon(pageNum, sqlStr, username)
	if err != nil {
		logger.Debug("queryArticleWithCon, ", zap.Any("error", err))
		return nil, err
	}

	logger.Debug("QueryCurrUserArticleWithPage,", zap.Any("articleList", articleList))
	return articleList, nil
}

// 根据Id查询文章
func QueryArticleWithId(id string) (article *Article, err error) {
	article = new(Article)
	sqlStr := "select id,title,tags,short,content,author,create_time from article where id=?"
	err = dao.QueryRowDB(article, sqlStr, id)
	return
}

// 根据查询条件查询指定页数有的文章
func queryArticleWithCon(pageNum int, sqlStr string, args ...interface{}) (articleList []*Article, err error) {
	pageNum--
	args = append(args, pageNum*pageSize, pageSize)
	logger.Debug("queryArticleWithCon", zap.Any("pageNum", pageNum), zap.Any("args", args))
	err = dao.QueryRows(&articleList, sqlStr, args...)
	logger.Debug("dao.QueryRows result", zap.Any("articleList", articleList))
	return
}

// 查询文章的总条数
func QueryArticleRowNum() (num int, err error) {
	err = dao.QueryRowDB(&num, "select count(id) from article")
	return
}

// 根据id查文章 按顺序
func QueryArticlesByIds(ids []int64, idStrs []string) ([]*Article, error) {
	// 让MySQL排序
	query, args, err := sql.In("select id, title from article where id in (?) order by FIND_IN_SET(id, ?)", ids, strings.Join(idStrs, ","))
	if err != nil {
		logger.Error("QueryArticlesByIds", zap.Any("error", err))
		return nil, err
	}

	var dest []*Article
	err = dao.QueryRows(&dest, query, args...)
	return dest, err
}

  • home.go
package models

import (
	"blogweb_gin/logger"
	"blogweb_gin/utils"
	"fmt"
	"go.uber.org/zap"
	"strconv"
	"strings"
)

type HomeBlockParam struct {
	Article *Article

	TagLinks      []*TagLink
	CreateTimeStr string
	//查看文章的地址
	Link string

	//修改文章的地址
	UpdateLink string
	DeleteLink string

	//记录是否登录
	IsLogin bool
}

type TagLink struct {
	TagName string
	TagUrl  string
}

// HomePagination 分页器
type HomePagination struct {
	HasPre   bool
	HasNext  bool
	ShowPage string
	PreLink  string
	NextLink string
}

//将tags字符串转化成首页模板所需要的数据结构
func createTagsLinks(tagStr string) []*TagLink {
	var tagLinks = make([]*TagLink, 0, strings.Count(tagStr, "&"))
	tagList := strings.Split(tagStr, "&")
	for _, tag := range tagList {
		tagLinks = append(tagLinks, &TagLink{tag, "/?tag=" + tag})
	}
	return tagLinks
}

// 生成home页面数据结构
func GenHomeBlocks(articleList []*Article, isLogin bool) (ret []*HomeBlockParam) {
	// 内存申请一次到位
	ret = make([]*HomeBlockParam, 0, len(articleList))
	for _, art := range articleList {
		// 将数据库model转换为首页模板所需要的model
		homeParam := HomeBlockParam{
			Article: art,
			IsLogin: isLogin,
		}
		homeParam.TagLinks = createTagsLinks(art.Tags)
		homeParam.CreateTimeStr = utils.SwitchTimeStampToStr(art.CreateTime)

		homeParam.Link = fmt.Sprintf("/article/show/%d", art.Id)
		homeParam.UpdateLink = fmt.Sprintf("/article/update?id=%d", art.Id)
		homeParam.DeleteLink = fmt.Sprintf("/article/delete?id=%d", art.Id)
		ret = append(ret, &homeParam) // 不再需要动态扩容
	}
	return
}

// 生成home页面分页数据结构
func GenHomePagination(page int) *HomePagination {
	pageObj := new(HomePagination)

	// 查询出总的条数
	num, _ := QueryArticleRowNum()

	// 从配置文件中读取每页显示的条数
	// 计算出总页数
	allPageNum := (num-1)/pageSize + 1

	pageObj.ShowPage = fmt.Sprintf("%d/%d", page, allPageNum)

	//当前页数小于等于1,那么上一页的按钮不能点击
	if page <= 1 {
		pageObj.HasPre = false
	} else {
		pageObj.HasPre = true
	}

	//当前页数大于等于总页数,那么下一页的按钮不能点击
	if page >= allPageNum {
		pageObj.HasNext = false
	} else {
		pageObj.HasNext = true
	}

	pageObj.PreLink = "/?page=" + strconv.Itoa(page-1)
	pageObj.NextLink = "/?page=" + strconv.Itoa(page+1)
	logger.Debug("GenHomePagination", zap.Any("pageObj", *pageObj))
	return pageObj
}

  • user.go
package models

import (
	"blogweb_gin/dao"
)

// 定义 模型 与 数据库中的表相对应

type User struct {
	Id         int
	Username   string
	Password   string
	Status     int // 0 正常状态, 1删除
	CreateTime int64
}

//--------------数据库操作-----------------

// 插入新注册的用户
func InsertUser(user *User) (int64, error) {
	return dao.ModifyDB("insert into users(username,password,status,create_time) values (?,?,?,?)",
		user.Username, user.Password, user.Status, user.CreateTime)
}

// 根据用户名查询id
func QueryUserWithUsername(username string) int {
	var user User
	err := dao.QueryRowDB(&user, "select id from users where username=?", username)
	if err != nil {
		return 0
	}
	return user.Id
}

//根据用户名和密码,查询id
func QueryUserWithParam(username, password string) int {
	var user User
	err := dao.QueryRowDB(&user, "select id from users where username=? and password=?", username, password)
	if err != nil {
		return 0
	}
	return user.Id
}

  • router.go
package routers

import (
	"blogweb_gin/controllers"
	"blogweb_gin/logger"
	"blogweb_gin/middlewares"
	"github.com/gin-contrib/sessions" // session包 定义了一套session操作的接口 类似于 database/sql
	"github.com/gin-gonic/gin"
	"html/template"
	"time"

	//"github.com/gin-contrib/sessions/cookie"  // session具体存储的介质
	"github.com/gin-contrib/sessions/redis" // session具体存储的介质
	//"github.com/gin-contrib/sessions/memcached"  // session具体存储的介质

	// github.com/go-redis/redis  --> go连接redis的一个第三方库
)

// 设置路由
func SetupRouter() *gin.Engine {
	r := gin.New()

	// 接管Gin框架的Logger模块  和 Recovery模块
	r.Use(logger.GinLogger(logger.Logger), logger.GinRecovery(logger.Logger, true))

	// 设置时间格式
	r.SetFuncMap(template.FuncMap{
		"timeStr": func(timestamp int64) string {
			return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
		},
	})

	// 配置静态文件
	r.Static("/static", "static")

	// 配置模板
	r.LoadHTMLGlob("views/*")

	// 设置session   和中间件middleware
	store, _ := redis.NewStore(10, "tcp", "127.0.0.1:6379", "", []byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	// 登录注册 无需认证
	{
		r.GET("/register", controllers.RegisterGet)
		r.POST("/register", controllers.RegisterPost)

		r.GET("/login", controllers.LoginGet)
		r.POST("/login", controllers.LoginPost)

		// 获取阅读排行榜前几
		r.GET("/article/top/:n", controllers.ArticleTopN)
	}

	// 需要认证的一些路由
	{
		// 路由组注册中间件
		basicAuthGroup := r.Group("/", middlewares.BasicAuth())
		basicAuthGroup.GET("/home", controllers.HomeGet)
		basicAuthGroup.GET("/", controllers.IndexGet)
		basicAuthGroup.GET("/logout", controllers.LogoutHandler)

		//路由组
		article := basicAuthGroup.Group("/article")
		{
			// 写文章
			article.GET("/add", controllers.AddArticleGet)
			article.POST("/add", controllers.AddArticlePost)

			// 文章详情
			article.GET("/show/:id", controllers.ShowArticleGet)

			// 更新文章
			article.GET("/update", controllers.UpdateArticleGet)
			article.POST("/update", controllers.UpdateArticlePost)

			// 删除文章
			article.GET("/delete", controllers.DeleteArticle)

		}

		// 相册
		basicAuthGroup.GET("/album", controllers.AlbumGet)

		// 文件上传
		basicAuthGroup.POST("/upload", controllers.UploadPost)
	}

	return r
}

utils

  • tools.go
package utils

import (
	"crypto/md5"
	"fmt"
	"time"
)

const (
	secret = "你猜不到的东西"
)

//传入的数据不一样,那么MD5后的32位长度的数据肯定会不一样
func MD5(str string) string {
	md5str := fmt.Sprintf("%x", md5.Sum(append([]byte(str), []byte(secret)...)))
	return md5str
}

//将传入的时间戳转为时间
func SwitchTimeStampToStr(timeStamp int64) string {
	t := time.Unix(timeStamp, 0)
	return t.Format("2006-01-02 15:04:05")
}

入口函数

  • main.go
package main

import (
	"blogweb_gin/config"
	"blogweb_gin/dao"
	"blogweb_gin/logger"
	"blogweb_gin/routers"
	"fmt"
)

func main() {
	// 用conf/conf.json初始化
	//if len(os.Args) < 2 {
	//	return
	//}
	//if err := config.Init(os.Args[1]); err != nil {
	//	fmt.Printf("config.Init failed, err:%v\n", err)
	//	return
	//}

	// 调试方便,先字符串初始化
	s := `{
"server": {
  "port": 8080
},
"mysql": {
  "host": "127.0.0.1",
  "port": 3306,
  "db": "gin_blog",
  "username": "root",
  "password": "wxlzs999"
},
"redis": {
  "host": "127.0.0.1",
  "port": 6379,
  "db": 0,
  "password": ""
},
"log":{
	"level": "debug",
  "filename": "log/gin_blog.log",
  "maxsize": 500,
  "max_age": 7,
  "max_backups": 10
}
}`

	// 初始化Config的全局变量
	if err := config.InitFromStr(s); err != nil {
		fmt.Printf("config.Init failed, err:%v\n", err)
		return
	}

	// 初始化日志模块
	if err := logger.InitLogger(config.Conf.LogConfig); err != nil {
		fmt.Printf("init logger failed, err:%v\n", err)
		return
	}

	// 初始化Mysql数据库
	if err := dao.InitMySQL(config.Conf.MySQLConfig); err != nil {
		fmt.Printf("init redis failed, err:%v\n", err)
		return
	}

	// 初始化redis数据库
	if err := dao.InitRedis(config.Conf.RedisConfig); err != nil {
		fmt.Printf("init redis failed, err:%v\n", err)
		return
	}

	// 初始化
	logger.Logger.Info("start project...")

	r := routers.SetupRouter() // 初始化路由
	r.Run()
}

用到的第三方库

module blogweb_gin

go 1.14

require (
	github.com/gin-contrib/sessions v0.0.3
	github.com/gin-gonic/gin v1.6.3
	github.com/go-ini/ini v1.62.0
	github.com/go-redis/redis v6.15.9+incompatible
	github.com/go-sql-driver/mysql v1.5.0
	github.com/jmoiron/sqlx v1.2.0
	github.com/natefinch/lumberjack v2.0.0+incompatible
	github.com/smartystreets/goconvey v1.6.4 // indirect
	go.uber.org/zap v1.16.0
	gopkg.in/ini.v1 v1.62.0 // indirect
	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
)

分析总结

1、处理请求中,Gin框架的*Context贯穿全程,前面的处理函数Set一个值,后面的函数就可以Get
2、核心思路是,一个请求对应一个响应
3、使用MySql的时候,记得导入驱动,Go对各个数据库开放了驱动接口,一个数据库实现该接口,则实现了一个数据库

`database/sql` 是一套接口,第三方去实现

4、MySQL优化方向:SQL语句 --> 表结构设计 --> 数据库配置
5、Session示意图
在这里插入图片描述
6、项目配置文件,为了避免 硬编码:写死在代码里的参数
7、上传的文件存储在数据库中,数据库存储的都是文件的“路径”
8、防刷的问题

防刷的问题

如何防止某些人频繁的访问某篇文章刷点击?

关键点在于:如何区分正常的阅读数和不正常的阅读数。

- 根据ip来
- 根据访问的用户来
- 24小时时间内 同一个用户对某一篇文章的点击只记录一次!
  • 28
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值