根据前端提供的基础,实现业务逻辑的开发,主要包括:
用户的登陆注册功能登陆后,用户具有撰写帖、修改帖子、阅读帖子以及删除帖子的需求提供一个相册,用户可以上传图片、展示图片实现一个分页机制,使得可以限定每页展示的帖子数量(即上一页,下一页)提供一个社区阅读排行榜,统计文章点击的数量,并且进行显示(类似于微博热搜)注意:
项目需要提供一个配置组件,提供一个配置文件,减少后期代码的改动项目需要提供一个日志组件,便于记录/调试项目需要提供一个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小时时间内 同一个用户对某一篇文章的点击只记录一次!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)