一、概述
书籍系统框架如图:
文件内容持续更新在GitHub上,可自行查看。
本篇主要是介绍:评论和阅读量中的阅读量统计
二、阅读量
思路
1.通过网页的访问次数来决定;
2.因为次数变化频繁,考虑使用redis做递增,再在某一时间点更新到数据库;
3.防止恶意刷流量,需要有个拦截器,同一个ip在一段时间内不计入次数;
代码
1.拦截器(中间件): 在Expire时间段内不计入次数,使用redis分布式锁进行判定,符合要求的相应文章阅读量则加1
traffic_statistics_middleware.go
package Middlewares
import (
"WebApi/Svc"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"regexp"
)
var Expire = 10
func TrafficStatisticsMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
ip := c.ClientIP()
url := c.Request.URL
//redis 出错的情况下不记录阅读量并通知工作人员
if repeat, err := IsRepeat(ip + url.String()); err == nil {
if !repeat {
err = TrafficStatistics(url.String())
if err != nil {
fmt.Println(err)
}
}
} else {
fmt.Println(err)
}
c.Next()
}
}
//判斷訪問是否在指定時間內重複 true为重复,反之false
func IsRepeat(key string) (bool, error) {
ok, err := redis.Bool(Svc.SvcContext.Redis.Do("EXISTS", key))
if err != nil {
return false, err
}
if !ok {
_, err = Svc.SvcContext.Redis.Do("SET", key, []byte{}, "NX", "EX", Expire)
if err != nil {
return false, err
}
}
//重复
return ok, nil
}
//在redis记录访问量
func TrafficStatistics(key string) error {
re, err := regexp.Compile("[0-9]+") //解析出来哪本书哪个章节
if err != nil {
fmt.Println(err)
}
res := re.FindAll([]byte(key), -1)
key = "traffic_statistic"
if len(res) == 2 {
member := string(res[0]) + ":" + string(res[1])
_, err := Svc.SvcContext.Redis.Do("ZINCRBY", key, 1, member)
if err != nil {
return err
}
return nil
} else {
return errors.New("url不是正确的格式,无法用正则表达式匹配")
}
}
2.获取访问统计信息:所有访问量都从redis中获取。
ps: 这里我煞笔了,我一开始设想还统计某书某一章的阅读量,其实发现没啥大用,还多做了一层统计。--!
get_traffic_statistic_handler.go
package action
import (
"WebApi/Svc"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"net/http"
"strconv"
"strings"
)
func GetTrafficStatisticByBookIdAndChapterNumHandler(c *gin.Context) {
bookId := c.Query("bookId")
chapterNum := c.Query("chapterNum")
//找到redis访问量的内容
key := "traffic_statistic"
res, err := redis.String(Svc.SvcContext.Redis.Do("ZSCORE", key, bookId+":"+chapterNum))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
v, err := strconv.Atoi(res)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, v)
}
func GetAllTrafficStatisticHandler(c *gin.Context) {
//找到redis访问量的内容
key := "traffic_statistic"
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", key, 0, -1, "WITHSCORES"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, res)
}
func GetAllTrafficStatisticHandlerByBookId(c *gin.Context) {
//通过书籍ID找到redis访问量的内容
bookId := c.Query("bookId")
key := "traffic_statistic"
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", key, 0, -1, "WITHSCORES"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var count int64
for k, _ := range res {
if strings.Split(k, ":")[0] == bookId {
n, err := strconv.ParseInt(res[k], 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
count += n
}
}
c.JSON(http.StatusOK, count)
}
3.定时让redis和数据库交换数据:使用"github.com/robfig/cron"做定时任务.
cron.go
package Utils
import (
"WebApi/Pb/action"
"WebApi/Svc"
"context"
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/robfig/cron"
"strconv"
"strings"
)
func init() {
c := cron.New()
//凌晨5点更新DB中书籍的访问量
if err := c.AddFunc("0 0 5 * * ?", func() {
_ = TrafficStatisticsImportDB()
}); err != nil {
fmt.Println(err)
}
fmt.Println("cron start")
c.Start()
}
//定时在缓存和数据库之间进行访问量的同步
func TrafficStatisticsImportDB() error {
var err error
//找到所有redis访问量的内容
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", "traffic_statistic", 0, -1, "WITHSCORES"))
if err != nil {
return err
}
fmt.Println(res)
// Update to DB
var bookId, chapterNum, trafficNumber int64
for key, _ := range res {
bookId, err = strconv.ParseInt(strings.Split(key, ":")[0], 10, 64)
if err != nil {
return err
}
chapterNum, err = strconv.ParseInt(strings.Split(key, ":")[1], 10, 64)
if err != nil {
return err
}
trafficNumber, err = strconv.ParseInt(res[key], 10, 64)
if err != nil {
return err
}
rep, err := Svc.SvcContext.Grpc.ActionGrpc.GetTrafficStatisticByBookIdAndChapterNum(context.Background(),
&action.TrafficStatisticReq{
BookId: bookId,
ChapterNum: chapterNum})
fmt.Println(rep, err)
if err != nil {
if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" {
_, err := Svc.SvcContext.Grpc.ActionGrpc.CreateTrafficStatistic(context.Background(), &action.TrafficStatisticReq{
BookId: bookId,
ChapterNum: chapterNum,
TrafficNumber: trafficNumber,
})
if err != nil {
return err
}
} else {
return err
}
} else {
if trafficNumber > rep.TrafficNumber {
_, err := Svc.SvcContext.Grpc.ActionGrpc.UpdateTrafficStatistic(context.Background(), &action.TrafficStatisticReq{
Id: rep.Id,
BookId: bookId,
ChapterNum: chapterNum,
TrafficNumber: trafficNumber,
})
if err != nil {
return err
}
}
}
}
//DownLoad to Redis(防止Redis数据丢失)
if len(res) == 0 || res == nil {
resp, err := Svc.SvcContext.Grpc.ActionGrpc.GetAllTrafficStatistics(context.Background(), &action.Request{})
if err != nil {
return err
}
ts := resp.TrafficStatistics
key := "traffic_statistic"
for i, _ := range ts {
_, err = Svc.SvcContext.Redis.Do("ZADD", key, ts[i].TrafficNumber, strconv.FormatInt(ts[i].BookId, 10)+":"+strconv.FormatInt(ts[i].ChapterNum, 10))
if err != nil {
return err
}
}
}
return nil
}
4.结果展示
三、Tips
最近工作中忙了起来,更新可能会比之前慢一些,请多多包涵。