Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"retry_test.go",
"search_test.go",
"service_test.go",
"sort_test.go",
"stat_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/openplatform/article/model:go_default_library",
"//app/job/openplatform/article/conf:go_default_library",
"//app/job/openplatform/article/dao:go_default_library",
"//library/cache/redis:go_default_library",
"//library/queue/databus:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"article.go",
"author.go",
"cron.go",
"infoc.go",
"read.go",
"recommend.go",
"search.go",
"service.go",
"setting.go",
"sitemap.go",
"sort.go",
"stat.go",
"tag.go",
],
importpath = "go-common/app/job/openplatform/article/service",
tags = ["automanaged"],
deps = [
"//app/interface/main/tag/model:go_default_library",
"//app/interface/main/tag/rpc/client:go_default_library",
"//app/interface/openplatform/article/model:go_default_library",
"//app/interface/openplatform/article/rpc/client:go_default_library",
"//app/job/openplatform/article/conf:go_default_library",
"//app/job/openplatform/article/dao:go_default_library",
"//app/job/openplatform/article/model:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//library/conf/env:go_default_library",
"//library/log:go_default_library",
"//library/log/infoc:go_default_library",
"//library/queue/databus:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//vendor/github.com/jaytaylor/html2text:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,224 @@
package service
import (
"context"
"encoding/json"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/conf/env"
"go-common/library/log"
)
func (s *Service) upArticles(c context.Context, action string, newMsg []byte, oldMsg []byte) {
log.Info("s.upArticles action(%s) old(%s) new(%s)", action, string(oldMsg), string(newMsg))
if action != model.ActUpdate && action != model.ActInsert && action != model.ActDelete {
return
}
var (
err error
aid int64
newArticle = &model.Article{}
oldArticle = &model.Article{}
)
if err = json.Unmarshal(newMsg, newArticle); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", newMsg, err)
dao.PromError("article:解析过审文章databus新内容")
return
}
if action == model.ActUpdate {
if err = json.Unmarshal(oldMsg, oldArticle); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", oldMsg, err)
dao.PromError("article:解析过审文章databus旧内容")
return
}
}
aid = newArticle.ID
mid := newArticle.Mid
show := true
var comment string
if artmdl.NoDistributeAttr(newArticle.Attributes) {
show = false
comment = "禁止分发"
}
switch action {
case model.ActInsert:
s.openReply(c, aid, mid)
s.addArtCache(c, aid)
s.updateSearchArt(c, newArticle)
s.flowSync(c, aid, mid)
s.addURLNode(c, aid)
if comment == "" {
comment = "文章过审"
}
case model.ActUpdate:
s.updateArtCache(c, aid, oldArticle.CategoryID)
s.addURLNode(c, aid)
if !artmdl.NoDistributeAttr(oldArticle.Attributes) && artmdl.NoDistributeAttr(newArticle.Attributes) {
s.delSearchArt(c, aid)
} else {
s.updateSearchArt(c, newArticle)
}
if comment == "" {
comment = "文章修改"
}
case model.ActDelete:
s.closeReply(c, aid, mid)
s.deleteArtCache(c, aid, mid)
s.deleteArtRecommendCache(c, aid, newArticle.CategoryID)
s.delSearchArt(c, aid)
// s.delMediaScore(c, aid, newArticle.MediaID, mid)
show = false
comment = "文章不可见"
}
if e := s.dao.PubDynamic(c, mid, aid, show, comment, int64(newArticle.PublishTime), newArticle.DynamicIntro); e != nil {
s.dao.PushDynamicCache(c, &model.DynamicCacheRetry{
Aid: aid,
Mid: mid,
Show: show,
Comment: comment,
Ts: int64(newArticle.PublishTime),
DynamicIntro: newArticle.DynamicIntro,
})
}
if env.DeployEnv != env.DeployEnvProd {
return
}
s.gameSync(c, aid, mid, action)
urls := model.ReadURLs(aid)
for _, u := range urls {
if err = s.dao.PurgeCDN(c, u); err == nil {
log.Info("s.dao.PurgeCDN(%s) success.", u)
dao.PromInfo("article:刷新CDN")
}
}
}
func (s *Service) gameSync(c context.Context, aid, mid int64, action string) {
authors, err := s.dao.GameList(c)
if err != nil {
dao.PromError("service:获得游戏数据")
log.Error("s.gameSync(aid: %v, mid: %v) init err: %+v", aid, mid, err)
return
}
var exist bool
for _, author := range authors {
if author == mid {
exist = true
break
}
}
if !exist {
return
}
if err = s.dao.GameSync(c, action, aid); err != nil {
log.Error("s.gameSync(%d, %d, %s) error(%+v)", aid, mid, action, err)
dao.PromError("service:同步游戏数据")
s.dao.PushGameCache(c, &model.GameCacheRetry{
Action: action,
Aid: aid,
})
return
}
log.Info("s.gameSync(%d, %d, %s) success", aid, mid, action)
}
func (s *Service) flowSync(c context.Context, aid, mid int64) {
if err := s.dao.FlowSync(c, mid, aid); err != nil {
s.dao.PushFlowCache(c, &model.FlowCacheRetry{
Aid: aid,
Mid: mid,
})
return
}
log.Info("s.flowSync(aid: %d, mid: %d) success", aid, mid)
}
func (s *Service) openReply(c context.Context, aid, mid int64) (err error) {
if err = s.dao.OpenReply(c, aid, mid); err == nil {
log.Info("OpenReply(%d,%d) success.", aid, mid)
dao.PromInfo("article:打开评论区")
}
return
}
func (s *Service) closeReply(c context.Context, aid, mid int64) (err error) {
if err = s.dao.CloseReply(c, aid, mid); err == nil {
log.Info("CloseReply(%d,%d) success.", aid, mid)
dao.PromInfo("article:关闭评论区")
}
return
}
func (s *Service) addArtCache(c context.Context, aid int64) (err error) {
arg := &artmdl.ArgAid{Aid: aid}
if err = s.articleRPC.AddArticleCache(c, arg); err != nil {
log.Error("s.articleRPC.AddArticleCache(%d) error(%+v)", aid, err)
dao.PromError("article:新增文章缓存")
s.dao.PushArtCache(c, &dao.CacheRetry{
Action: dao.RetryAddArtCache,
Aid: aid,
})
} else {
log.Info("s.articleRPC.AddArticleCache(%d) success", aid)
dao.PromInfo("article:新增文章缓存")
}
return
}
func (s *Service) updateArtCache(c context.Context, aid, cid int64) (err error) {
arg := &artmdl.ArgAidCid{Aid: aid, Cid: cid}
if err = s.articleRPC.UpdateArticleCache(c, arg); err != nil {
log.Error("s.articleRPC.UpdateArticleCache(%d,%d) error(%+v)", aid, cid, err)
dao.PromError("article:更新文章缓存")
s.dao.PushArtCache(c, &dao.CacheRetry{
Action: dao.RetryUpdateArtCache,
Aid: aid,
Cid: cid,
})
} else {
log.Info("s.articleRPC.UpdateArticleCache(%d,%d) success", aid, cid)
dao.PromInfo("article:更新文章缓存")
}
return
}
func (s *Service) deleteArtCache(c context.Context, aid, mid int64) (err error) {
arg := &artmdl.ArgAidMid{Aid: aid, Mid: mid}
if err = s.articleRPC.DelArticleCache(c, arg); err != nil {
log.Error("s.articleRPC.DelArticleCache(%d,%d) error(%+v)", aid, mid, err)
dao.PromError("article:删除文章缓存")
s.dao.PushArtCache(c, &dao.CacheRetry{
Action: dao.RetryDeleteArtCache,
Aid: aid,
Mid: mid,
})
} else {
log.Info("s.articleRPC.DelArticleCache(%d,%d) success", aid, mid)
dao.PromInfo("article:删除文章缓存")
}
return
}
func (s *Service) deleteArtRecommendCache(c context.Context, aid, cid int64) (err error) {
arg := &artmdl.ArgAidCid{Aid: aid, Cid: cid}
if err = s.articleRPC.DelRecommendArtCache(c, arg); err != nil {
log.Error("s.articleRPC.DelRecommendArtCache(%d,%d) error(%+v)", aid, cid, err)
dao.PromError("article:删除文章推荐缓存")
s.dao.PushArtCache(c, &dao.CacheRetry{
Action: dao.RetryDeleteArtRecCache,
Aid: aid,
Cid: cid,
})
} else {
log.Info("s.articleRPC.DelRecommendArtCache(%d,%d) success", aid, cid)
dao.PromInfo("article:删除文章推荐缓存")
}
return
}
func (s *Service) delMediaScore(c context.Context, aid, mediaID, mid int64) (err error) {
err = s.dao.DelScore(c, aid, mediaID, mid)
return
}

View File

@@ -0,0 +1,38 @@
package service
import (
"context"
"encoding/json"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/log"
)
func (s *Service) upAuthors(c context.Context, action string, newMsg []byte, oldMsg []byte) {
log.Info("s.upAuthors action(%s) old(%s) new(%s)", action, string(oldMsg), string(newMsg))
var (
err error
newAuthor = &model.Author{}
)
if err = json.Unmarshal(newMsg, newAuthor); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", newMsg, err)
dao.PromError("article:解析作者表databus新内容")
return
}
s.updateAuthorCache(c, newAuthor.Mid)
}
// updateAuthorCache update author cache
func (s *Service) updateAuthorCache(c context.Context, mid int64) (err error) {
arg := &artmdl.ArgAuthor{Mid: mid}
if err = s.articleRPC.UpdateAuthorCache(c, arg); err != nil {
log.Error("s.articleRPC.UpdateAuthorCache(%+v) error(%+v)", arg, err)
dao.PromError("article:更新作者缓存")
return
}
log.Info("s.articleRPC.UpdateAuthorCache(%+v) success", arg)
dao.PromInfo("article:更新作者缓存")
return
}

View File

@@ -0,0 +1,21 @@
package service
import (
"context"
"time"
"go-common/app/job/openplatform/article/dao"
"go-common/library/log"
)
func (s *Service) updateSortproc() {
for {
if err := s.UpdateSort(context.TODO()); err != nil {
log.Error("s.UpdateSort error(%+v)", err)
dao.PromError("service:刷新分区投稿")
} else {
dao.PromInfo("service:刷新分区投稿")
}
time.Sleep(s.updateSortInterval)
}
}

View File

@@ -0,0 +1,57 @@
package service
import (
"go-common/library/log"
binfoc "go-common/library/log/infoc"
"go-common/library/stat/prom"
)
// 用户阅读专栏时长上报
type readInfo struct {
aid int64
mid int64
buvid string
ip string
duration int64
from string
}
// ReadInfoc .
func (s *Service) ReadInfoc(aid int64, mid int64, buvid string, ip string, duration int64, from string) {
s.infoc(readInfo{
aid: aid,
mid: mid,
buvid: buvid,
ip: ip,
duration: duration,
from: from,
})
}
func (s *Service) infoc(i interface{}) {
select {
case s.logCh <- i:
default:
log.Warn("infocproc chan full")
}
}
// writeInfoc
func (s *Service) infocproc() {
var (
readInfoc = binfoc.New(s.c.ReadInfoc)
)
for {
i, ok := <-s.logCh
if !ok {
log.Warn("infoc proc exit")
return
}
prom.BusinessInfoCount.State("infoc_channel", int64(len(s.logCh)))
switch l := i.(type) {
case readInfo:
readInfoc.Info(l.aid, l.mid, l.buvid, l.ip, l.duration, l.from)
log.Info("infocproc readInfoc param(%+v)", l)
}
}
}

View File

@@ -0,0 +1,49 @@
package service
import (
"context"
"time"
"go-common/library/log"
)
func (s *Service) checkReadStatus() {
for {
var c = context.TODO()
readSet, err := s.dao.ReadPingSet(c)
if err != nil || len(readSet) == 0 {
time.Sleep(5 * time.Second)
continue
}
now := time.Now().Unix()
for _, read := range readSet {
last, err := s.dao.ReadPing(c, read.Buvid, read.Aid)
if err != nil {
time.Sleep(time.Second)
continue
}
if now-last < 30 {
continue
}
if err = s.dao.DelReadPingSet(c, read); err != nil {
time.Sleep(time.Second)
continue
}
if last == 0 {
log.Error("阅读心跳没取到:buvid(%s) aid(%d)", read.Buvid, read.Aid)
continue
}
read.EndTime = last
log.Info("上传用户阅读记录至数据中心: %+v", read)
s.ReadInfoc(read.Aid, read.Mid, read.Buvid, read.IP, read.EndTime-read.StartTime, read.From)
}
return
}
}
func (s *Service) checkReadStatusProc() {
for {
s.checkReadStatus()
time.Sleep(time.Minute)
}
}

View File

@@ -0,0 +1,77 @@
package service
import (
"context"
"time"
)
func (s *Service) recommendAuthorproc() {
for {
s.calRecommendAuthor()
time.Sleep(time.Hour)
}
}
func (s *Service) calRecommendAuthor() {
var (
mids []int64
err error
m = make(map[int64][]int64)
)
for {
if mids, err = s.dao.MidsByPublishTime(context.TODO(), time.Now().Unix()-86400*s.c.Job.StatDays); err != nil {
time.Sleep(time.Minute)
continue
}
for _, mid := range mids {
if categoris, ok := s.statAuthorInfo(mid); ok {
for _, category := range categoris {
m[category] = append(m[category], mid)
}
s.dao.AddAuthorMostCategories(context.TODO(), mid, categoris)
}
}
s.dao.AddcategoriesAuthors(context.TODO(), m)
return
}
}
func (s *Service) statAuthorInfo(mid int64) (categories []int64, res bool) {
var (
m map[int64][2]int64
err error
words int64
cm = make(map[int64]int64)
max int64
)
if m, err = s.dao.StatByMid(context.TODO(), mid); err != nil {
return
}
for cid, data := range m {
if cate, ok := s.categoriesMap[cid]; ok {
if cate.ParentID != 0 {
cid = cate.ParentID
}
words += data[1]
if _, ok := cm[cid]; ok {
cm[cid] += data[0]
} else {
cm[cid] = data[0]
}
}
}
if words < s.c.Job.Words {
return
}
res = true
for k, v := range cm {
if v > max {
max = v
categories = []int64{k}
} else if v == max {
categories = append(categories, k)
}
}
return
}

View File

@@ -0,0 +1,64 @@
package service
import (
"context"
"encoding/json"
"fmt"
"testing"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/library/cache/redis"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Retry(t *testing.T) {
Convey("retry", t, WithoutProcService(func(s *Service) {
var (
err error
c = context.TODO()
)
SkipConvey("push retry", func() {
var (
favCnt = int64(1)
replyCnt = int64(2)
statRetry = &dao.StatRetry{
Data: &artmdl.StatMsg{
Aid: 888,
Favorite: &favCnt,
Reply: &replyCnt,
},
Action: dao.RetryUpdateStatCache,
Count: 3,
}
)
err = s.dao.PushStat(c, statRetry)
So(err, ShouldBeNil)
})
SkipConvey("pop retry", func() {
bs, err := s.dao.PopStat(c)
if err != redis.ErrNil {
So(err, ShouldBeNil)
}
So(bs, ShouldNotBeEmpty)
msg := &dao.StatRetry{}
err = json.Unmarshal(bs, msg)
So(err, ShouldBeNil)
So(msg.Action, ShouldEqual, dao.RetryUpdateStatCache)
So(msg.Count, ShouldEqual, 3)
})
Convey("retry reply", func() {
err := s.dao.PushReply(context.TODO(), 8, 9)
So(err, ShouldBeNil)
aid, mid, err := s.dao.PopReply(c)
So(err, ShouldBeNil)
fmt.Println(aid)
fmt.Println(mid)
})
}))
}

View File

@@ -0,0 +1,92 @@
package service
import (
"context"
"strings"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/log"
"go-common/library/sync/errgroup"
"github.com/jaytaylor/html2text"
)
func (s *Service) updateSearchArt(c context.Context, art *model.Article) (err error) {
if art == nil {
return
}
a := artmdl.Meta{Attributes: art.Attributes}
if a.AttrVal(artmdl.AttrBitNoDistribute) {
return
}
searchArt := &model.SearchArticle{Article: *art}
var (
group *errgroup.Group
errCtx context.Context
)
group, errCtx = errgroup.WithContext(c)
group.Go(func() (err error) {
if searchArt.Content, err = s.dao.ArticleContent(errCtx, art.ID); err != nil {
return
}
searchArt.Content, err = extractText(searchArt.Content)
return
})
group.Go(func() (err error) {
var stat *artmdl.StatMsg
if stat, err = s.dao.Stat(errCtx, art.ID); (err != nil) || (stat == nil) {
return
}
searchArt.StatsDisLike = *stat.Dislike
searchArt.StatsFavorite = *stat.Favorite
searchArt.StatsLikes = *stat.Like
searchArt.StatsReply = *stat.Reply
searchArt.StatsShare = *stat.Share
searchArt.StatsView = *stat.View
searchArt.StatsCoin = *stat.Coin
return
})
group.Go(func() (err error) {
searchArt.Tags, err = s.tags(errCtx, art.ID)
return
})
group.Go(func() (err error) {
searchArt.Keywords, err = s.dao.Keywords(errCtx, art.ID)
return
})
if err = group.Wait(); err == nil {
err = s.dao.UpdateSearch(c, searchArt)
}
if err != nil {
dao.PromError("search:更新文章")
log.Error("updateSearchArt(%+v) err: %+v", searchArt, err)
return
}
log.Info("success updateSearchArt(id: %v, title: %v)", searchArt.ID, searchArt.Title)
return
}
func (s *Service) delSearchArt(c context.Context, aid int64) (err error) {
err = s.dao.DelSearch(c, aid)
return
}
func extractText(html string) (res string, err error) {
ops := html2text.Options{PrettyTables: false}
res, err = html2text.FromString(html, ops)
res = strings.Replace(res, "*", "", -1)
return
}
func (s *Service) updateSearchStats(c context.Context, stat *artmdl.StatMsg) (err error) {
if err = s.dao.UpdateSearchStats(c, stat); err != nil {
log.Error("updateSearchStats(%v) err: %+v", stat.String(), err)
return
}
log.Info("updateSearchStats(%v) success", stat.String())
return
}

View File

@@ -0,0 +1,19 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Search(t *testing.T) {
Convey("work", t, func() {
a := `
<div><em><br>AlphaGO战胜柯洁、Google翻译准确度大幅提升、自动驾驶技术日趋成熟这些无一不在告诉我们人工智能技术正飞速发展。那么如果让人工智能来玩STG会怎么样呢<br></em><br></div><div><a href="http://www.bilibili.com/video/av12432">视频链接</a><br>STG也称为弹幕游戏。往往由于满屏幕都是子弹而使一部分玩家望而却步。长久以来初学者觉得游戏难度无法想象简直反人类而高级玩家却觉得还不够难不够过瘾。这对矛盾已经成为了影响STG发展的主要矛盾。<br>av12679</div><div>av23412<br>现今大多数STG都会为了解决或缓解这一矛盾而采取了一些措施比如大多数游戏会设置多个难度使初学者玩简单难度而高手可以玩疯狂难度有的游戏会设置教学关卡指导初学者避弹有一些游戏设置了自动雷击功能来为初学者降低难度。<br><br></div><div><strong><br>然而就在最近,我们在游戏《弹幕音乐绘》中,提出了一种更加别出心裁的解决方案——自动避弹。<br></strong><em><br>AlphaGO战胜柯洁、Google翻译准确度大幅提升、自动驾驶技术日趋成熟这些无一不在告诉我们人工智能技术正飞速发展。那么如果让人工智能来玩STG会怎么样呢<br></em><br></div><div><br>STG也称为弹幕游戏。往往由于满屏幕都是子弹而使一部分玩家望而却步。长久以来初学者觉得游戏难度无法想象简直反人类而高级玩家却觉得还不够难不够过瘾。这对矛盾已经成为了影响STG发展的主要矛盾。<br><br></div><div><br>现今大多数STG都会为了解决或缓解这一矛盾而采取了一些措施比如大多数游戏会设置多个难度使初学者玩简单难度而高手可以玩疯狂难度有的游戏会设置教学关卡指导初学者避弹有一些游戏设置了自动雷击功能来为初学者降低难度。<br><br></div><div><strong><br>然而就在最近,我们在游戏《弹幕音乐绘》中,提出了一种更加别出心裁的解决方案——自动避弹。<br></strong><br></div>
`
res, err := extractText(a)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
// fmt.Println(res)
})
}

View File

@@ -0,0 +1,616 @@
package service
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
tagrpc "go-common/app/interface/main/tag/rpc/client"
artmdl "go-common/app/interface/openplatform/article/model"
artrpc "go-common/app/interface/openplatform/article/rpc/client"
"go-common/app/job/openplatform/article/conf"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
thumdl "go-common/app/service/main/thumbup/model"
"go-common/library/conf/env"
"go-common/library/log"
"go-common/library/log/infoc"
"go-common/library/queue/databus"
"go-common/library/stat/prom"
)
const (
_sharding = 100 // goroutines for dealing the stat
_chanSize = 10240
_articleTable = "filtered_articles" // article table name
_authorTable = "article_authors" // 作者表
)
type lastTimeStat struct {
time int64
stat *artmdl.StatMsg
}
// Service .
type Service struct {
c *conf.Config
dao *dao.Dao
waiter *sync.WaitGroup
closed bool
// monitor *monitor.Service
articleSub *databus.Databus
articleStatSub *databus.Databus
likeStatSub *databus.Databus
replyStatSub *databus.Databus
favoriteStatSub *databus.Databus
coinStatSub *databus.Databus
articleRPC *artrpc.Service
tagRPC *tagrpc.Service
statCh [_sharding]chan *artmdl.StatMsg
statLastTime [_sharding]map[int64]*lastTimeStat
categoriesMap map[int64]*artmdl.Category
categoriesReverseMap map[int64][]*artmdl.Category
sitemapMap map[int64]struct{}
urlListHead *urlNode
urlListTail *urlNode
lastURLNode *urlNode
sitemapXML string
likeCh chan *thumdl.StatMsg
updateDbInterval int64
updateSortInterval time.Duration
cheatInfoc *infoc.Infoc
// *Cnt means sum of consumed messages.
statCnt, binCnt int64
cheatArts map[int64]int
sortLimitTime int64
setting *model.Setting
// infoc
logCh chan interface{}
}
// New creates a Service instance.
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
waiter: new(sync.WaitGroup),
// monitor: monitor.New(),
articleSub: databus.New(c.ArticleSub),
articleStatSub: databus.New(c.ArticleStatSub),
replyStatSub: databus.New(c.ReplyStatSub),
favoriteStatSub: databus.New(c.FavoriteStatSub),
coinStatSub: databus.New(c.CoinStatSub),
likeStatSub: databus.New(c.LikeStatSub),
articleRPC: artrpc.New(c.ArticleRPC),
tagRPC: tagrpc.New2(c.TagRPC),
updateDbInterval: int64(time.Duration(c.Job.UpdateDbInterval) / time.Second),
updateSortInterval: time.Duration(c.Job.UpdateSortInterval),
likeCh: make(chan *thumdl.StatMsg, 1e4),
cheatInfoc: infoc.New(c.CheatInfoc),
categoriesMap: make(map[int64]*artmdl.Category),
categoriesReverseMap: make(map[int64][]*artmdl.Category),
sitemapMap: make(map[int64]struct{}),
sortLimitTime: int64(time.Duration(c.Job.SortLimitTime) / time.Second),
logCh: make(chan interface{}, 1024),
}
// s.monitor.SetConfig(c.HTTPClient)
for i := int64(0); i < _sharding; i++ {
i := i
// for stat
s.statCh[i] = make(chan *artmdl.StatMsg, _chanSize)
s.statLastTime[i] = make(map[int64]*lastTimeStat)
s.waiter.Add(1)
go s.statproc(i)
}
s.loadCategories()
s.loadSettings()
// 10 go routines with WaitGroup
s.waiter.Add(10)
go s.consumeStat()
go s.consumeCanal()
go s.checkConsumer()
go s.retryStat()
go s.retryReply()
go s.retryGame()
go s.retryFlow()
go s.retryDynamic()
go s.retryCDN()
go s.retryCache()
// other go routines
go s.updateSortproc()
go s.activityLikeproc()
go s.updateReadCountproc()
go s.updateHotspotsproc()
go s.updateCheatArtsproc()
go s.loadCategoriesproc()
go s.loadSettingsproc()
go s.recommendAuthorproc()
go s.checkReadStatusProc()
go s.infocproc()
go s.sitemapproc()
return
}
// consumeStat consumes article's stat.
func (s *Service) consumeStat() {
defer s.waiter.Done()
for {
if s.closed {
for i := 0; i < _sharding; i++ {
close(s.statCh[i])
}
log.Info("databus: article-job stat consumer exit!")
return
}
L:
select {
case msg, ok := <-s.articleStatSub.Messages():
if !ok {
break L
}
s.statCnt++
msg.Commit()
sm := &artmdl.StatMsg{}
if err := json.Unmarshal(msg.Value, sm); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err)
dao.PromError("service:解析计数databus消息")
break L
}
if sm.Aid <= 0 {
log.Warn("aid(%d) <=0 message(%s)", sm.Aid, msg.Value)
break L
}
key := sm.Aid % _sharding
s.statCh[key] <- sm
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key])))
log.Info("consumeStat key:%s partition:%d offset:%d msg: %v)", msg.Key, msg.Partition, msg.Offset, sm.String())
case msg, ok := <-s.likeStatSub.Messages():
if !ok {
break L
}
msg.Commit()
like := &thumdl.StatMsg{}
if err := json.Unmarshal(msg.Value, like); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err)
dao.PromError("service:解析like计数databus消息")
break L
}
if like.Type != "article" {
continue
}
select {
case s.likeCh <- like:
default:
}
key := like.ID % _sharding
s.statCh[key] <- &artmdl.StatMsg{Like: &like.Count, Dislike: &like.DislikeCount, Aid: like.ID}
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key])))
log.Info("consumeLikeStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, like)
case msg, ok := <-s.replyStatSub.Messages():
if !ok {
break L
}
msg.Commit()
m := &thumdl.StatMsg{}
if err := json.Unmarshal(msg.Value, m); err != nil {
log.Error("reply json.Unmarshal(%s) error(%+v)", msg.Value, err)
dao.PromError("service:解析reply计数databus消息")
break L
}
if m.Type != "article" {
continue
}
key := m.ID % _sharding
s.statCh[key] <- &artmdl.StatMsg{Reply: &m.Count, Aid: m.ID}
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key])))
log.Info("consumeReplyStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m)
case msg, ok := <-s.favoriteStatSub.Messages():
if !ok {
break L
}
msg.Commit()
m := &thumdl.StatMsg{}
if err := json.Unmarshal(msg.Value, m); err != nil {
log.Error("favorite json.Unmarshal(%s) error(%+v)", msg.Value, err)
dao.PromError("service:解析favorite计数databus消息")
break L
}
if m.Type != "article" {
continue
}
key := m.ID % _sharding
s.statCh[key] <- &artmdl.StatMsg{Favorite: &m.Count, Aid: m.ID}
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key])))
log.Info("consumeFavoriteStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m)
case msg, ok := <-s.coinStatSub.Messages():
if !ok {
break L
}
msg.Commit()
m := &thumdl.StatMsg{}
if err := json.Unmarshal(msg.Value, m); err != nil {
log.Error("coin json.Unmarshal(%s) error(%+v)", msg.Value, err)
dao.PromError("service:解析coin计数databus消息")
break L
}
if m.Type != "article" {
continue
}
key := m.ID % _sharding
s.statCh[key] <- &artmdl.StatMsg{Coin: &m.Count, Aid: m.ID}
prom.BusinessInfoCount.State(fmt.Sprintf("statChan-%v", key), int64(len(s.statCh[key])))
log.Info("consumeCoinStat key:%s partition:%d offset:%d msg: %+v)", msg.Key, msg.Partition, msg.Offset, m)
}
}
}
// consumeCanal consumes article's binlog databus.
func (s *Service) consumeCanal() {
defer s.waiter.Done()
var c = context.TODO()
for {
msg, ok := <-s.articleSub.Messages()
if !ok {
log.Info("databus: article-job binlog consumer exit!")
return
}
s.binCnt++
msg.Commit()
m := &model.Message{}
if err := json.Unmarshal(msg.Value, m); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", msg.Value, err)
continue
}
switch m.Table {
case _articleTable:
s.upArticles(c, m.Action, m.New, m.Old)
case _authorTable:
s.upAuthors(c, m.Action, m.New, m.Old)
}
log.Info("consumeCanal key:%s partition:%d offset:%d", msg.Key, msg.Partition, msg.Offset)
}
}
func (s *Service) retryStat() {
defer s.waiter.Done()
var (
err error
bs []byte
c = context.TODO()
)
for {
if s.closed {
return
}
bs, err = s.dao.PopStat(c)
if err != nil || bs == nil {
time.Sleep(time.Second)
continue
}
msg := &dao.StatRetry{}
if err = json.Unmarshal(bs, msg); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", bs, err)
dao.PromError("service:解析计数重试消息")
continue
}
if msg.Count > dao.RetryStatCount {
continue
}
log.Info("retry: %s", bs)
switch msg.Action {
case dao.RetryUpdateStatCache:
if err = s.updateCache(c, msg.Data, msg.Count+1); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试计数更新缓存")
case dao.RetryUpdateStatDB:
if err = s.updateDB(c, msg.Data, msg.Count+1); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试计数更新DB")
}
}
}
func (s *Service) retryReply() {
defer s.waiter.Done()
var (
err error
aid, mid int64
c = context.TODO()
)
for {
if s.closed {
return
}
aid, mid, err = s.dao.PopReply(c)
if err != nil || aid == 0 || mid == 0 {
time.Sleep(time.Second)
continue
}
log.Info("retry reply: aid(%d) mid(%d)", aid, mid)
if err = s.openReply(c, aid, mid); err != nil {
log.Error("s.openReply(%d,%d) error(%+v)", aid, mid, err)
dao.PromInfo("service:重试打开评论区")
time.Sleep(100 * time.Millisecond)
continue
}
log.Info("s.openReply(%d,%d) retry success", aid, mid)
}
}
func (s *Service) retryCDN() {
defer s.waiter.Done()
var (
err error
file string
c = context.TODO()
)
for {
if s.closed {
return
}
file, err = s.dao.PopCDN(c)
if err != nil || file == "" {
time.Sleep(time.Second)
continue
}
log.Info("retry CDN: file(%s)", file)
if err = s.dao.PurgeCDN(c, file); err != nil {
log.Error("s.dao.PurgeCDN(%s) error(%+v)", file, err)
dao.PromInfo("service:刷新CDN重试")
time.Sleep(100 * time.Millisecond)
continue
}
log.Info("s.dao.PurgeCDN(%s) retry success.", file)
}
}
func (s *Service) retryCache() {
defer s.waiter.Done()
var (
err error
bs []byte
c = context.TODO()
)
for {
if s.closed {
return
}
bs, err = s.dao.PopArtCache(c)
if err != nil || bs == nil {
time.Sleep(time.Second)
continue
}
msg := &dao.CacheRetry{}
if err = json.Unmarshal(bs, msg); err != nil {
log.Error("json.Unmarshal(%s) error(%+v)", bs, err)
dao.PromError("service:解析文章缓存重试消息")
continue
}
log.Info("retry cache: %s", bs)
switch msg.Action {
case dao.RetryAddArtCache:
if err = s.addArtCache(c, msg.Aid); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试添加文章缓存")
case dao.RetryUpdateArtCache:
if err = s.updateArtCache(c, msg.Aid, msg.Cid); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试更新文章缓存")
case dao.RetryDeleteArtCache:
if err = s.deleteArtCache(c, msg.Aid, msg.Mid); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试删除文章缓存")
case dao.RetryDeleteArtRecCache:
if err = s.deleteArtRecommendCache(c, msg.Aid, msg.Cid); err != nil {
time.Sleep(100 * time.Millisecond)
}
dao.PromInfo("service:重试删除文章推荐缓存")
}
}
}
// checkConsumer checks consumer state.
func (s *Service) checkConsumer() {
defer s.waiter.Done()
if env.DeployEnv != env.DeployEnvProd {
return
}
var (
// ctx = context.TODO()
// c* means sum of consumed messages.
c1, c2 int64
)
for {
time.Sleep(5 * time.Hour)
if s.statCnt-c1 == 0 {
// msg := "databus: article-job stat did not consume within a minute"
// s.monitor.Sms(ctx, s.c.SMS.Phone, s.c.SMS.Token, msg)
// log.Warn(msg)
log.Warn("databus: article-job stat did not consume within a minute")
}
c1 = s.statCnt
if s.binCnt-c2 == 0 {
// msg := "databus: article-job binlog did not consume within a minute"
// s.monitor.Sms(ctx, s.c.SMS.Phone, s.c.SMS.Token, msg)
// log.Warn(msg)
log.Warn("databus: article-job binlog did not consume within a minute")
}
c2 = s.binCnt
}
}
// Ping reports the heath of services.
func (s *Service) Ping(c context.Context) (err error) {
return s.dao.Ping(c)
}
// Close releases resources which owned by the Service instance.
func (s *Service) Close() (err error) {
defer s.waiter.Wait()
s.articleSub.Close()
s.articleStatSub.Close()
s.closed = true
log.Info("article-job has been closed.")
return
}
func (s *Service) retryGame() {
defer s.waiter.Done()
var (
err error
info *model.GameCacheRetry
c = context.TODO()
)
for {
if s.closed {
return
}
info, err = s.dao.PopGameCache(c)
if (err != nil) || (info == nil) {
time.Sleep(time.Second)
continue
}
for {
log.Info("retry_game: %+v", info)
dao.PromInfo("service:重试同步游戏")
if err = s.dao.GameSync(c, info.Action, info.Aid); err != nil {
time.Sleep(time.Millisecond * 100)
continue
}
break
}
log.Info("s.GameSync(%s, aid: %d) retry success", info.Action, info.Aid)
}
}
func (s *Service) activityLikeproc() {
var c = context.TODO()
for {
msg, ok := <-s.likeCh
if !ok {
log.Info("activityLikeproc: exit!")
return
}
s.dao.LikeSync(c, msg.ID, msg.Count)
}
}
func (s *Service) retryFlow() {
defer s.waiter.Done()
var (
err error
info *model.FlowCacheRetry
c = context.TODO()
)
for {
if s.closed {
return
}
info, err = s.dao.PopFlowCache(c)
if (err != nil) || (info == nil) {
time.Sleep(time.Second)
continue
}
for {
log.Info("retry_flow: %+v", info)
dao.PromInfo("service:重试同步flow")
if err = s.dao.FlowSync(c, info.Mid, info.Aid); err != nil {
time.Sleep(time.Second * 1)
continue
}
break
}
log.Info("s.FlowSync(mid:%v, aid: %d) retry success", info.Mid, info.Aid)
}
}
func (s *Service) retryDynamic() {
defer s.waiter.Done()
var (
err error
info *model.DynamicCacheRetry
c = context.TODO()
)
for {
if s.closed {
return
}
info, err = s.dao.PopDynamicCache(c)
if (err != nil) || (info == nil) {
time.Sleep(time.Second)
continue
}
for {
log.Info("retry_dynamic: %+v", info)
dao.PromInfo("service:重试同步dynamic")
if err = s.dao.PubDynamic(c, info.Mid, info.Aid, info.Show, info.Comment, info.Ts, info.DynamicIntro); err != nil {
time.Sleep(time.Second * 1)
continue
}
break
}
log.Info("s.PubDynamic(mid:%v, aid: %d) retry success %+v", info.Mid, info.Aid, info)
}
}
func (s *Service) updateReadCountproc() {
var c = context.TODO()
for {
err := s.articleRPC.RebuildAllListReadCount(c)
if err != nil {
log.Error("s.updateReadCountproc() err: %+v", err)
time.Sleep(time.Second * 5)
continue
}
log.Info("s.updateReadCountproc() success")
dao.PromError("更新文集阅读数")
time.Sleep(time.Duration(s.c.Job.ListReadCountInterval))
}
}
func (s *Service) updateHotspotsproc() {
var c = context.TODO()
var lastUpdate int64
for {
var force bool
duration := int64(time.Duration(s.c.Job.HotspotForceInterval) / time.Second)
if (time.Now().Unix() - lastUpdate) > duration {
force = true
}
err := s.articleRPC.UpdateHotspots(c, &artmdl.ArgForce{Force: force})
if err != nil {
log.Error("s.UpdateHotspots() err: %+v", err)
dao.PromError("更新热点运营文章")
time.Sleep(time.Second * 5)
continue
}
if force {
lastUpdate = time.Now().Unix()
}
log.Info("s.UpdateHotspots() success force:%v", force)
time.Sleep(time.Duration(s.c.Job.HotspotInterval))
}
}
func (s *Service) updateCheatArtsproc() {
var c = context.TODO()
for {
arts, err := s.dao.CheatArts(c)
if err != nil {
log.Error("s.updateCheatArtsproc() err: %+v", err)
dao.PromError("更新反作弊文章列表")
time.Sleep(time.Second * 5)
continue
}
log.Info("s.updateCheatArtsproc() success, len: %v", len(arts))
s.cheatArts = arts
time.Sleep(time.Minute)
}
}

View File

@@ -0,0 +1,41 @@
package service
import (
"flag"
"path/filepath"
"sync"
"time"
"go-common/app/job/openplatform/article/conf"
"go-common/app/job/openplatform/article/dao"
"go-common/library/queue/databus"
)
// func WithService(f func(s *Service)) func() {
// return func() {
// dir, _ := filepath.Abs("../goconvey.toml")
// flag.Set("conf", dir)
// conf.Init()
// s := New(conf.Conf)
// // s.dao = dao.New(conf.Conf)
// f(s)
// }
// }
func WithoutProcService(f func(s *Service)) func() {
return func() {
dir, _ := filepath.Abs("../goconvey.toml")
flag.Set("conf", dir)
conf.Init()
s := &Service{
c: conf.Conf,
dao: dao.New(conf.Conf),
waiter: new(sync.WaitGroup),
// articleRPC: artrpc.New(conf.Conf.ArticleRPC),
articleSub: databus.New(conf.Conf.ArticleSub),
articleStatSub: databus.New(conf.Conf.ArticleStatSub),
updateDbInterval: int64(time.Duration(conf.Conf.Job.UpdateDbInterval) / time.Second),
}
f(s)
}
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"encoding/json"
"time"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/log"
)
func (s *Service) loadSettings() {
for {
settings, err := s.dao.Settings(context.TODO())
if err != nil || len(settings) == 0 {
dao.PromError("service:获取配置")
time.Sleep(time.Second)
continue
}
if s.setting == nil {
s.setting = &model.Setting{}
}
for name, value := range settings {
switch name {
case "recheck_view":
var recheckView = &model.Recheck{}
if err = json.Unmarshal([]byte(value), recheckView); err != nil {
log.Error("setting.Unmarshal(%s) error(%+v)", value, err)
dao.PromError("service:配置项无效")
} else {
s.setting.Recheck = recheckView
}
}
}
return
}
}
func (s *Service) loadSettingsproc() {
for {
time.Sleep(time.Minute)
s.loadSettings()
}
}

View File

@@ -0,0 +1,153 @@
package service
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync"
"time"
"go-common/library/log"
)
type urlNode struct {
id int64
next *urlNode
}
const (
_sitemapSize = 20000
_sitemapInterval = int64(60)
)
func (s *Service) sitemapproc() {
if s.urlListHead == nil {
s.initURLList(context.TODO())
}
var interval = s.c.Sitemap.Interval
if interval == 0 {
interval = _sitemapInterval
}
for {
if s.lastURLNode == nil || s.lastURLNode != s.urlListHead {
s.updateXML()
}
time.Sleep(time.Second * time.Duration(interval))
}
}
// add new url.
func (s *Service) addURLNode(c context.Context, id int64) {
go s.pushShenma(c, id)
if s.urlListHead == nil {
s.initURLList(c)
}
if _, ok := s.sitemapMap[id]; ok {
return
}
var (
mutex sync.Mutex
empty struct{}
)
mutex.Lock()
node := &urlNode{id: id}
s.urlListTail.next = node
s.urlListTail = node
oldID := s.urlListHead.id
delete(s.sitemapMap, oldID)
s.sitemapMap[id] = empty
s.urlListHead = s.urlListHead.next
mutex.Unlock()
}
func (s *Service) initURLList(c context.Context) {
var (
ids []int64
err error
size = s.c.Sitemap.Size
head, tail *urlNode
empty struct{}
mutex sync.Mutex
)
if size == 0 {
size = _sitemapSize
}
if ids, err = s.dao.LastModIDs(c, size); err != nil {
log.Error("s.initURLList error(%+v) size(%d)", err, size)
return
}
for i := len(ids); i > 0; i-- {
id := ids[i-1]
node := &urlNode{id: id}
if head == nil {
head = node
tail = node
s.sitemapMap[id] = empty
continue
}
tail.next = node
tail = node
s.sitemapMap[id] = empty
}
if head != nil && tail != nil {
mutex.Lock()
s.urlListHead = head
s.urlListTail = tail
mutex.Unlock()
}
}
func (s *Service) updateXML() {
var (
node = s.urlListHead
xml = ""
mutex sync.Mutex
)
for node != nil {
xml = `<url><loc>http://www.bilibili.com/read/cv` + strconv.FormatInt(node.id, 10) + `/</loc><lastmod>` + time.Now().Format("2006-01-02") + `</lastmod><changefreq>always</changefreq></url>` + xml
node = node.next
}
xml = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.google.com/schemas/sitemap/0.84">` + xml + `</urlset>`
mutex.Lock()
s.sitemapXML = xml
s.lastURLNode = s.urlListHead
mutex.Unlock()
}
// SitemapXML .
func (s *Service) SitemapXML(c context.Context) (string, string) {
return s.sitemapXML, strconv.Itoa(len(s.sitemapXML))
}
func (s *Service) pushShenma(c context.Context, id int64) {
url := `http://data.zhanzhang.sm.cn/push?site=www.bilibili.com&user_name=zjdxgxy@163.com&resource_name=mip_add&token=TI_a1e0216ad1a64b63e9bbadfc7579f131`
urlNode := "www.bilibili.com/read/mobile/" + strconv.FormatInt(id, 10)
resp, err := http.Post(url, "text/plain", strings.NewReader(urlNode))
if err != nil {
log.Error("s.pushShenma post error(%+v)", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("s.pushShenma ReadAll error(%+v)", err)
return
}
var data struct {
ReturnCode int `json:"returnCode"`
ErrMsg string `json:"errMsg"`
}
if err = json.Unmarshal(body, &data); err != nil {
log.Error("s.pushShenma json unmarshal error(%+v)", err)
return
}
if data.ReturnCode == 200 {
log.Info("s.pushShenma success, url(%s)", urlNode)
} else {
log.Error("s.pushShenma success, errMsg(%s), url(%s)", data.ErrMsg, urlNode)
}
return
}

View File

@@ -0,0 +1,182 @@
package service
import (
"context"
"sort"
"time"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/log"
)
var (
_recommendCategory = int64(0)
)
// UpdateSort update sort
func (s *Service) UpdateSort(c context.Context) (err error) {
ids := []int64{_recommendCategory}
for id := range s.categoriesMap {
ids = append(ids, id)
}
var (
cache bool
allArts map[int64]map[int][][2]int64
)
for _, categoryID := range ids {
for _, field := range artmdl.SortFields {
if (categoryID == _recommendCategory) && (field != artmdl.FieldNew) {
continue
}
if cache, err = s.dao.ExpireSortCache(c, categoryID, field); err != nil {
dao.PromError("recommends:更新文章排序")
return
}
// cache不存在才更新
if cache {
continue
}
dao.PromInfo("sort:初始化排序缓存")
if allArts == nil {
allArts, _ = s.loadSortArts(context.TODO())
}
if field == artmdl.FieldNew {
s.updateNewArts(c, categoryID)
} else {
s.updateStatSort(c, allArts, categoryID, field)
}
}
}
return
}
func (s *Service) updateNewArts(c context.Context, categoryID int64) (err error) {
var arts [][2]int64
if categoryID == _recommendCategory {
if arts, err = s.dao.NewestArtIDs(c, s.c.Job.MaxNewArtsNum); err == nil {
log.Info("s.updateNewArts() len: %v", len(arts))
// 不异步
err = s.dao.AddSortCaches(c, _recommendCategory, artmdl.FieldNew, arts, s.c.Job.MaxNewArtsNum)
}
return
}
ids := []int64{}
cs := s.categoriesReverseMap[categoryID]
if len(cs) == 0 {
// 二级分区
ids = append(ids, categoryID)
} else {
// 一级分区聚合子分区
for _, s := range cs {
ids = append(ids, s.ID)
}
}
if arts, err = s.dao.NewestArtIDByCategory(c, ids, s.c.Job.MaxNewArtsNum); err == nil {
// 不异步
err = s.dao.AddSortCaches(c, categoryID, artmdl.FieldNew, arts, s.c.Job.MaxNewArtsNum)
}
return
}
func (s *Service) updateStatSort(c context.Context, allArts map[int64]map[int][][2]int64, categoryID int64, field int) (err error) {
if allArts == nil {
return
}
if categoryID == _recommendCategory {
//推荐下没有排序
return
}
arts := trimArts(allArts[categoryID][field], int(s.c.Job.MaxSortArtsNum))
err = s.dao.AddSortCaches(c, categoryID, field, arts, s.c.Job.MaxSortArtsNum)
return
}
func trimArts(arts [][2]int64, max int) (res [][2]int64) {
sort.Slice(arts, func(i, j int) bool {
return arts[i][1] > arts[j][1]
})
if len(arts) > max {
return arts[:max]
}
return arts
}
func (s *Service) loadSortArts(c context.Context) (res map[int64]map[int][][2]int64, err error) {
// init category and field
res = make(map[int64]map[int][][2]int64)
for id := range s.categoriesMap {
res[id] = make(map[int][][2]int64)
}
var (
arts []*model.SearchArticle
limitTime = time.Now().Unix() - s.sortLimitTime
)
if arts, err = s.dao.SearchArts(c, limitTime); err != nil {
dao.PromError("sort:初始化排序计数失败")
return
}
for _, art := range arts {
if artmdl.NoDistributeAttr(art.Attributes) || artmdl.NoRegionAttr(art.Attributes) {
continue
}
if res[art.CategoryID] == nil {
res[art.CategoryID] = make(map[int][][2]int64)
}
res[art.CategoryID][artmdl.FieldFav] = append(res[art.CategoryID][artmdl.FieldFav], [2]int64{art.ID, art.StatsFavorite})
res[art.CategoryID][artmdl.FieldLike] = append(res[art.CategoryID][artmdl.FieldLike], [2]int64{art.ID, art.StatsLikes})
res[art.CategoryID][artmdl.FieldReply] = append(res[art.CategoryID][artmdl.FieldReply], [2]int64{art.ID, art.StatsReply})
res[art.CategoryID][artmdl.FieldView] = append(res[art.CategoryID][artmdl.FieldView], [2]int64{art.ID, art.StatsView})
var parentID int64
if id, ok := s.categoriesMap[art.CategoryID]; ok {
parentID = id.ParentID
} else {
continue
}
if res[parentID] == nil {
res[parentID] = make(map[int][][2]int64)
}
res[parentID][artmdl.FieldFav] = append(res[parentID][artmdl.FieldFav], [2]int64{art.ID, art.StatsFavorite})
res[parentID][artmdl.FieldLike] = append(res[parentID][artmdl.FieldLike], [2]int64{art.ID, art.StatsLikes})
res[parentID][artmdl.FieldReply] = append(res[parentID][artmdl.FieldReply], [2]int64{art.ID, art.StatsReply})
res[parentID][artmdl.FieldView] = append(res[parentID][artmdl.FieldView], [2]int64{art.ID, art.StatsView})
}
return
}
func (s *Service) loadCategoriesproc() {
for {
time.Sleep(time.Minute * 10)
s.loadCategories()
}
}
func (s *Service) loadCategories() {
for {
c, err := s.articleRPC.CategoriesMap(context.TODO(), &artmdl.ArgIP{})
if err != nil || len(c) == 0 {
dao.PromError("service:获取分类")
log.Error("s.articleRPC.CategoriesMap err %v", err)
time.Sleep(time.Second)
continue
}
s.categoriesMap = c
s.categoriesReverseMap = transformReverseCategory(c)
return
}
}
// 生成某个分类下的所有子分类
func transformReverseCategory(cs map[int64]*artmdl.Category) (res map[int64][]*artmdl.Category) {
res = make(map[int64][]*artmdl.Category)
for _, c := range cs {
n := c
old := c
for (n != nil) && (n.ParentID != 0) {
res[n.ParentID] = append(res[n.ParentID], old)
n = cs[n.ParentID]
}
}
return
}

View File

@@ -0,0 +1,38 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UpdateSort(t *testing.T) {
Convey("get data", t, WithoutProcService(func(s *Service) {
err := s.UpdateSort(context.TODO())
So(err, ShouldBeNil)
}))
}
func Test_loadSortArts(t *testing.T) {
Convey("get data", t, WithoutProcService(func(s *Service) {
res, err := s.loadSortArts(context.TODO())
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
}))
}
func Test_TrimArts(t *testing.T) {
arts := [][2]int64{
{1, 11},
{0, 10},
{2, 12},
}
Convey("trim arts", t, func() {
ns := trimArts(arts, 3)
expt := [][2]int64{
{2, 12},
{1, 11},
}
So(ns, ShouldResemble, expt)
})
}

View File

@@ -0,0 +1,216 @@
package service
import (
"context"
"strconv"
"time"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/job/openplatform/article/dao"
"go-common/app/job/openplatform/article/model"
"go-common/library/log"
)
func (s *Service) statproc(i int64) {
defer s.waiter.Done()
var (
err error
ls *lastTimeStat
c = context.TODO()
ch = s.statCh[i]
last = s.statLastTime[i]
)
for {
stat, ok := <-ch
if !ok {
log.Warn("statproc(%d) quit", i)
s.multiUpdateDB(i, last)
return
}
// filter view count
if stat.View != nil && *stat.View > 0 {
var ban bool
var reason, valid string
if ban = s.intercept(stat); ban {
log.Info("intercept view count (aid:%d, ip:%s, mid:%d)", stat.Aid, stat.IP, stat.Mid)
dao.PromInfo("stat:访问计数拦截")
reason = "访问计数拦截"
} else if ban = s.dao.DupViewIntercept(c, stat.Aid, stat.Mid); ban {
log.Info("dupintercept view count (aid:%d, ip:%s, mid:%d)", stat.Aid, stat.IP, stat.Mid)
dao.PromInfo("stat:重复访问计数拦截")
reason = "重复访问计数拦截"
} else if stat.CheatInfo != nil {
viewLv, _ := strconv.Atoi(stat.CheatInfo.Lv)
if limitLv, ok1 := s.cheatArts[stat.Aid]; ok1 && (viewLv <= limitLv) {
ban = true
log.Info("lvintercept view count (aid:%d, ip:%s, mid:%d)", stat.Aid, stat.IP, stat.Mid)
dao.PromInfo("stat:等级访问计数拦截")
reason = "等级访问计数拦截"
}
}
if ban {
valid = "0"
} else {
valid = "1"
}
if stat.CheatInfo != nil {
stat.CheatInfo.Valid = valid
stat.CheatInfo.Reason = reason
}
s.cheatInfo(stat.CheatInfo)
if ban {
continue
}
}
// get stat
if ls, ok = last[stat.Aid]; !ok {
var st *artmdl.StatMsg
if st, err = s.dao.Stat(c, stat.Aid); err != nil {
log.Error("s.dao.Stat(%d) error(%+v)", stat.Aid, err)
continue
}
ls = &lastTimeStat{}
if st == nil {
ls.stat = &artmdl.StatMsg{Aid: stat.Aid, View: new(int64), Like: new(int64), Dislike: new(int64), Favorite: new(int64), Reply: new(int64), Share: new(int64), Coin: new(int64)}
ls.time = 0 // NOTE: make sure update db in first.
} else {
ls.stat = st
ls.time = time.Now().Unix()
}
last[stat.Aid] = ls
}
changed := model.Merge(ls.stat, stat)
// update cache
s.updateCache(c, ls.stat, 0)
s.updateSortCache(c, ls.stat.Aid, changed)
// update db after 120s
if time.Now().Unix()-ls.time > s.updateDbInterval {
s.updateDB(c, ls.stat, 0)
s.updateRecheckDB(c, ls.stat)
s.updateSearchStats(c, ls.stat)
delete(last, stat.Aid) // NOTE: delete ensures that memory should be normal in 120s after channel has been closed.
}
}
}
// updateCache purge stat info in cache
func (s *Service) updateCache(c context.Context, sm *artmdl.StatMsg, count int) (err error) {
stat := &artmdl.ArgStats{
Aid: sm.Aid,
Stats: &artmdl.Stats{
View: *sm.View,
Like: *sm.Like,
Dislike: *sm.Dislike,
Favorite: *sm.Favorite,
Reply: *sm.Reply,
Share: *sm.Share,
Coin: *sm.Coin,
},
}
if err = s.articleRPC.SetStat(context.TODO(), stat); err != nil {
log.Error("s.articleRPC.SetStat aid(%d) view(%d) likes(%d) dislike(%d) favorite(%d) reply(%d) share(%d) coin(%d) error(%+v)",
sm.Aid, *sm.View, *sm.Like, *sm.Dislike, *sm.Favorite, *sm.Reply, *sm.Share, *sm.Coin, err)
dao.PromError("stat:更新计数缓存")
s.dao.PushStat(c, &dao.StatRetry{
Action: dao.RetryUpdateStatCache,
Count: count,
Data: sm,
})
return
}
log.Info("update cache success aid(%d) view(%d) likes(%d) dislike(%d) favorite(%d) reply(%d) share(%d) coin(%d)",
sm.Aid, *sm.View, *sm.Like, *sm.Dislike, *sm.Favorite, *sm.Reply, *sm.Share, *sm.Coin)
dao.PromInfo("stat:更新计数缓存")
return
}
// updateSortCache update sort cache
func (s *Service) updateSortCache(c context.Context, aid int64, changed [][2]int64) (err error) {
if len(changed) == 0 {
return
}
arg := &artmdl.ArgSort{
Aid: aid,
Changed: changed,
}
if err = s.articleRPC.UpdateSortCache(context.TODO(), arg); err != nil {
log.Error("s.articleRPC.UpdateSortCache(aid:%v arg: %+v)", aid, arg)
dao.PromError("stat:更新排序缓存")
return
}
log.Info("success s.articleRPC.UpdateSortCache(aid:%v arg: %+v)", aid, arg)
dao.PromInfo("stat:更新排序缓存")
return
}
// updateDB update stat in db.
func (s *Service) updateDB(c context.Context, stat *artmdl.StatMsg, count int) (err error) {
if _, err = s.dao.Update(context.TODO(), stat); err != nil {
dao.PromError("stat:更新计数DB")
s.dao.PushStat(c, &dao.StatRetry{
Action: dao.RetryUpdateStatDB,
Count: count,
Data: stat,
})
return
}
log.Info("update db success aid(%d) view(%d) likes(%d) dislike(%d) favorite(%d) reply(%d) share(%d) coin(%d)",
stat.Aid, *stat.View, *stat.Like, *stat.Dislike, *stat.Favorite, *stat.Reply, *stat.Share, *stat.Coin)
return
}
// multiUpdateDB update some stat in db.
func (s *Service) multiUpdateDB(i int64, last map[int64]*lastTimeStat) (err error) {
log.Info("multiUpdateDB close(%d) start", i)
c := context.TODO()
for aid, ls := range last {
s.updateDB(c, ls.stat, 0)
log.Info("multiUpdateDB close(%d) update stats aid: %d, value: %+v", i, aid, ls.stat)
}
log.Info("multiUpdateDB close(%d) end", i)
return
}
// intercept intercepts illegal views.
func (s *Service) intercept(stat *artmdl.StatMsg) bool {
return s.dao.Intercept(context.TODO(), stat.Aid, stat.Mid, stat.IP)
}
func (s *Service) cheatInfo(cheat *artmdl.CheatInfo) {
if cheat == nil {
return
}
log.Info("cheatInfo %+v", cheat)
if err := s.cheatInfoc.Info(cheat.Valid, cheat.Client, cheat.Cvid, cheat.Mid, cheat.Lv, cheat.Ts, cheat.IP, cheat.UA, cheat.Refer, cheat.Sid, cheat.Buvid, cheat.DeviceID, cheat.Build, cheat.Reason); err != nil {
log.Error("cheatInfo error(%+v)", err)
}
}
func (s *Service) updateRecheckDB(c context.Context, stat *artmdl.StatMsg) (err error) {
var (
publishTime int64
checkState int
)
if s.setting.Recheck.View == 0 || s.setting.Recheck.Day == 0 {
return
}
if isRecheck, _ := s.dao.GetRecheckCache(c, stat.Aid); isRecheck {
return
}
if publishTime, checkState, err = s.dao.GetRecheckInfo(c, stat.Aid); err != nil || checkState != 0 {
return
}
if s.dao.IsAct(c, stat.Aid) {
return
}
if *(stat.View) > s.setting.Recheck.View {
if publishTime+60*60*24*s.setting.Recheck.Day+s.updateDbInterval > time.Now().Unix() {
if err = s.dao.UpdateRecheck(c, stat.Aid); err == nil {
log.Info("update recheck success aid(%d)", stat.Aid)
s.dao.SetRecheckCache(c, stat.Aid)
}
}
}
return
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"testing"
artmdl "go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Stat(t *testing.T) {
var (
err error
c = context.TODO()
statCnt = int64(5)
stat = &artmdl.StatMsg{
Aid: 88,
View: &statCnt,
Favorite: &statCnt,
Like: &statCnt,
Dislike: &statCnt,
Reply: &statCnt,
Share: &statCnt,
}
)
Convey("updateCache", t, WithoutProcService(func(s *Service) {
err = s.updateCache(c, stat, 0)
So(err, ShouldBeNil)
}))
Convey("updateDB", t, WithoutProcService(func(s *Service) {
err = s.updateDB(c, stat, 0)
So(err, ShouldBeNil)
}))
Convey("select Stat", t, WithoutProcService(func(s *Service) {
var stat *artmdl.StatMsg
stat, err = s.dao.Stat(c, 1)
So(err, ShouldBeNil)
So(stat, ShouldNotBeEmpty)
}))
}

View File

@@ -0,0 +1,31 @@
package service
import (
"context"
"strings"
"go-common/app/interface/main/tag/model"
"go-common/app/job/openplatform/article/dao"
"go-common/library/log"
)
// tags gets article tags.
func (s *Service) tags(c context.Context, aid int64) (res string, err error) {
var (
tags map[int64][]*model.Tag
ts []string
arg = &model.ArgResTags{Type: model.PicResType, Oids: []int64{aid}}
)
if tags, err = s.tagRPC.ResTags(c, arg); err != nil {
log.Error("s.Tags(%d) error(%+v)", aid, err)
dao.PromError("rpc:获取Tag")
return
} else if tags == nil || len(tags[aid]) == 0 {
return
}
for _, t := range tags[aid] {
ts = append(ts, t.Name)
}
res = strings.Join(ts, ",")
return
}