src/dao/article.go
package dao
import (
"JuneGoBlog/src"
"JuneGoBlog/src/consts"
"errors"
"fmt"
juneDao "github.com/520MianXiangDuiXiang520/GinTools/dao"
juneLog "github.com/520MianXiangDuiXiang520/GinTools/log"
"github.com/garyburd/redigo/redis"
"log"
"reflect"
"strconv"
"strings"
)
/**
* WiKi: 通过缓存中文章ID列表的下标得到文章ID
* Author: JuneBao
* Time: 2020/8/22 16:21
**/
func QueryArticleIDFromCacheByIndex(index int) (int, error) {
rc := juneDao.GetRedisConn()
defer rc.Close()
value, err := redis.String(rc.Do("LINDEX", consts.ArticleIDListCache, index))
v, _ := strconv.Atoi(value)
return v, err
}
/**
* WiKi: 从缓存中查询文章ID列表
* Author: JuneBao
* Time: 2020/8/22 0:45
**/
func queryArticleIDListFromCache(page, pageSize, total int) ([]int, error) {
start := (page-1)*pageSize + 1
r := make([]int, 0)
rc := juneDao.GetRedisConn()
defer rc.Close()
newPageSize := total - start + 1
if newPageSize < pageSize {
pageSize = newPageSize
}
for i := 0; i < pageSize; i++ {
if err := rc.Send("lIndex", consts.ArticleIDListCache, i+start-1); err != nil {
log.Fatal("Send Lindex ERROR!")
return r, err
}
}
if err := rc.Flush(); err != nil {
log.Fatal("Flush Lindex ERROR!")
return r, err
}
for i := 0; i < pageSize; i++ {
result, err := rc.Receive()
if err != nil {
log.Fatal("Send Lindex ERROR!")
return r, err
}
if result == nil {
return nil, errors.New("ReceiveNil")
}
re, _ := strconv.Atoi(string(result.([]byte)))
r = append(r, re)
}
return r, nil
}
/**
* WiKi: 向缓存中插入一条文章信息记录
* Author: JuneBao
* Time: 2020/8/22 11:52
**/
func setNewArticleInfoToCache(id int, article *ArticleListInfo) error {
rc := juneDao.GetRedisConn()
defer rc.Close()
fields := []string{
"Title", "Abstract", "ID", "Author", "CreateTime",
}
immutable := reflect.ValueOf(article).Elem()
for _, field := range fields {
val := immutable.FieldByName(field)
_, err := rc.Do("HSET", consts.ArticleInfoHashCache+strconv.Itoa(id), field, val)
if err != nil {
log.Printf("setNewArticleInfoToCache 执行 HSET 失败, id = [%v]", id)
return err
}
}
// 插入Tag 信息
tagIDs := make([]string, len(article.Tags))
for i, tag := range article.Tags {
tagIDs[i] = strconv.Itoa(tag.ID)
}
_, err := rc.Do("HSET", consts.ArticleInfoHashCache+strconv.Itoa(id),
"Tags", strings.Join(tagIDs, consts.CacheTagsSplitStr))
if err != nil {
msg := fmt.Sprintf("Failed to insert tag information into cache, tags = %v, articleID = %v", tagIDs, id)
juneLog.ExceptionLog(err, msg)
return err
}
return nil
}
/**
* WiKi: 文章信息未命中缓存时更新缓存
* Author: JuneBao
* Time: 2020/8/22 11:59
**/
func articleInfoMissHitsUpdate(id int) (ArticleListInfo, error) {
ali, err := QueryArticleListInfoByIDWithDB(id)
if err != nil {
log.Printf("缓存未命中后从数据库中获取信息失败!id = [%v]", id)
return ali, err
}
if ali.ID == 0 {
log.Printf("缓存未命中,id对应文章信息不存在!id = [%v]", id)
return ali, err
}
err = setNewArticleInfoToCache(id, &ali)
if err != nil {
log.Printf("缓存未命中后更新缓存失败, id = [%v]", id)
return ali, err
}
return ali, nil
}
/**
* WiKi: 从缓存中查询单个文章的信息,没有查询到会从数据库中补充
* Author: JuneBao
* Time: 2020/8/22 16:14
**/
func queryArticleInfoFromCache(id int) (ArticleListInfo, error) {
var fields = []string{
"Title", "Abstract", "ID", "CreateTime", "Tags",
}
result := ArticleListInfo{}
articleFields := make([]string, 0)
rc := juneDao.GetRedisConn()
tags := make([]Tag, 0)
// 如果这篇文章的标签信息没在缓存中, 就会从数据库中查找,这样就不需要再根据id查找了
queryTagFlag := true
defer rc.Close()
for _, field := range fields {
err := rc.Send("Hget", consts.ArticleInfoHashCache+strconv.Itoa(id), field)
if err != nil {
msg := fmt.Sprintf("Hget article info from redis fail, article id = %v, field = %v", id, field)
juneLog.ExceptionLog(err, msg)
}
}
juneLog.ExceptionLog(rc.Flush(), "redis flush fail")
for _, field := range fields {
r, err := rc.Receive()
if err != nil {
msg := fmt.Sprintf("do rc.Receive fail, article id = %v", id)
juneLog.ExceptionLog(err, msg)
return result, err
}
// 缓存未命中
if r == nil {
log.Printf("缓存未命中!article id = %v, field = %v\n", id, field)
a, _ := articleInfoMissHitsUpdate(id)
value := reflect.ValueOf(a).FieldByName(field).String()
if field == "Tags" {
tagStr := make([]string, len(a.Tags))
for i, tag := range a.Tags {
tagStr[i] = strconv.Itoa(tag.ID)
}
articleFields = append(articleFields, strings.Join(tagStr, consts.CacheTagsSplitStr))
tags = a.Tags
queryTagFlag = false
} else {
articleFields = append(articleFields, value)
}
continue
}
articleFields = append(articleFields, string(r.([]byte)))
}
if len(articleFields) >= len(fields) {
id, _ := strconv.Atoi(articleFields[2])
cTime, _ := strconv.Atoi(articleFields[3])
tagIDs := strings.Split(articleFields[4], "-")
if queryTagFlag {
for _, tagID := range tagIDs {
tid, _ := strconv.Atoi(tagID)
tag, err := QueryTagByID(tid)
if err != nil {
return result, nil
}
tags = append(tags, *tag)
}
}
result.ID = id
result.Title = articleFields[0]
result.Author = "Junebao"
result.Abstract = articleFields[1]
result.CreateTime = int64(cTime)
result.Tags = tags
}
return result, nil
}
/**
* WiKi: 通过缓存查询文章列表页信息
* Author: JuneBao
* Time: 2020/9/11 23:27
**/
func queryArticleInfoByLimitByCache(page, pageSize, total int) ([]ArticleListInfo, error) {
result := make([]ArticleListInfo, 0)
ids, err := queryArticleIDListFromCache(page, pageSize, total)
if err != nil {
juneLog.LogPlus("从缓存中获取文章ID列表失败")
go func() {
iErr := InitArticleIDListCache()
if iErr != nil {
msg := fmt.Sprintf("Failed to update the article ID in the cache asynchronously")
juneLog.ExceptionLog(iErr, msg)
}
}()
return nil, err
}
for _, id := range ids {
var a ArticleListInfo
var e error
a, e = queryArticleInfoFromCache(id)
if e != nil {
return nil, e
}
result = append(result, a)
}
return result, nil
}
/**
* WiKi: 查询单页文章列表信息
* Author: JuneBao
* Time: 2020/9/11 23:27
**/
func QueryArticleInfoByLimit(page, pageSize int) ([]ArticleListInfo, int, error) {
total, err := QueryArticleTotal()
if err != nil {
return nil, 0, err
}
if src.GetSetting().Others.Redis {
result, err := queryArticleInfoByLimitByCache(page, pageSize, total)
if err == nil {
return result, total, err
}
juneLog.LogPlus("Fail to query from cache!!")
}
// 缓存中没有查到,从数据库中查
start := (page - 1) * pageSize
if total < start {
juneLog.LogPlus("total < start")
return nil, 0, errors.New("total < start")
}
newPageSize := total - start
if newPageSize < pageSize {
pageSize = newPageSize
}
articleList := make([]Article, 0)
err = juneDao.GetDB().Order("create_time desc").Limit(pageSize).Offset(start).Find(&articleList).Error
if err != nil {
return nil, 0, err
}
articleListInfos := make([]ArticleListInfo, len(articleList))
for i, article := range articleList {
tags := make([]Tag, 0)
err := QueryAllTagsByArticleID(article.ID, &tags)
if err != nil {
msg := fmt.Sprintf("get article tags fail, article id = %v", article.ID)
juneLog.ExceptionLog(err, msg)
}
articleListInfos[i] = ArticleListInfo{
Tags: tags,
ID: article.ID,
Title: article.Title,
CreateTime: article.CreateTime.Unix(),
Abstract: article.Abstract,
Author: "Junebao",
}
}
return articleListInfos, total, nil
}
/**
* WiKi: 查询某个 Tag 下的单页文章信息
* Author: JuneBao
* Time: 2020/9/24 10:56
**/
func QueryArticleInfoByLimitWithTag(tagID, page, pageSize int) ([]ArticleListInfo, int, error) {
aIDs, err := QueryAllArticleByTagID(tagID)
if err != nil {
return nil, 0, err
}
total := len(aIDs)
start := (page - 1) * pageSize
if total < start {
juneLog.LogPlus("total < start")
return nil, 0, errors.New("total < start")
}
newPageSize := total - start
if newPageSize < pageSize {
pageSize = newPageSize
}
articleList := make([]ArticleListInfo, 0)
if total > pageSize {
aIDs = aIDs[start : start+pageSize]
}
for _, a := range aIDs {
article, err := QueryArticleByID(a.ArticleID)
if err != nil {
return nil, 0, err
}
articleList = append(articleList, article)
}
return articleList, total, nil
}
func QueryAllArticle(articleList *[]Article) error {
return juneDao.GetDB().Order("create_time desc").Find(&articleList).Error
}
/**
* WiKi: 通过文章ID在数据库中查询文章信息
* Author: JuneBao
* Time: 2020/8/22 11:40
**/
func QueryArticleListInfoByIDWithDB(id int) (ArticleListInfo, error) {
result := ArticleListInfo{}
article := Article{}
err := juneDao.GetDB().Select("id, title, abstract,"+
" author_id, create_time").Where("id = ?", id).First(&article).Error
if err != nil {
msg := fmt.Sprintf("query article by id fail, article id = %v", id)
juneLog.ExceptionLog(err, msg)
return result, err
}
tags := make([]Tag, 0)
err = QueryAllTagsByArticleID(id, &tags)
if err != nil {
msg := fmt.Sprintf("query all tags by articleID fail, articleID = %v", id)
juneLog.ExceptionLog(err, msg)
return result, err
}
result = ArticleListInfo{
ID: article.ID,
Title: article.Title,
Abstract: article.Abstract,
CreateTime: article.CreateTime.Unix(),
Author: "Junebao",
Tags: tags,
}
return result, nil
}
/**
* WiKi: 通过 ID 查询文章信息
* Author: JuneBao
* Time: 2020/9/24 10:56
**/
func QueryArticleByID(id int) (ArticleListInfo, error) {
var articleListInfo ArticleListInfo
if src.GetSetting().Others.Redis {
return queryArticleInfoFromCache(id)
}
articleListInfo, err := QueryArticleListInfoByIDWithDB(id)
return articleListInfo, err
}
func QueryArticleDetail(id int) (Article, error) {
a := Article{}
r := juneDao.GetDB().Where("id = ?", id).First(&a)
return a, r.Error
}
func queryArticleTotalByCache() (int, error) {
rc := juneDao.GetRedisConn()
defer rc.Close()
result, err := redis.Int(rc.Do("LLen", consts.ArticleIDListCache))
if err != nil {
msg := fmt.Sprintf("Fail to query articles total")
juneLog.ExceptionLog(err, msg)
return 0, err
}
if result == 0 {
msg := fmt.Sprintf("The total number of articles queried from the cache is 0")
err := errors.New("ResultIsZero")
juneLog.ExceptionLog(err, msg)
return 0, err
}
return result, nil
}
func queryArticleTotalByDB() (int, error) {
var total int
c := juneDao.GetDB().Model(&Article{}).Count(&total)
return total, c.Error
}
func QueryArticleTotal() (int, error) {
var total int
var err error
if src.GetSetting().Others.Redis {
total, err = queryArticleTotalByCache()
if err != nil {
log.Printf("通过缓存获取文章总数失败!!!: %v", err)
return queryArticleTotalByDB()
}
return total, nil
}
return queryArticleTotalByDB()
}
func addArticleWithCache(newArticle *Article, tags []Tag) error {
// 更新 ArticleIDList
rp := juneDao.GetRedisConn()
var err error
defer func() {
rp.Close()
}()
_, err = rp.Do("LPUSH", consts.ArticleIDListCache, newArticle.ID)
if err != nil {
msg := fmt.Sprintf("update %v fail, article id = %v", consts.ArticleIDListCache, newArticle.ID)
juneLog.ExceptionLog(err, msg)
return err
}
// 更新 ArticleListInfo
err = setNewArticleInfoToCache(newArticle.ID, &ArticleListInfo{
ID: newArticle.ID,
Title: newArticle.Title,
Abstract: newArticle.Abstract,
CreateTime: newArticle.CreateTime.Unix(),
Author: "Junebao",
Tags: tags,
})
if err != nil {
msg := fmt.Sprintf("update %v fail, article id = %v", "article info", newArticle.ID)
juneLog.ExceptionLog(err, msg)
}
return err
}
func AddArticle(newArticle *Article, tagIDs []int) (*Article, error) {
tx := juneDao.GetDB().Begin()
var err error
defer func() {
if err != nil {
msg := fmt.Sprintf("insert new article fail, title = %v", newArticle.Title)
juneLog.ExceptionLog(err, msg)
tx.Rollback()
}
tx.Commit()
}()
err = tx.Create(newArticle).Error
if err != nil {
return nil, err
}
// 更新 article_tag 表
tags := make([]Tag, len(tagIDs))
for i, tagID := range tagIDs {
// 保证 Tag 存在
tag, err := QueryTagByID(tagID)
if err != nil {
return nil, err
}
if tag == nil {
msg := fmt.Sprintf("Get nil when query tag, tagID = %v", tagID)
err := errors.New("return nil")
juneLog.ExceptionLog(err, msg)
return nil, err
}
tags[i] = *tag
// 插入一条 ArticleTags 记录
err = tx.Create(&ArticleTags{
ArticleID: newArticle.ID,
TagID: tagID,
}).Error
if err != nil {
msg := fmt.Sprintf("fail to insert new article tag;"+
" articleID = %v, tagID = %v", newArticle.ID, tagID)
juneLog.ExceptionLog(err, msg)
return nil, err
}
err = lockedUpdateArticleTotal(tx, tagID, 1)
if err != nil {
return nil, err
}
}
if src.GetSetting().Others.Redis {
_ = addArticleWithCache(newArticle, tags)
}
return newArticle, err
}
func hasArticleWithDB(id int) bool {
article := &Article{}
juneDao.GetDB().Where("id = ?", id).First(article)
return article.ID != 0
}
func HasArticle(id int) bool {
if src.GetSetting().Others.Redis {
// TODO: bitmap
}
return hasArticleWithDB(id)
}
func updateArticleWithCache(id int, article *Article) error {
rc := juneDao.GetRedisConn()
defer func() {
rc.Close()
}()
var err error
err = rc.Send("HSet", consts.ArticleInfoHashCache+strconv.Itoa(id),
"Title", article.Title)
err = rc.Send("HSet", consts.ArticleInfoHashCache+strconv.Itoa(id),
"Abstract", article.Abstract)
if err != nil {
msg := fmt.Sprintf("Fail to send new value with cache when update article, article = %v", article)
juneLog.ExceptionLog(err, msg)
return err
}
err = rc.Flush()
if err != nil {
msg := fmt.Sprintf("flush fail, id = %v", id)
juneLog.ExceptionLog(err, msg)
return err
}
return nil
}
func UpdateArticle(id int, article *Article) error {
tx := juneDao.GetDB().Begin()
var err error
defer func() {
if err != nil {
msg := fmt.Sprintf("update article fail, id = %v, title = %v", id, article.Title)
juneLog.ExceptionLog(err, msg)
tx.Rollback()
}
tx.Commit()
}()
// 更新文章不更新创建时间
err = tx.Model(&Article{}).Omit("create_time").Where("id = ?", id).Updates(article).Error
if err != nil {
msg := fmt.Sprintf("Fail to update article table, articleID = %v", id)
juneLog.ExceptionLog(err, msg)
return err
}
if src.GetSetting().Others.Redis {
err = updateArticleWithCache(id, article)
if err != nil {
msg := fmt.Sprintf("Fail to update article cache, articleID = %v", id)
juneLog.ExceptionLog(err, msg)
return err
}
}
return nil
}
func deleteArticleFromDB(id int) error {
tx := juneDao.GetDB().Begin()
var err error
defer func() {
if err != nil {
msg := fmt.Sprintf("Fail to delete article from db, article id = %v", id)
juneLog.ExceptionLog(err, msg)
tx.Rollback()
}
tx.Commit()
}()
tags := make([]Tag, 0)
err = QueryAllTagsByArticleID(id, &tags)
if err != nil {
msg := fmt.Sprintf("Failed to query all tags of articles with id = %d", id)
juneLog.ExceptionLog(err, msg)
return err
}
// 删除 article 表中的数据
err = tx.Where("id = ?", id).Delete(&Article{}).Error
// 删除 article_tag 表中的数据
err = tx.Where("article_id = ?", id).Delete(&ArticleTags{}).Error
// 更新 tag 表中的 total
for _, tag := range tags {
err := lockedUpdateArticleTotal(tx, tag.ID, -1)
if err != nil {
return err
}
}
return err
}
func deleteArticleIDListCacheByID(id int) error {
rc := juneDao.GetRedisConn()
defer rc.Close()
_, err := rc.Do("LREM", consts.ArticleIDListCache, 0, id)
if err != nil {
msg := fmt.Sprintf("Fail to delete ArticleIDListCache by id, id = %v", id)
juneLog.ExceptionLog(err, msg)
return err
}
return nil
}
func deleteArticleInfoHashCacheByID(id int) error {
rc := juneDao.GetRedisConn()
defer rc.Close()
_, err := rc.Do("DEL", consts.ArticleInfoHashCache+strconv.Itoa(id))
if err != nil {
msg := fmt.Sprintf("Fail to delete ArticleInfoHashCache by id, id = %v", id)
juneLog.ExceptionLog(err, msg)
return err
}
return nil
}
func deleteArticleFromCache(id int) error {
// 修改 ArticleIDListCache
err := deleteArticleIDListCacheByID(id)
if err != nil {
return err
}
// 删除 ArticleInfoHashCache
err = deleteArticleInfoHashCacheByID(id)
if err != nil {
return err
}
// TODO: 修改 BitMap
return nil
}
func DeleteArticle(id int) error {
// 1. 从数据库中删除
err := deleteArticleFromDB(id)
if err != nil {
return err
}
// 2. 从 Redis 中删除
if src.GetSetting().Others.Redis {
err := deleteArticleFromCache(id)
if err != nil {
return err
}
}
return nil
}