租售同体的书屋项目——书籍系统(二)——第二部分(阅读量)

538 阅读3分钟

一、概述

书籍系统框架如图:

书籍系统.png

文件内容持续更新在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.结果展示

阅读量统计.png

三、Tips

最近工作中忙了起来,更新可能会比之前慢一些,请多多包涵。