之前利用空闲时间做了一个个人博客系统,后端主要使用golang+gin框架+Mysql实现,前端使用vue+element ui + bootstrap。项目的部署使用docker,nginx作为静态资源服务器和反向代理服务器。
结构图如下:
1、效果图
访问地址:我的博客
博客首页:
博客详情页
文章标签
归档
资源库
博客后台管理
2、数据库设计
数据库主要包含12张表,分别是用户表、博客类型表、博客标签表、资源库表、资源类别表、格言表、留言表、随笔表、博客评论表、博客标签中间表、博客信息表、背景图片表,如下图所示:
用户表
博客类型表
博客标签表
中间表:
资源库表
资源类别表:
格言表
留言表
随笔表
博客评论表
博客详情表
背景图片表
3、golang gin框架使用和封装
3.1 配置文件读取
配置文件的读取使用viper,地址:github.com/spf13/viper
package utils
import (
"fmt"
"github.com/spf13/viper"
)
func init() {
viper.SetConfigName("application")
viper.SetConfigType("yaml")
viper.AddConfigPath("./conf")
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("an error occurs when load conf file:%v", err))
}
CreateLogger()
}
配置文件主要包含server、mysql、log以及阿里云OSS的相关配置
server:
host: "0.0.0.0"
port: 8080
imagePath: "./" # 图片保存路径
imageBaseUrl: "http://localhost:8080" # 图片的网络访问基路径
mysql:
dataSourceName: "root:passwd@(192.168.44.100:3306)/blog?charset=utf8mb4&parseTime=true&loc=Local"
maxOpenConns: 20 # 连接池最大连接数
maxIdleConns: 10 # 连接池最大空闲连接数
log:
filepath: "./logs"
filename: "blog.log"
toFile: false
level: "DEBUG"
aliOSS:
endPoint: "" # 注意:在填写endPoint时要使用带有bucketName的地址,否则会上传失败,例如:my-upload.oss-cn-beijing.aliyuncs.com,前面无需加http
accessKeyId: ""
accessKeySecret: ""
bucket: ""
maxImageSize: 5242880 # 上传的最大图片大小,如果大于该文件大小,将会对图片进行压缩
3.2 JWT的使用
JWT主要用于登录时的Token发放和认证
package utils
import (
"github.com/dgrijalva/jwt-go"
"time"
)
type Claim struct {
Username string
UserId uint32
jwt.StandardClaims
}
var jwtKey = []byte("golang-server")
func CreateToken(id uint32, username string, expireDuration time.Duration) (string, error) {
claims := &Claim{
Username: username,
UserId: id,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
Issuer: "asyouwant",
Subject: "User_Token",
ExpiresAt: time.Now().Add(expireDuration).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenStr, nil
}
func VerifyToken(token string) (string, uint32, bool) {
if token == "" {
return "", 0, false
}
tok, err := jwt.ParseWithClaims(token, &Claim{}, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
Logger().Warning("ParseWithClaims error %v", err)
return "", 0, false
}
if claims, ok := tok.Claims.(*Claim); ok && tok.Valid {
return claims.Username, claims.UserId, true
} else {
Logger().Warning("%v", err)
return "", 0, false
}
}
3.3 mysql数据库驱动
mysql数据库驱动使用的是sqlx,这个库需要自己写sql语句,也可以使用gorm
package dao
import (
_ "blog_web/utils"
"context"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/spf13/viper"
"time"
)
/*
* @Author: mgh
* @Date: 2022/2/23 21:32
* @Desc:
*/
var sqldb *sqlx.DB
var Sqldb *sqlx.DB
type MysqlConf struct {
DataSourceName string `json:"data_source_name"`
MaxOpenConns uint32 `json:"max_open_conns"`
MaxIdleConns uint32 `json:"max_idle_conns"`
}
var conf = &MysqlConf{
DataSourceName: viper.GetString("mysql.dataSourceName"),
MaxOpenConns: viper.GetUint32("mysql.maxOpenConns"),
MaxIdleConns: viper.GetUint32("mysql.maxIdleConns"),
}
func init() {
fmt.Println("mysql conf:", conf)
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
var err error
sqldb, err = sqlx.ConnectContext(timeoutCtx, "mysql", conf.DataSourceName)
if err != nil {
panic(err)
}
Sqldb = sqldb
sqldb.SetMaxOpenConns(int(conf.MaxOpenConns))
sqldb.SetMaxIdleConns(int(conf.MaxIdleConns))
fmt.Println("Connect mysql success")
}
func CloseMysqlConn() {
sqldb.Close()
}
3.4 使用装饰器来简化使用gin时的controller
在使用gin框架的时候,需要在controller中使用ctx.JSON或其它接口来返回数据,如果有很多的分支语句那么就会出现很多该类代码,不仅看起来不舒服,写起来也不是很顺畅。如果将数据返回就可以将数据写出,那么就会变得更方便,这时可以使用装饰器来简化。
为了统一返回的数据,我们需要先定义一个response数据结构:
package response
import (
"net/http"
"sync"
)
type resResult struct {
Data []interface{} `json:"data"`
Status uint32 `json:"status"`
Message string `json:"message"`
}
type Response struct {
HttpStatus int
R resResult
}
var pool = sync.Pool{
New: func() interface{} {
return &Response{}
},
}
func NewResponse(status int, code uint32, data ...interface{}) *Response {
response := pool.Get().(*Response)
response.HttpStatus = status
response.R.Status = code
response.R.Message = MessageForCode[code]
if (len(data) == 0) {
response.R.Data = make([]interface{}, 0)
} else {
response.R.Data = data
}
return response
}
func PutResponse(res *Response) {
pool.Put(res)
}
func NewResponseOk(code uint32, data ...interface{}) *Response {
return NewResponse(http.StatusOK, code, data...)
}
func NewResponseNoData(status int, code uint32) *Response {
return NewResponse(status, code)
}
func NewResponseOkND(code uint32) *Response {
return NewResponse(http.StatusOK, code)
}
func ResponseQuerySuccess(data ...interface{}) *Response {
return NewResponse(http.StatusOK, QuerySuccess, data...)
}
func ResponseQueryFailed() *Response {
return NewResponse(http.StatusOK, QueryFailed)
}
func ResponseOperateFailed() *Response {
return NewResponse(http.StatusOK, OperateFailed)
}
func ResponseOperateSuccess() *Response {
return NewResponse(http.StatusOK, OperateSuccess)
}
func ResponseDeleteFailed() *Response {
return NewResponse(http.StatusOK, DeleteFailed)
}
func ResponseDeleteSuccess() *Response {
return NewResponse(http.StatusOK, DeleteSuccess)
}
定义一些返回码以及相关描述:
package response
const (
QueryFailed uint32 = iota
QuerySuccess
OperateFailed = iota + 98
OperateSuccess
LoginFailed = iota + 196
LoginSuccess
Unauthorized = iota + 294
DeleteFailed = iota + 393
DeleteSuccess
)
var MessageForCode = map[uint32]string{
QuerySuccess: "查询成功",
QueryFailed: "查询失败",
OperateFailed: "操作失败",
OperateSuccess: "操作成功",
LoginFailed: "登录失败",
LoginSuccess: "登录成功",
Unauthorized: "未获得授权",
DeleteFailed: "删除失败",
DeleteSuccess: "删除成功",
}
装饰器:
type Handler func(ctx *gin.Context) *response.Response
// 装饰器
func Decorate(h Handler) gin.HandlerFunc {
return func(ctx *gin.Context) {
r := h(ctx)
if r != nil {
ctx.JSON(r.HttpStatus, &r.R)
}
response.PutResponse(r)
}
}
在定义controller时,就可以使用返回值为Response类型的结构体了,在注册处理函数的时候就可以使用装饰器进行包装。
controller定义:
// 主页博客展示
func (h *HomeController) HomeListBlogs(ctx *gin.Context) *response.Response {
pageNum := utils.QueryInt(ctx, "pageNum")
pageSize := utils.QueryInt(ctx, "pageSize")
utils.Logger().Debug("pageNum:%v, pageSize:%v", pageNum, pageSize)
if pageNum <= 0 || pageSize <= 0 {
return response.ResponseQueryFailed()
}
blogs, err := h.blogService.GetHomePageBlogs(pageNum, pageSize)
if response.CheckError(err, "Get Blogs error") {
return response.ResponseQueryFailed()
}
count, err := h.blogService.GetBolgCount()
if response.CheckError(err, "Get Blogs error") {
return response.ResponseQueryFailed()
}
return response.ResponseQuerySuccess(blogs, count)
}
注册处理函数:
blogGroup.GET("/blogLists", Decorate(homeRouter.HomeListBlogs))
3.5 图片存储
图片的存储实现了两种方式,第一种是直接存在web服务器所在的主机上,第二种是使用阿里云OSS来进行图片存储。如果你的服务器带宽太低的话,就可以将图片存在阿里云OSS,速度更快,存储空间很便宜,不过下行流量需要额外付费。
定义图片存储服务接口
type UploadServicer interface {
UploadImage(string, io.Reader) (string, error)
}
阿里云OSS图片存储接口:
/*
OssUploadService: 阿里云OSS对象存储服务存储图片
*/
type OssUploadService struct {
endPoint string
accessKeyId string
accessKeySecret string
bucketName string
client *oss.Client
}
func NewOssUploadService(endPoint, accessKeyId, accessKeySecret, bucketName string) *OssUploadService {
o := &OssUploadService{
endPoint: endPoint,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
bucketName: bucketName,
}
o.init()
return o
}
func (o *OssUploadService) init() {
client, err := oss.New(o.endPoint, o.accessKeyId, o.accessKeySecret, oss.UseCname(true))
if err != nil {
panic(fmt.Errorf("Create oss client failed:%v", err))
}
o.client = client
}
func (o *OssUploadService) UploadImage(objName string, reader io.Reader) (string, error) {
bucket, err := o.client.Bucket(o.bucketName)
if err != nil {
panic(fmt.Errorf("Get bucket failed:%v", err))
}
err = bucket.PutObject(objName, reader)
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s/%s", o.endPoint, objName), nil
}
本地图片存储服务接口
/*
LocalUploadService: 本地存储图片
*/
type LocalUploadService struct {
filePath string // ./images
netBasePath string // http://localhost:8080/images
}
func NewLocalUploadService(filePath, netBasePath string) *LocalUploadService {
return &LocalUploadService{
filePath: filePath,
netBasePath: netBasePath,
}
}
func (l *LocalUploadService) UploadImage(objName string, reader io.Reader) (string, error) {
dst := path.Join(l.filePath, objName)
out, err := os.Create(dst)
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, reader)
return path.Join(l.netBasePath, objName) , err
}
前端的部分太过繁琐就先不介绍了。