Golang+Mysql+Vue+Nginx前后端分离项目--个人博客

之前利用空闲时间做了一个个人博客系统,后端主要使用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
}

前端的部分太过繁琐就先不介绍了。
 

  • 8
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值