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,149 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"article_test.go",
"author_test.go",
"category_test.go",
"coin_test.go",
"creation_check_test.go",
"creation_test.go",
"creative_list_test.go",
"draft_test.go",
"favorite_test.go",
"hotspots_test.go",
"like_test.go",
"list_test.go",
"message_test.go",
"notice_test.go",
"rank_test.go",
"recommends_test.go",
"service_test.go",
"stat_test.go",
"tag_test.go",
"upper_test.go",
"users_test.go",
"view_test.go",
"xss_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/openplatform/article/conf:go_default_library",
"//app/interface/openplatform/article/model:go_default_library",
"//app/service/main/coin/api/gorpc:go_default_library",
"//app/service/main/coin/model:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/rpc/client:go_default_library",
"//library/cache/redis:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/golang/mock/gomock:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"activity.go",
"anniversary.go",
"apply.go",
"archive.go",
"article.go",
"author.go",
"banner.go",
"cards.go",
"category.go",
"coin.go",
"creation.go",
"creation_check.go",
"creative_list.go",
"draft.go",
"favorite.go",
"history.go",
"hotspots.go",
"infoc.go",
"like.go",
"list.go",
"message.go",
"notice.go",
"old_creative.go",
"rank.go",
"read.go",
"recommends.go",
"search.go",
"sentinel.go",
"service.go",
"setting.go",
"share.go",
"slide.go",
"sort.go",
"stat.go",
"tag.go",
"upper.go",
"users.go",
"view.go",
"xss.go",
],
importpath = "go-common/app/interface/openplatform/article/service",
tags = ["automanaged"],
deps = [
"//app/interface/main/history/model:go_default_library",
"//app/interface/main/history/rpc/client:go_default_library",
"//app/interface/main/tag/model:go_default_library",
"//app/interface/main/tag/rpc/client:go_default_library",
"//app/interface/openplatform/article/conf:go_default_library",
"//app/interface/openplatform/article/dao:go_default_library",
"//app/interface/openplatform/article/model:go_default_library",
"//app/interface/openplatform/article/model/search:go_default_library",
"//app/service/main/account/model:go_default_library",
"//app/service/main/account/rpc/client:go_default_library",
"//app/service/main/archive/api:go_default_library",
"//app/service/main/archive/api/gorpc:go_default_library",
"//app/service/main/archive/model/archive:go_default_library",
"//app/service/main/coin/api/gorpc:go_default_library",
"//app/service/main/coin/model:go_default_library",
"//app/service/main/favorite/api/gorpc:go_default_library",
"//app/service/main/favorite/model:go_default_library",
"//app/service/main/filter/model/rpc:go_default_library",
"//app/service/main/filter/rpc/client:go_default_library",
"//app/service/main/resource/model:go_default_library",
"//app/service/main/resource/rpc/client:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/rpc/client:go_default_library",
"//library/cache:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/log/anticheat:go_default_library",
"//library/log/infoc:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/grokify/html-strip-tags-go:go_default_library",
"//vendor/github.com/microcosm-cc/bluemonday: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,20 @@
package service
import (
"context"
"time"
"go-common/app/interface/openplatform/article/model"
)
// ActInfo .
func (s *Service) ActInfo(c context.Context, plat int8) (res *model.ActInfo, err error) {
res = &model.ActInfo{Activities: []*model.Activity{}, Banners: []*model.Banner{}}
if bs, _ := s.actBanners(c, plat, time.Now()); len(bs) > 0 {
res.Banners = bs
}
for _, act := range s.activities {
res.Activities = append(res.Activities, act)
}
return
}

View File

@@ -0,0 +1,48 @@
package service
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"go-common/app/interface/openplatform/article/model"
account "go-common/app/service/main/account/model"
)
// AnniversaryInfo get reader and author info in passed year
func (s *Service) AnniversaryInfo(c context.Context, mid int64) (res *model.AnniversaryInfo, err error) {
if res, err = s.dao.CacheAnniversary(c, mid); err != nil {
return
}
if res == nil {
res = new(model.AnniversaryInfo)
}
res.Mid = mid
user, _ := s.accountRPC.Info3(c, &account.ArgMid{Mid: mid})
if user != nil {
res.Uname = user.Name
res.Face = user.Face
}
if res.AuthorInfo != nil && res.AuthorInfo.ReaderMid != 0 {
user, _ := s.accountRPC.Info3(c, &account.ArgMid{Mid: res.AuthorInfo.ReaderMid})
if user != nil {
res.AuthorInfo.ReaderUname = user.Name
res.AuthorInfo.ReaderFace = user.Face
}
}
if res.ReaderInfo != nil && res.ReaderInfo.AuthorMid != 0 {
wordsFloat := float64(res.ReaderInfo.Words) / 1000
res.ReaderInfo.Words = int64(math.Pow(wordsFloat, 2))
rankFloat, _ := strconv.ParseFloat(strings.Split(res.ReaderInfo.Rank, "%")[0], 10)
rankFloat = rankFloat / 100
rankFloat = math.Pow(rankFloat, 2)
res.ReaderInfo.Rank = fmt.Sprintf("%.2f", rankFloat*100) + "%"
user, _ := s.accountRPC.Info3(c, &account.ArgMid{Mid: res.ReaderInfo.AuthorMid})
if user != nil {
res.ReaderInfo.AuthorUname = user.Name
}
}
return
}

View File

@@ -0,0 +1,100 @@
package service
import (
"context"
"time"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
)
const (
// identify state
// 0: normal phone number 1: virtual phone number 2: no phone number
_identifyPhoneVirtual = 1 // virtual phone number
_identifyPhoneEmpty = 2 // no phone number
)
// ApplyInfo get apply info
// 检查顺序: 是否已通过->拒绝-> 已提交-> 开放申请-> 申请已满 -> 实名认证/封禁状态/绑定手机
func (s *Service) ApplyInfo(c context.Context, mid int64) (res *model.Apply, err error) {
if mid == 0 {
if !s.setting.ApplyOpen {
err = ecode.ArtApplyClose
return
}
if s.checkApplyFull(c) {
err = ecode.ArtApplyFull
return
}
return
}
res = &model.Apply{}
res.Forbid, _, _ = s.UserDisabled(c, mid)
if res.Forbid {
err = ecode.ArtApplyForbid
return
}
if pass, _, _ := s.IsAuthor(c, mid); pass {
err = ecode.ArtApplyPass
return
}
var author *model.AuthorLimit
if author, err = s.dao.RawAuthor(c, mid); err != nil {
return
}
if author != nil {
if author.State == model.AuthorStatePass {
err = ecode.ArtApplyPass
return
} else if author.State == model.AuthorStateReject {
if time.Now().Unix()-int64(author.Rtime) <= s.setting.ApplyFrozenDuration {
err = ecode.ArtApplyReject
return
}
} else if author.State == model.AuthorStatePending {
err = ecode.ArtApplySubmit
return
}
}
if !s.setting.ApplyOpen {
err = ecode.ArtApplyClose
return
}
if s.checkApplyFull(c) {
err = ecode.ArtApplyFull
return
}
var identify *model.Identify
if identify, err = s.dao.Identify(c, mid); err != nil {
return
}
res.Verify = (identify.Identify == 0)
res.Phone = identify.Phone
if res.Phone == _identifyPhoneEmpty {
err = ecode.ArtApplyPhone
}
return
}
func (s *Service) checkApplyFull(c context.Context) (full bool) {
if count, err := s.dao.ApplyCount(c); err != nil {
return
} else if count > 0 {
return count >= s.setting.ApplyLimit
}
return
}
// Apply add apply
func (s *Service) Apply(c context.Context, mid int64, content, category string) (err error) {
var res = &model.Apply{}
if res, err = s.ApplyInfo(c, mid); err != nil {
return
} else if res.Phone == _identifyPhoneVirtual {
err = ecode.ArtApplyPhoneVirtual
return
}
err = s.dao.AddApply(c, mid, content, category)
return
}

View File

@@ -0,0 +1,38 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/service/main/archive/api"
"go-common/app/service/main/archive/model/archive"
"go-common/library/log"
)
// Archives gets archives by aids.
func (s *Service) Archives(c context.Context, aids []int64, ip string) (arcs map[int64]*api.Arc, err error) {
arg := &archive.ArgAids2{
Aids: aids,
RealIP: ip,
}
if arcs, err = s.arcRPC.Archives3(c, arg); err != nil {
dao.PromError("rpc:获取视频稿件信息")
log.Error("s.arcRPC.Archives(%v) error(%+v)", aids, err)
return
}
fmtArcs(arcs)
return
}
func fmtArcs(arcs map[int64]*api.Arc) {
for id, v := range arcs {
if !v.IsNormal() {
delete(arcs, id)
continue
}
// 会员可见 不展示播放数
if v.Access >= 10000 {
v.Stat.View = -1
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
package service
import (
"context"
"testing"
artmdl "go-common/app/interface/openplatform/article/model"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Article(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
res, err := s.Article(context.TODO(), dataID)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
So(res, ShouldNotBeEmpty)
}))
Convey("no data", t, WithService(func(s *Service) {
res, err := s.Article(context.TODO(), noDataID)
So(err, ShouldBeNil)
So(res, ShouldBeNil)
}))
Convey("ArticleRemainCount", t, WithService(func(s *Service) {
_, err := s.ArticleRemainCount(context.TODO(), art.Author.Mid)
So(err, ShouldBeNil)
}))
}
func Test_ArticleMetas(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
res, err := s.ArticleMetas(context.TODO(), []int64{dataID})
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
So(res, ShouldNotBeEmpty)
}))
Convey("no data", t, WithService(func(s *Service) {
res, err := s.ArticleMetas(context.TODO(), []int64{noDataID})
So(err, ShouldBeNil)
So(res, ShouldBeEmpty)
}))
}
func Test_AddArticleCache(t *testing.T) {
Convey("add data", t, WithService(func(s *Service) {
var c = context.TODO()
err := s.AddArticleCache(context.TODO(), dataID)
So(err, ShouldBeNil)
Convey("del cache", func() {
err := s.DelArticleCache(c, 175, dataID)
So(err, ShouldBeNil)
Convey("delete twice return null", func() {
err := s.DelArticleCache(c, 175, dataID)
So(err, ShouldBeNil)
})
})
}))
}
func Test_FilterNoDistributeArts(t *testing.T) {
a1 := artmdl.Meta{ID: 1}
a2 := artmdl.Meta{ID: 2}
a3 := artmdl.Meta{ID: 3}
a2.AttrSet(int32(1), artmdl.AttrBitNoDistribute)
Convey("array work", t, WithService(func(s *Service) {
res := filterNoDistributeArts([]*artmdl.Meta{&a1, &a2, &a3})
So(res, ShouldResemble, []*artmdl.Meta{&a1, &a3})
}))
Convey("map work", t, WithService(func(s *Service) {
arg := map[int64]*artmdl.Meta{1: &a1, 2: &a2, 3: &a3}
res := map[int64]*artmdl.Meta{1: &a1, 3: &a3}
filterNoDistributeArtsMap(arg)
So(res, ShouldResemble, res)
}))
}
func Test_fmtMoreArts(t *testing.T) {
a1 := &artmdl.Meta{ID: 1, PublishTime: xtime.Time(1)}
a2 := &artmdl.Meta{ID: 2, PublishTime: xtime.Time(2)}
a3 := &artmdl.Meta{ID: 3, PublishTime: xtime.Time(3)}
a4 := &artmdl.Meta{ID: 4, PublishTime: xtime.Time(4)}
a5 := &artmdl.Meta{ID: 5, PublishTime: xtime.Time(5)}
m := map[int64]*artmdl.Meta{1: a1, 2: a2, 3: a3, 4: a4, 5: a5}
Convey("position: x5432", t, func() {
res := fmtMoreArts([]int64{2, 3, 4, 5}, []int64{}, m)
So(res, ShouldResemble, []*artmdl.Meta{a5, a4, a3, a2})
})
Convey("position: x32", t, func() {
res := fmtMoreArts([]int64{2, 3}, []int64{}, m)
So(res, ShouldResemble, []*artmdl.Meta{a3, a2})
})
Convey("position: 54x321", t, func() {
res := fmtMoreArts([]int64{3, 2, 1}, []int64{5, 4}, m)
So(res, ShouldResemble, []*artmdl.Meta{a4, a3, a2, a1})
})
Convey("position: 5432x1", t, func() {
res := fmtMoreArts([]int64{1}, []int64{5, 4, 3, 2}, m)
So(res, ShouldResemble, []*artmdl.Meta{a4, a3, a2, a1})
})
Convey("position: 4321x", t, func() {
res := fmtMoreArts([]int64{}, []int64{4, 3, 2, 1}, m)
So(res, ShouldResemble, []*artmdl.Meta{a4, a3, a2, a1})
})
Convey("position: 2x1", t, func() {
res := fmtMoreArts([]int64{1}, []int64{2}, m)
So(res, ShouldResemble, []*artmdl.Meta{a2, a1})
})
}
func Test_splitAids(t *testing.T) {
aids := []int64{4, 3, 2, 1}
Convey("position: 4", t, func() {
before, after := splitAids(aids, 4)
So(after, ShouldResemble, []int64{})
So(before, ShouldResemble, []int64{3, 2, 1})
})
Convey("position: 3", t, func() {
before, after := splitAids(aids, 3)
So(after, ShouldResemble, []int64{4})
So(before, ShouldResemble, []int64{2, 1})
})
Convey("position: 2", t, func() {
before, after := splitAids(aids, 2)
So(after, ShouldResemble, []int64{4, 3})
So(before, ShouldResemble, []int64{1})
})
Convey("position: 1", t, func() {
before, after := splitAids(aids, 1)
So(after, ShouldResemble, []int64{4, 3, 2})
So(before, ShouldResemble, []int64{})
})
}

View File

@@ -0,0 +1,234 @@
package service
import (
"context"
"fmt"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
account "go-common/app/service/main/account/model"
"go-common/library/ecode"
"go-common/library/log"
)
const (
_serviceArea = "article_info"
_recType = "up_rec"
_rapagesize = 20
)
// UpdateAuthorCache update author cache
func (s *Service) UpdateAuthorCache(c context.Context, mid int64) (err error) {
err = s.dao.DelCacheAuthor(c, mid)
dao.PromInfo("author:更新作者状态")
return
}
// AddAuthor add author to db
func (s *Service) AddAuthor(c context.Context, mid int64) (err error) {
if len(s.activities) == 0 {
err = ecode.ArtNoActivity
return
}
res, forbid, _ := s.IsAuthor(c, mid)
if res {
return
}
if forbid {
err = ecode.ArtUserDisabled
return
}
author, err := s.dao.RawAuthor(c, mid)
if err != nil {
return
}
if author != nil {
if (author.State == model.AuthorStateReject) || (author.State == model.AuthorStateClose) {
err = ecode.ArtAuthorReject
return
}
}
err = s.dao.AddAuthor(c, mid)
if err == nil {
s.dao.DelCacheAuthor(c, mid)
}
return
}
// IsAuthor check that whether user has permission to write article.
func (s *Service) IsAuthor(c context.Context, mid int64) (res bool, forbid bool, err error) {
var level int
forbid, level, _ = s.UserDisabled(c, mid)
if forbid {
return
}
var limit *model.AuthorLimit
limit, _ = s.dao.Author(c, mid)
if limit.Pass() {
res = true
return
}
if limit.Forbid() {
return
}
if level >= 2 {
res = true
return
}
res, err = s.isUpper(c, mid)
return
}
// Authors recommends similar authors by categories
func (s *Service) Authors(c context.Context, mid int64, author int64) (res []*model.AccountCard) {
var (
categories []int64
authors []int64
attentions map[int64]bool
filterAuthors []int64
blacks map[int64]struct{}
mids []int64
err error
)
if categories, err = s.dao.AuthorMostCategories(c, author); err != nil {
return
}
for _, category := range categories {
if authors, err = s.dao.CategoryAuthors(c, category, s.c.Article.RecommendAuthors); err != nil {
return
}
if attentions, err = s.isAttentions(c, mid, authors); err != nil {
return
}
if blacks, err = s.isBlacks(c, mid, authors); err != nil {
return
}
for _, a := range authors {
if _, ok := blacks[a]; !attentions[a] && !ok {
filterAuthors = append(filterAuthors, a)
}
}
}
for _, filterAuthor := range filterAuthors {
if filterAuthor == mid || filterAuthor == author {
continue
}
if forbid, _, _ := s.UserDisabled(c, filterAuthor); !forbid {
mids = append(mids, filterAuthor)
}
}
if len(mids) < 2 {
return
}
for _, m := range mids {
var (
accountCard = &model.AccountCard{}
profile *account.ProfileStat
)
if profile, err = s.accountRPC.ProfileWithStat3(c, &account.ArgMid{Mid: m}); err != nil {
dao.PromError("article:ProfileWithStat3")
log.Error("s.acc.ProfileWithStat3(%d) error %v", m, err)
return
}
if profile != nil {
accountCard.FromProfileStat(profile)
}
res = append(res, accountCard)
}
if len(res) > 3 {
res = res[0:3]
}
return
}
// LevelRequired .
func (s *Service) LevelRequired(c context.Context, mid int64) (ok bool, err error) {
var (
card *account.Card
arg = account.ArgMid{Mid: mid}
)
if card, err = s.accountRPC.Card3(c, &arg); err != nil {
dao.PromError("accountRPC.Card3")
log.Error("s.LevelRequired.accountRPC.Card3(%d) error %v", mid, err)
return
}
if card.Level >= 4 {
ok = true
}
return
}
// RecommendAuthors get recommends from search.
func (s *Service) RecommendAuthors(c context.Context, platform string, mobiApp string, device string, build int, clientIP string, userID int64, buvid string, mid int64) (res *model.RecommendAuthors, err error) {
var ra []*model.RecommendAuthor
if ra, err = s.dao.RecommendAuthors(c, platform, mobiApp, device, build, clientIP, userID, buvid, _recType, _serviceArea, _rapagesize, mid); err != nil {
return
}
if ra == nil {
return
}
res = &model.RecommendAuthors{}
for _, r := range ra {
var (
author = &model.RecAuthor{}
profile *account.ProfileStat
)
if profile, err = s.accountRPC.ProfileWithStat3(c, &account.ArgMid{Mid: r.UpID}); err != nil {
dao.PromError("article:ProfileWithStat3")
log.Error("s.acc.ProfileWithStat3(%d) error %v", r.UpID, err)
return
}
if profile != nil {
author.AccountCard.FromProfileStat(profile)
}
if r.RecReason == "" {
var (
view int64
fans int
viewStr, fansStr string
)
fans = author.Fans
if view, err = s.readCount(c, r.UpID); err != nil {
continue
}
if fans < 1e4 {
fansStr = fmt.Sprintf("粉丝:%d", fans)
} else if fans < 1e8 {
fansStr = fmt.Sprintf("粉丝:%.1f万", float64(fans/1e4))
} else {
fansStr = fmt.Sprintf("粉丝:%.1f亿", float64(fans/1e8))
}
if view < 1e4 {
viewStr = fmt.Sprintf("阅读:%d", view)
} else if view < 1e8 {
viewStr = fmt.Sprintf("阅读:%.1f万", float64(view/1e4))
} else {
viewStr = fmt.Sprintf("阅读:%.1f亿", float64(view/1e8))
}
r.RecReason = fansStr + " " + viewStr
}
author.RecReason = r.RecReason
res.Authors = append(res.Authors, author)
}
res.Count = len(res.Authors)
return
}
func (s *Service) readCount(c context.Context, mid int64) (res int64, err error) {
var (
stat *model.UpStat
st model.UpStat
)
if stat, err = s.dao.CacheUpStatDaily(c, mid); err != nil {
dao.PromError("article:CacheUpStatDaily")
}
if stat != nil {
res = stat.View
return
}
if st, err = s.dao.UpStat(c, mid); err != nil {
dao.PromError("article:获取作者文章数")
}
res = st.View
return
}

View File

@@ -0,0 +1,21 @@
package service
import (
"context"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UpdateAuthorCache(t *testing.T) {
mid := int64(99999999)
Convey("add author", t, WithCleanCache(func() {
//load data
err := s.UpdateAuthorCache(context.TODO(), mid)
So(err, ShouldBeNil)
time.Sleep(time.Millisecond * 100)
ok, _, _ := s.IsAuthor(context.TODO(), mid)
So(ok, ShouldBeTrue)
}))
}

View File

@@ -0,0 +1,133 @@
package service
import (
"context"
"encoding/json"
"strconv"
"time"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
resmdl "go-common/app/service/main/resource/model"
"go-common/library/log"
)
func (s *Service) loadBannersproc() {
for {
now := time.Now()
ts := now.Unix()
if (s.bannersMap == nil) || (ts%s.dao.UpdateBannersInterval == 0) {
banners, err := s.banners(context.TODO(), s.c.Article.BannerIDs)
if err != nil {
dao.PromError("service:更新banner数据")
time.Sleep(time.Second)
continue
}
s.bannersMap = banners
}
// 这里不是每秒钟一更新
time.Sleep(time.Second)
}
}
func (s *Service) loadActBannersproc() {
for {
now := time.Now()
ts := now.Unix()
if (s.actBannersMap == nil) || (ts%s.dao.UpdateBannersInterval == 0) {
banners, err := s.banners(context.TODO(), s.c.Article.ActBannerIDs)
if err != nil {
dao.PromError("service:更新actBanner数据")
time.Sleep(time.Second)
continue
}
s.actBannersMap = banners
}
// 这里不是每秒钟一更新
time.Sleep(time.Second)
}
}
// Banners get banners
func (s *Service) Banners(c context.Context, plat int8, build int, t time.Time) (res []*model.Banner, err error) {
tStr := strconv.FormatInt((t.UnixNano() / 1e6), 10)
for _, banner := range s.bannersMap[int8(plat)] {
if !invalidBuild(build, banner.Build, banner.Condition) {
b := &model.Banner{}
*b = *banner
b.RequestID = tStr
res = append(res, b)
}
}
return
}
func (s *Service) actBanners(c context.Context, plat int8, t time.Time) (res []*model.Banner, err error) {
tStr := strconv.FormatInt((t.UnixNano() / 1e6), 10)
for _, banner := range s.actBannersMap[int8(plat)] {
b := &model.Banner{}
*b = *banner
b.RequestID = tStr
res = append(res, b)
}
return
}
func invalidBuild(srcBuild, cfgBuild int, cfgCond string) bool {
if cfgBuild != 0 && cfgCond != "" {
switch cfgCond {
case "gt":
if cfgBuild >= srcBuild {
return true
}
case "lt":
if cfgBuild <= srcBuild {
return true
}
case "eq":
if cfgBuild != srcBuild {
return true
}
case "ne":
if cfgBuild == srcBuild {
return true
}
}
}
return false
}
func (s *Service) banners(c context.Context, resIDs []int) (res map[int8][]*model.Banner, err error) {
arg := &resmdl.ArgRess{ResIDs: resIDs}
bs, err := s.resRPC.Resources(c, arg)
if err != nil {
dao.PromError("banner:RPC")
log.Error("s.resRPC.Resources(%+v) err: %+v", arg, err)
return
}
res = make(map[int8][]*model.Banner)
for _, r := range bs {
for i, a := range r.Assignments {
b := &model.Banner{
ID: a.ID,
Title: a.Name,
URL: a.URL,
Image: a.Pic,
Position: i + 1,
Plat: int8(r.Platform),
Rule: string(a.Rule),
ResID: r.ID,
}
if b.Rule != "" {
var tmp *model.BannerRule
if json.Unmarshal([]byte(b.Rule), &tmp) == nil {
b.Build = tmp.Build
b.Condition = tmp.Condition
}
}
b.Plat = model.ConvertPlat(b.Plat)
res[b.Plat] = append(res[b.Plat], b)
}
}
return
}

View File

@@ -0,0 +1,231 @@
package service
import (
"context"
"strconv"
"go-common/app/interface/openplatform/article/model"
"go-common/app/service/main/archive/api"
"go-common/library/ecode"
"go-common/library/sync/errgroup"
)
// FindCard find card from source
func (s *Service) FindCard(c context.Context, idStr string) (res interface{}, err error) {
var id int64
if len(idStr) < 3 {
err = ecode.RequestErr
return
}
id, _ = strconv.ParseInt(idStr[2:], 10, 64)
prefix := idStr[:2]
if prefix == model.CardPrefixAudio {
resp, err1 := s.dao.AudioCard(c, []int64{id})
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixBangumi {
resp, err1 := s.dao.BangumiCard(c, []int64{id}, nil)
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixBangumiEp {
resp, err1 := s.dao.BangumiCard(c, nil, []int64{id})
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixTicket {
resp, err1 := s.dao.TicketCard(c, []int64{id})
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixMall {
resp, err1 := s.dao.MallCard(c, []int64{id})
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixArchive {
resp, err1 := s.Archives(c, []int64{id}, "")
if err1 != nil {
return nil, err1
}
if resp != nil {
res = resp[id]
}
return res, err1
}
if prefix == model.CardPrefixArticle {
resp, err1 := s.ArticleMeta(c, id)
if err1 != nil {
return nil, err1
}
return resp, err1
}
err = ecode.RequestErr
return
}
// FindCards find cards
func (s *Service) FindCards(c context.Context, ids []string) (res map[string]interface{}, err error) {
var (
bangumis, eps, audios, malls, tickets, archives, articles []int64
bangumiRes map[int64]*model.BangumiCard
epRes map[int64]*model.BangumiCard
audioRes map[int64]*model.AudioCard
mallRes map[int64]*model.MallCard
ticketRes map[int64]*model.TicketCard
archiveRes map[int64]*api.Arc
articleRes map[int64]*model.Meta
)
for _, idStr := range ids {
if len(idStr) < 2 {
continue
}
id, _ := strconv.ParseInt(idStr[2:], 10, 64)
switch idStr[:2] {
case model.CardPrefixAudio:
audios = append(audios, id)
case model.CardPrefixBangumi:
bangumis = append(bangumis, id)
case model.CardPrefixBangumiEp:
eps = append(eps, id)
case model.CardPrefixMall:
malls = append(malls, id)
case model.CardPrefixTicket:
tickets = append(tickets, id)
case model.CardPrefixArchive:
archives = append(archives, id)
case model.CardPrefixArticle:
articles = append(articles, id)
}
}
group := errgroup.Group{}
group.Go(func() (err error) {
if len(bangumis) < 1 {
return nil
}
if bangumiRes, err = s.dao.BangumiCard(c, bangumis, nil); err == nil {
cache.Save(func() {
s.dao.AddBangumiCardsCache(context.TODO(), bangumiRes)
})
} else {
bangumiRes, _ = s.dao.BangumiCardsCache(c, bangumis)
}
return nil
})
group.Go(func() (err error) {
if len(eps) < 1 {
return nil
}
if epRes, err = s.dao.BangumiCard(c, nil, eps); err == nil {
cache.Save(func() {
s.dao.AddBangumiEpCardsCache(context.TODO(), epRes)
})
} else {
epRes, _ = s.dao.BangumiEpCardsCache(c, eps)
}
return nil
})
group.Go(func() (err error) {
if len(audios) < 1 {
return nil
}
if audioRes, err = s.dao.AudioCard(c, audios); err == nil {
cache.Save(func() {
s.dao.AddAudioCardsCache(context.TODO(), audioRes)
})
} else {
audioRes, _ = s.dao.AudioCardsCache(c, audios)
}
return nil
})
group.Go(func() (err error) {
if len(malls) < 1 {
return nil
}
if mallRes, err = s.dao.MallCard(c, malls); err == nil {
cache.Save(func() {
s.dao.AddMallCardsCache(context.TODO(), mallRes)
})
} else {
mallRes, _ = s.dao.MallCardsCache(c, malls)
}
return nil
})
group.Go(func() (err error) {
if len(tickets) < 1 {
return nil
}
if ticketRes, err = s.dao.TicketCard(c, tickets); err == nil {
cache.Save(func() {
s.dao.AddTicketCardsCache(context.TODO(), ticketRes)
})
} else {
ticketRes, _ = s.dao.TicketCardsCache(c, tickets)
}
return nil
})
group.Go(func() (err error) {
if len(archives) < 1 {
return nil
}
archiveRes, _ = s.Archives(c, archives, "")
return nil
})
group.Go(func() (err error) {
if len(articles) < 1 {
return nil
}
articleRes, _ = s.ArticleMetas(c, articles)
return nil
})
group.Wait()
res = make(map[string]interface{})
for id, v := range bangumiRes {
res[model.CardPrefixBangumi+strconv.FormatInt(id, 10)] = v
}
for id, v := range epRes {
res[model.CardPrefixBangumiEp+strconv.FormatInt(id, 10)] = v
}
for id, v := range ticketRes {
res[model.CardPrefixTicket+strconv.FormatInt(id, 10)] = v
}
for id, v := range mallRes {
res[model.CardPrefixMall+strconv.FormatInt(id, 10)] = v
}
for id, v := range audioRes {
res[model.CardPrefixAudio+strconv.FormatInt(id, 10)] = v
}
for id, v := range archiveRes {
res[model.CardPrefixArchive+strconv.FormatInt(id, 10)] = v
}
for id, v := range articleRes {
res[model.CardPrefixArticle+strconv.FormatInt(id, 10)] = v
}
return
}

View File

@@ -0,0 +1,121 @@
package service
import (
"context"
"sort"
"time"
"go-common/app/interface/openplatform/article/conf"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
)
func (s *Service) loadCategoriesproc() {
for {
time.Sleep(time.Minute * 10)
s.loadCategories()
}
}
func (s *Service) loadCategories() {
for {
c, err := s.dao.Categories(context.TODO())
if err != nil || len(c) == 0 {
dao.PromError("service:获取分类")
time.Sleep(time.Second)
continue
}
s.primaryCategories = transformPrimaryCategory(c)
removeCategoryBanner(c)
s.categoriesMap = c
s.Categories = transformCategory(c)
s.categoriesReverseMap = transformReverseCategory(c)
s.categoryParents = transformCategoryParents(c)
return
}
}
func removeCategoryBanner(cs map[int64]*artmdl.Category) {
for _, c := range cs {
c.BannerURL = ""
}
}
// transformPrimaryCategory 生成一级分区列表
func transformPrimaryCategory(cs map[int64]*artmdl.Category) (res []*artmdl.Category) {
res = make([]*artmdl.Category, 0, len(cs))
for _, c := range cs {
if c.ParentID == 0 {
nc := new(artmdl.Category)
*nc = *c
res = append(res, nc)
}
}
sort.Sort(artmdl.Categories(res))
if len(res) > 4 {
res[4].Name = conf.Conf.Article.AppCategoryName
res[4].BannerURL = conf.Conf.Article.AppCategoryURL
}
return
}
// 生成一级分类含children的数组
func transformCategory(cs map[int64]*artmdl.Category) (res artmdl.Categories) {
newCs := make(map[int64]*artmdl.Category)
for k, c := range cs {
n := *c
newCs[k] = &n
}
m := make(map[int64][]*artmdl.Category)
for _, c := range newCs {
m[c.ParentID] = append(m[c.ParentID], c)
}
for _, x := range m {
sort.Sort(artmdl.Categories(x))
}
res = m[0]
for id, cate := range newCs {
if len(m[id]) != 0 {
cate.Children = m[id]
}
}
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
}
// 生成每个分区的父分区列表(含本分区)
func transformCategoryParents(cs map[int64]*artmdl.Category) (res map[int64][]*artmdl.Category) {
res = make(map[int64][]*artmdl.Category)
for _, c := range cs {
categories := []*artmdl.Category{c}
parentID := c.ParentID
for parentID != 0 {
if cs[parentID] != nil {
categories = append(categories, cs[parentID])
parentID = cs[parentID].ParentID
continue
}
break
}
//reverse categories
for i := len(categories)/2 - 1; i >= 0; i-- {
opp := len(categories) - 1 - i
categories[i], categories[opp] = categories[opp], categories[i]
}
res[c.ID] = categories
}
return
}

View File

@@ -0,0 +1,67 @@
package service
import (
"testing"
artmdl "go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
var a = map[int64]*artmdl.Category{
1: &artmdl.Category{ID: 1, ParentID: 0, Position: 1},
2: &artmdl.Category{ID: 2, ParentID: 0, Position: 0},
3: &artmdl.Category{ID: 3, ParentID: 1, Position: 3},
4: &artmdl.Category{ID: 4, ParentID: 3, Position: 4},
5: &artmdl.Category{ID: 5, ParentID: 2, Position: 5},
}
func Test_transformCategory(t *testing.T) {
Convey("should generate children", t, func() {
expt := artmdl.Categories{
&artmdl.Category{ID: 2, ParentID: 0, Position: 0, Children: []*artmdl.Category{
&artmdl.Category{ID: 5, ParentID: 2, Position: 5},
},
},
&artmdl.Category{ID: 1, ParentID: 0, Position: 1, Children: []*artmdl.Category{
&artmdl.Category{ID: 3, ParentID: 1, Position: 3, Children: []*artmdl.Category{
&artmdl.Category{ID: 4, ParentID: 3, Position: 4},
},
},
}},
}
res := transformCategory(a)
So(res, ShouldResemble, expt)
})
}
func Test_transformReverseCategory(t *testing.T) {
Convey("should generate all children category under a category", t, func() {
expt1 := map[int64][]*artmdl.Category{
1: []*artmdl.Category{a[3], a[4]},
2: []*artmdl.Category{a[5]},
3: []*artmdl.Category{a[4]},
}
expt2 := map[int64][]*artmdl.Category{
1: []*artmdl.Category{a[4], a[3]},
2: []*artmdl.Category{a[5]},
3: []*artmdl.Category{a[4]},
}
res := transformReverseCategory(a)
So(res, ShouldBeIn, expt1, expt2)
})
}
func Test_transformCategoryParents(t *testing.T) {
Convey("should generate all parent category", t, func() {
expt := map[int64][]*artmdl.Category{
1: []*artmdl.Category{a[1]},
2: []*artmdl.Category{a[2]},
3: []*artmdl.Category{a[1], a[3]},
4: []*artmdl.Category{a[1], a[3], a[4]},
5: []*artmdl.Category{a[2], a[5]},
}
res := transformCategoryParents(a)
So(res, ShouldResemble, expt)
})
}

View File

@@ -0,0 +1,22 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/dao"
coin "go-common/app/service/main/coin/model"
"go-common/library/log"
)
// Coin get user coin number
func (s *Service) Coin(c context.Context, mid, aid int64, ip string) (res int64, err error) {
var coins *coin.ArchiveUserCoins
arg := coin.ArgCoinInfo{Mid: mid, Aid: aid, RealIP: ip, AvType: 2}
if coins, err = s.coinRPC.ArchiveUserCoins(c, &arg); err != nil {
dao.PromError("coin:获取投币数量")
log.Error("s.coinRPC.ArchiveUserCoins(%+v) error(%+v)", arg, err)
return
}
res = coins.Multiply
return
}

View File

@@ -0,0 +1,26 @@
package service
import (
"context"
"testing"
coin "go-common/app/service/main/coin/api/gorpc"
coinMdl "go-common/app/service/main/coin/model"
"github.com/golang/mock/gomock"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Coin(t *testing.T) {
mid := int64(1)
aid := int64(1)
Convey("get data", t, WithMock(t, func(mockCtrl *gomock.Controller) {
mock := coin.NewMockRPC(mockCtrl)
s.coinRPC = mock
arg := &coinMdl.ArgCoinInfo{Mid: mid, Aid: aid, AvType: 2}
mock.EXPECT().ArchiveUserCoins(gomock.Any(), arg).Return(&coinMdl.ArchiveUserCoins{Multiply: 10}, nil)
res, err := s.Coin(context.TODO(), mid, aid, "")
So(err, ShouldBeNil)
So(res, ShouldEqual, 10)
}))
}

View File

@@ -0,0 +1,602 @@
package service
import (
"context"
"sort"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
const (
_sortDefault = 0
_sortByCtime = 1
_sortByLike = 2
_sortByReply = 3
_sortByView = 4
_sortByFavorite = 5
_sortByCoin = 6
//_stateCancel 取消活动
// _stateCancel = -1
//_stateJoin 参加活动
_stateJoin = 0
_editTimes = 1
)
// checkPrivilege check that whether user has permission to write article.
func (s *Service) checkPrivilege(c context.Context, mid int64) (err error) {
if res, _, e := s.IsAuthor(c, mid); !res {
err = ecode.ArtCreationNoPrivilege
if e != nil {
dao.PromError("creation:检查作者权限")
}
}
return
}
func (s *Service) checkArtAuthor(c context.Context, aid, mid int64) (am *model.Meta, err error) {
if am, err = s.creationArticleMeta(c, aid); err != nil {
return
} else if am == nil {
err = ecode.NothingFound
return
}
if am.Author.Mid != mid {
err = ecode.ArtCreationMIDErr
}
return
}
// CreationArticle get creation article
func (s *Service) creationArticleMeta(c context.Context, aid int64) (am *model.Meta, err error) {
if am, err = s.dao.CreationArticleMeta(c, aid); err != nil {
dao.PromError("creation:获取文章meta")
return
} else if am == nil {
return
}
if s.categoriesMap[am.Category.ID] != nil {
am.Category = s.categoriesMap[am.Category.ID]
}
var author *model.Author
if author, _ = s.author(c, am.Author.Mid); author != nil {
am.Author = author
}
return
}
// CreationArticle .
func (s *Service) CreationArticle(c context.Context, aid, mid int64) (a *model.Article, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
var (
content string
am *model.Meta
)
a = &model.Article{}
if am, err = s.checkArtAuthor(c, aid, mid); err != nil {
return
}
if am.State == model.StateRePending || am.State == model.StateReReject {
if a, err = s.ArticleVersion(c, aid); err != nil {
return
}
a.Author = am.Author
a.Category = s.categoriesMap[a.Category.ID]
a.List, _ = s.dao.ArtList(c, aid)
a.Stats, _ = s.stat(c, aid)
return
}
group, errCtx := errgroup.WithContext(c)
group.Go(func() (err error) {
content, err = s.dao.CreationArticleContent(c, aid)
return
})
group.Go(func() (err error) {
am.Stats, _ = s.stat(errCtx, aid)
return
})
group.Go(func() (err error) {
am.Tags, _ = s.Tags(errCtx, aid, true)
return
})
group.Go(func() (err error) {
am.List, _ = s.dao.ArtList(errCtx, aid)
return
})
if err = group.Wait(); err != nil {
return
}
a.Meta = am
a.Content = content
log.Info("s.CreationArticle() aid(%d) title(%s) content length(%d)", a.ID, a.Title, len(a.Content))
return
}
// AddArticle adds article.
func (s *Service) AddArticle(c context.Context, a *model.Article, actID, listID int64, ip string) (id int64, err error) {
log.Info("s.AddArticle() aid(%d) title(%s) actID(%d) content length(%d)", a.ID, a.Title, actID, len(a.Content))
defer func() {
if err != nil && err != ecode.CreativeArticleCanNotRepeat {
s.dao.DelSubmitCache(c, a.Author.Mid, a.Title)
}
}()
if err = s.checkPrivilege(c, a.Author.Mid); err != nil {
return
}
a.Content = xssFilter(a.Content)
if err = s.preArticleCheck(c, a); err != nil {
return
}
var num int
if num, err = s.ArticleRemainCount(c, a.Author.Mid); err != nil {
return
} else if num <= 0 {
err = ecode.ArtCreationArticleFull
return
}
if s.checkList(c, a.Author.Mid, listID); err != nil {
return
}
a.State = model.StatePending
var tx *sql.Tx
if tx, err = s.dao.BeginTran(c); err != nil {
log.Error("tx.BeginTran() error(%+v)", err)
return
}
defer func() {
if err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("tx.Rollback() error(%+v)", err1)
}
return
}
if err = tx.Commit(); err != nil {
dao.PromError("creation:添加文章")
log.Error("tx.Commit() error(%+v)", err)
return
}
// id is article id
if e1 := s.creativeAddArticleList(c, a.Meta.Author.Mid, listID, id, false); e1 != nil {
dao.PromError("creation:添加文章绑定文集")
log.Errorv(c, log.KV("log", "creativeAddArticleList"), log.KV("mid", a.Meta.Author.Mid), log.KV("listID", listID), log.KV("article_id", id), log.KV("error", err))
}
}()
// del draft
if err = s.dao.TxDeleteArticleDraft(c, tx, a.Author.Mid, a.ID); err != nil {
dao.PromError("creation:删除草稿")
return
}
if id, err = s.dao.TxAddArticleMeta(c, tx, a.Meta, actID); err != nil {
dao.PromError("creation:删除文章meta")
return
}
keywords, _ := s.Segment(c, int32(id), a.Content, 1, "article")
if err = s.dao.TxAddArticleContent(c, tx, id, a.Content, keywords); err != nil {
dao.PromError("creation:添加文章content")
return
}
// add version
if err = s.dao.TxAddArticleVersion(c, tx, id, a, actID); err != nil {
dao.PromError("creation:添加历史记录")
return
}
if actID > 0 {
if e := s.dao.HandleActivity(c, a.Author.Mid, id, actID, _stateJoin, ip); e != nil {
log.Error("creation: s.act.HandleActivity mid(%d) aid(%d) actID(%d) ip(%s) error(%+v)", a.Author.Mid, id, actID, ip, e)
}
}
var tags []string
for _, t := range a.Tags {
tags = append(tags, t.Name)
}
if err = s.BindTags(c, a.Meta.Author.Mid, id, tags, ip, actID); err != nil {
dao.PromError("creation:发布文章绑定tag")
}
return
}
// UpdateArticle update article.
func (s *Service) UpdateArticle(c context.Context, a *model.Article, actID, listID int64, ip string) (err error) {
log.Info("s.UpdateArticle() aid(%d) title(%s) content length(%d)", a.ID, a.Title, len(a.Content))
if err = s.checkPrivilege(c, a.Author.Mid); err != nil {
return
}
a.Content = xssFilter(a.Content)
if err = s.preArticleCheck(c, a); err != nil {
return
}
a.State = model.StatePending
if a.ID <= 0 {
err = ecode.ArtCreationIDErr
return
}
if _, err = s.checkArtAuthor(c, a.ID, a.Author.Mid); err != nil {
return
}
//这里转换成 -2 2 几种状态
if err = s.convertState(c, a); err != nil {
return
}
if a.State == model.StateRePending {
err = s.updateArticleVersion(c, a, actID)
return
}
if err = s.updateArticleDB(c, a); err != nil {
return
}
var tags []string
for _, t := range a.Tags {
tags = append(tags, t.Name)
}
s.BindTags(c, a.Meta.Author.Mid, a.Meta.ID, tags, ip, actID)
s.CreativeUpdateArticleList(c, a.Meta.Author.Mid, a.ID, listID, false)
return
}
func (s *Service) updateArticleVersion(c context.Context, a *model.Article, actID int64) (err error) {
var tx *sql.Tx
if tx, err = s.dao.BeginTran(c); err != nil {
log.Error("updateArticleVersion.tx.BeginTran() error(%+v)", err)
return
}
defer func() {
if err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("updateArticleVersion.tx.Rollback() error(%+v)", err1)
}
return
}
if err = tx.Commit(); err != nil {
log.Error("updateArticleVersion.tx.Commit() error(%+v)", err)
return
}
}()
if err = s.dao.TxUpdateArticleStateApplyTime(c, tx, a.ID, a.State); err != nil {
dao.PromError("creation:修改文章状态")
return
}
if x, e := s.ArticleVersion(c, a.ID); e == nil && x.ID != 0 {
if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, actID); err != nil {
dao.PromError("creation:更新版本")
}
} else {
if err = s.dao.TxAddArticleVersion(c, tx, a.ID, a, actID); err != nil {
dao.PromError("creation:创建版本")
}
}
// if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, actID); err != nil {
// dao.PromError("creation:更新版本")
// }
return
}
func (s *Service) updateArticleDB(c context.Context, a *model.Article) (err error) {
var tx *sql.Tx
if tx, err = s.dao.BeginTran(c); err != nil {
log.Error("tx.BeginTran() error(%+v)", err)
return
}
if err = s.dao.TxUpdateArticleMeta(c, tx, a.Meta); err != nil {
if err1 := tx.Rollback(); err1 != nil {
dao.PromError("creation:更新文章meta")
log.Error("tx.Rollback() error(%+v)", err1)
}
return
}
keywords, _ := s.Segment(c, int32(a.ID), a.Content, 1, "article")
if err = s.dao.TxUpdateArticleContent(c, tx, a.ID, a.Content, keywords); err != nil {
if err1 := tx.Rollback(); err1 != nil {
dao.PromError("creation:更新文章content")
log.Error("tx.Rollback() error(%+v)", err1)
}
return
}
// add version
if err = s.dao.TxUpdateArticleVersion(c, tx, a.ID, a, 0); err != nil {
dao.PromError("creation:添加历史记录")
return
}
if err = tx.Commit(); err != nil {
dao.PromError("creation:更新文章")
log.Error("tx.Commit() error(%+v)", err)
}
return
}
// DelArticle drops article.
func (s *Service) DelArticle(c context.Context, aid, mid int64) (err error) {
if aid <= 0 {
err = ecode.ArtCreationIDErr
return
}
if err = s.checkPrivilege(c, mid); err != nil {
return
}
var a *model.Meta
if a, err = s.checkArtAuthor(c, aid, mid); err != nil {
return
}
// can not delelte article which state is pending.
if a.State == model.StatePending || a.State == model.StateOpenPending {
err = ecode.ArtCreationDelPendingErr
return
}
lists, _ := s.dao.RawArtsListID(c, []int64{aid})
if err = s.delArticle(c, aid); err != nil {
return
}
s.dao.DelActivity(c, aid, "")
cache.Save(func() {
c := context.TODO()
s.dao.DelRecommend(c, aid)
s.DelRecommendArtCache(c, aid, a.Category.ID)
s.DelArticleCache(c, mid, aid)
s.deleteArtsListCache(c, aid)
if lists[aid] > 0 {
s.updateListInfo(c, lists[aid])
s.RebuildListCache(c, lists[aid])
}
})
return
}
func (s *Service) delArticle(c context.Context, aid int64) (err error) {
var tx *sql.Tx
if tx, err = s.dao.BeginTran(c); err != nil {
log.Error("tx.BeginTran() error(%+v)", err)
return
}
defer func() {
if err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("tx.Rollback() error(%+v)", err1)
}
return
}
if err = tx.Commit(); err != nil {
dao.PromError("creation:删除文章")
log.Error("tx.Commit() error(%+v)", err)
}
}()
if err = s.dao.TxDeleteArticleMeta(c, tx, aid); err != nil {
dao.PromError("creation:删除文章meta")
return
}
if err = s.dao.TxDeleteArticleContent(c, tx, aid); err != nil {
dao.PromError("creation:delete文章content")
return
}
if err = s.dao.TxDelFilteredArtMeta(c, tx, aid); err != nil {
dao.PromError("creation:删除过滤文章meta")
return
}
if err = s.dao.TxDelFilteredArtContent(c, tx, aid); err != nil {
dao.PromError("creation:删除过滤文章content")
return
}
if err = s.dao.TxDelArticleList(tx, aid); err != nil {
return
}
if err = s.dao.TxDelArticleVersion(c, tx, aid); err != nil {
return
}
return
}
// CreationUpperArticlesMeta gets article list by mid.
func (s *Service) CreationUpperArticlesMeta(c context.Context, mid int64, group, category, sortType, pn, ps int, ip string) (res *model.CreationArts, err error) {
res = &model.CreationArts{}
if err = s.checkPrivilege(c, mid); err != nil {
return
}
var (
aids []int64
ams, as []*model.Meta
stats = make(map[int64]*model.Stats)
start = (pn - 1) * ps
end = start + ps - 1
total int
)
res.Page = &model.ArtPage{
Pn: pn,
Ps: ps,
}
eg, errCtx := errgroup.WithContext(c)
eg.Go(func() (err error) {
res.Type, _ = s.dao.UpperArticlesTypeCount(errCtx, mid)
return
})
eg.Go(func() (err error) {
if ams, err = s.dao.UpperArticlesMeta(errCtx, mid, group, category); err != nil {
return
}
total = len(ams)
res.Page.Total = total
return
})
if err = eg.Wait(); err != nil {
log.Error("eg.Wait() error(%+v)", err)
return
}
if total == 0 || total < start {
return
}
for _, v := range ams {
aids = append(aids, v.ID)
}
if stats, err = s.stats(c, aids); err != nil {
dao.PromError("creation:获取计数信息")
log.Error("s.stats(%v) err: %+v", aids, err)
err = nil
}
for _, v := range ams {
// stats
if st := stats[v.ID]; st != nil {
v.Stats = st
} else {
v.Stats = new(model.Stats)
}
}
switch sortType {
case _sortDefault, _sortByCtime:
sort.Slice(ams, func(i, j int) bool { return ams[i].Ctime > ams[j].Ctime })
case _sortByLike:
sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Like > ams[j].Stats.Like })
case _sortByReply:
sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Reply > ams[j].Stats.Reply })
case _sortByView:
sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.View > ams[j].Stats.View })
case _sortByFavorite:
sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Favorite > ams[j].Stats.Favorite })
case _sortByCoin:
sort.Slice(ams, func(i, j int) bool { return ams[i].Stats.Coin > ams[j].Stats.Coin })
}
if total > end {
as = ams[start : end+1]
} else {
as = ams[start:]
}
for _, v := range as {
var pid int64
if pid, err = s.CategoryToRoot(v.Category.ID); err != nil {
dao.PromError("creation:获取根分区")
log.Error("s.CategoryToRoot(%d) error(%+v)", v.Category.ID, err)
continue
}
v.Category = s.categoriesMap[pid]
v.List, _ = s.dao.ArtList(c, v.ID)
}
res.Articles = as
return
}
// convertState converts -1 to 1, -2 to 2 if the article had been published.
func (s *Service) convertState(c context.Context, a *model.Article) (err error) {
var am *model.Meta
if am, err = s.dao.CreationArticleMeta(c, a.ID); err != nil {
return
}
if am.State == model.StateOpen || am.State == model.StateRePass || am.State == model.StateReReject {
if s.EditTimes(c, a.ID) <= 0 {
err = ecode.ArtUpdateFullErr
return
}
a.State = model.StateRePending
return
}
if (am.State != model.StateReject) && (am.State != model.StateOpenReject) {
err = ecode.ArtCannotEditErr
return
}
if am.State > 0 && a.State < 0 {
a.State = -a.State
}
return
}
// CreationWithdrawArticle recall the article and add it to draft.
func (s *Service) CreationWithdrawArticle(c context.Context, mid, aid int64) (err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
var am *model.Meta
if am, err = s.checkArtAuthor(c, aid, mid); err != nil {
return
}
// 只有初次提交待审的文章才可以撤回,发布后的修改待审不允许撤回
if am.State != model.StatePending {
err = ecode.ArtCreationStateErr
return
}
var content string
if content, err = s.dao.CreationArticleContent(c, aid); err != nil {
return
}
// add draft
var tags []string
if ts, e := s.Tags(c, aid, false); e == nil && len(ts) > 0 {
for _, v := range ts {
tags = append(tags, v.Name)
}
}
am.ID = 0
draft := &model.Draft{Article: &model.Article{Meta: am, Content: content}, Tags: tags}
if _, err = s.AddArtDraft(c, draft); err != nil {
return
}
// delete article
err = s.delArticle(c, aid)
// err = s.dao.UpdateArticleState(c, aid, model.StateOpen)
return
}
// UpStat up stat
func (s *Service) UpStat(c context.Context, mid int64) (res model.UpStat, err error) {
return s.dao.UpStat(c, mid)
}
// UpThirtyDayStat for 30 days stat.
func (s *Service) UpThirtyDayStat(c context.Context, mid int64) (res []*model.ThirtyDayArticle, err error) {
res, err = s.dao.ThirtyDayArticle(c, mid)
return
}
// ArticleUpCover article upload cover.
func (s *Service) ArticleUpCover(c context.Context, fileType string, body []byte) (url string, err error) {
if len(body) == 0 {
err = ecode.FileNotExists
return
}
if len(body) > s.c.BFS.MaxFileSize {
err = ecode.FileTooLarge
return
}
url, err = s.dao.UploadImage(c, fileType, body)
if err != nil {
log.Error("creation: s.bfs.Upload error(%v)", err)
}
return
}
// ArticleVersion .
func (s *Service) ArticleVersion(c context.Context, aid int64) (a *model.Article, err error) {
if a, err = s.dao.ArticleVersion(c, aid); err != nil {
return
}
a.Category = s.categoriesMap[a.Category.ID]
return
}
// EditTimes .
func (s *Service) EditTimes(c context.Context, id int64) (res int) {
var (
et = _editTimes
count = et
err error
)
if s.c.Article.EditTimes != 0 {
et = s.c.Article.EditTimes
}
if count, err = s.dao.EditTimes(c, id); err != nil {
return
}
res = et - count
if res < 0 {
res = 0
}
return
}
func (s *Service) lastReason(c context.Context, id int64, state int32) (res string, err error) {
return s.dao.LastReason(c, id, state)
}

View File

@@ -0,0 +1,295 @@
package service
import (
"context"
"html"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf16"
"unicode/utf8"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
strip "github.com/grokify/html-strip-tags-go"
)
var (
_zeroWidthReg = regexp.MustCompile(`[\x{200b}]+`)
_nocharReg = []*regexp.Regexp{
// regexp.MustCompile(`[\p{Hangul}]+`), // kr
regexp.MustCompile(`[\p{Tibetan}]+`), // tibe
regexp.MustCompile(`[\p{Arabic}]+`), // arabic
}
_chineseReg = regexp.MustCompile(`[\p{Han}]+`) // chinese
)
func (s *Service) allowRepeat(c context.Context, mid int64, title string) (ok bool) {
log.Info("allowRepeat check start | mid(%d) title(%s).", mid, title)
exist, _ := s.dao.SubmitCache(c, mid, title)
log.Info("allowRepeat from cache | mid(%d) title(%s) exist(%d).", mid, title, exist)
if !exist {
log.Info("allowRepeat not exist | mid(%d) title(%s)", mid, title)
s.dao.AddSubmitCache(c, mid, title)
log.Info("allowRepeat add cache | mid(%d) title(%s).", mid, title)
ok = true
return
}
dao.PromInfo("creation:禁止重复标题")
return
}
func (s *Service) preMust(c context.Context, art *model.Article) (err error) {
var ok bool
if art.Title, ok = s.checkTitle(art.Title); !ok || art.Title == "" {
log.Error("s.checkTitle mid(%d) art.Title(%s) title contains illegal char or is empty", art.Author.Mid, art.Title)
err = ecode.CreativeArticleTitleErr
return
}
if art.Content, ok = s.checkContent(art.Content); !ok {
log.Error("s.checkContent mid(%d) content too long", art.Author.Mid)
err = ecode.CreativeArticleContentErr
return
}
if !s.allowCategory(art.Category.ID) {
log.Error("s.allowCategory mid(%d) art.Category(%d) not exists", art.Author.Mid, art.Category)
err = ecode.CreativeArticleCategoryErr
return
}
if !s.allowReprints(int8(art.Reprint)) {
log.Error("s.allowReprints mid(%d) art.Reprint(%d) illegal reprint", art.Author.Mid, art.Reprint)
err = ecode.CreativeArticleReprintErr
return
}
if !s.allowTID(int8(art.TemplateID)) {
log.Error("s.allowTID mid(%d) art.TemplateID(%d) illegal reprint", art.Author.Mid, art.TemplateID)
err = ecode.CreativeArticleTIDErr
return
}
if !model.ValidTemplate(art.TemplateID, art.ImageURLs) {
err = ecode.ArtCreationTplErr
return
}
if !s.allowTag(art.Tags) {
log.Error("s.allowTag mid(%d) art.Tags(%s) tag name or number too large", art.Author.Mid, art.Tags)
err = ecode.CreativeArticleTagErr
}
if art.Dynamic, ok = s.allowDynamicIntro(art.Dynamic); !ok {
log.Error("s.checkDynamicIntro mid(%d) art.DynamicIntro(%s) title contains illegal char", art.Author.Mid, art.Dynamic)
err = ecode.CreativeDynamicIntroErr
return
}
return
}
func (s *Service) checkTitle(title string) (ct string, ok bool) {
title = strings.TrimSpace(title)
allCount := utf8.RuneCountInString(title)
enCount := utf8.RuneCountInString(_chineseReg.ReplaceAllString(title, ""))
chineseCount := allCount - enCount
if chineseCount*2+enCount > 80 {
return
}
for _, reg := range _nocharReg {
if reg.MatchString(title) {
return
}
}
ct = _zeroWidthReg.ReplaceAllString(title, "")
if utf8.RuneCountInString(ct) <= 0 {
return
}
ok = true
return
}
func (s *Service) contentStripSize(content string) (count int) {
stripped := strip.StripTags(content)
stripped = html.UnescapeString(stripped)
stripped = strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1
}
return r
}, stripped)
stripped = strings.Replace(stripped, "\u200B", "", -1)
stripped = strings.Replace(stripped, "\u00a0", "", -1)
// utf16 size
for _, r := range stripped {
count += len(utf16.Encode([]rune{rune(r)}))
}
// 图片计算为一个字
offset := strings.Count(content, "<img") - strings.Count(stripped, "<img")
count += offset
return
}
func (s *Service) checkContent(content string) (ct string, ok bool) {
ct = strings.TrimSpace(content)
ct = _zeroWidthReg.ReplaceAllString(ct, "")
if len(ct) > s.c.Article.MaxContentSize {
return
}
size := s.contentStripSize(ct)
if size < s.c.Article.MinContentLength || size > s.c.Article.MaxContentLength {
return
}
ok = true
return
}
func (s *Service) preArticleCheck(c context.Context, art *model.Article) (err error) {
if !s.allowRepeat(c, art.Author.Mid, art.Title) {
err = ecode.CreativeArticleCanNotRepeat
return
}
if err = s.preMust(c, art); err != nil {
return
}
return
}
func (s *Service) allowCategory(cid int64) (ok bool) {
_, ok = s.categoriesMap[cid]
return
}
func (s *Service) allowReprints(cp int8) (ok bool) {
ok = model.InReprints(cp)
return
}
func (s *Service) allowTID(tid int8) (ok bool) {
ok = model.InTemplateID(tid)
return
}
func (s *Service) allowTag(tags []*model.Tag) (ok bool) {
if (len(tags) > 12) || (len(tags) == 0) {
return
}
for _, tag := range tags {
if _zeroWidthReg.MatchString(tag.Name) {
return
}
if (utf8.RuneCountInString(tag.Name) > 20) || (tag.Name == "") {
return
}
}
return true
}
//allowDynamicIntro 移动端动态推荐语选填不能超过233字.
func (s *Service) allowDynamicIntro(dynamicIntro string) (ct string, ok bool) {
ct = strings.TrimSpace(dynamicIntro)
if utf8.RuneCountInString(ct) > 233 {
return
}
ok = true
return
}
func (s *Service) preDraftCheck(c context.Context, art *model.Draft) (err error) {
if art.Title == "" {
art.Title = "无标题"
}
var ok bool
if art.Title, ok = s.checkTitle(art.Title); !ok {
log.Error("s.checkTitle mid(%d) art.Title(%s) title contains illegal char or is empty", art.Author.Mid, art.Title)
err = ecode.CreativeArticleTitleErr
}
return
}
// ParseParam parse article param which type is int.
func (s *Service) ParseParam(c context.Context, categoryStr, reprintStr, tidStr, imageURLs, originImageURLs string) (art *model.ArtParam, err error) {
var (
category int64
tid, reprint int
)
category, err = strconv.ParseInt(categoryStr, 10, 64)
if err != nil || category <= 0 { //文章要求必须传大于0的分类
err = ecode.CreativeArticleCategoryErr
return
}
tid, err = strconv.Atoi(tidStr)
if err != nil || tid < 0 {
err = ecode.CreativeArticleTIDErr
return
}
reprint, err = strconv.Atoi(reprintStr)
if err != nil || reprint < 0 {
err = ecode.CreativeArticleReprintErr
return
}
imgs, oimgs, err := ParseImageURLs(imageURLs, originImageURLs)
if err != nil {
return
}
art = &model.ArtParam{
Category: category,
TemplateID: int32(tid),
Reprint: int32(reprint),
ImageURLs: imgs,
OriginImageURLs: oimgs,
}
return
}
// ParseDraftParam parse draft param which type is int.
func (s *Service) ParseDraftParam(c context.Context, categoryStr, reprintStr, tidStr, imageURLs, originImageURLs string) (art *model.ArtParam, err error) {
var (
category int64
tid, reprint int
)
if categoryStr != "" {
category, err = strconv.ParseInt(categoryStr, 10, 64)
if err != nil || category < 0 {
err = ecode.CreativeArticleCategoryErr
return
}
}
if tidStr != "" {
tid, err = strconv.Atoi(tidStr)
if err != nil || tid < 0 {
err = ecode.CreativeArticleTIDErr
return
}
}
if reprintStr != "" {
reprint, err = strconv.Atoi(reprintStr)
if err != nil || reprint < 0 {
err = ecode.CreativeArticleReprintErr
return
}
}
imgs, oimgs, err := ParseImageURLs(imageURLs, originImageURLs)
if err != nil {
return
}
art = &model.ArtParam{
Category: category,
TemplateID: int32(tid),
Reprint: int32(reprint),
ImageURLs: imgs,
OriginImageURLs: oimgs,
}
return
}
//ParseImageURLs parse img urls to []string.
func ParseImageURLs(imageURLs, originImageURLs string) (imgs, oimgs []string, err error) {
if originImageURLs == "" {
originImageURLs = imageURLs
}
imgs = strings.Split(imageURLs, ",")
oimgs = strings.Split(originImageURLs, ",")
if len(imgs) != len(oimgs) {
err = ecode.CreativeArticleImageURLsErr
}
return
}

View File

@@ -0,0 +1,59 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_contentStripSize(t *testing.T) {
Convey("emoji size 2", t, func() {
size := s.contentStripSize("😀")
So(size, ShouldEqual, 2)
})
Convey("chinese and english", t, func() {
size := s.contentStripSize("中a")
So(size, ShouldEqual, 2)
})
Convey("ignore normal blank char", t, func() {
size := s.contentStripSize("中 a \n \t b")
So(size, ShouldEqual, 3)
})
Convey("img size 1", t, func() {
size := s.contentStripSize("<img></img>")
So(size, ShouldEqual, 1)
})
Convey("truely data", t, func() {
data := `<p>音乐卡:</p><figure class="img-box" contenteditable="false"><img src="//uat-i0.hdslb.com/bfs/article/0aae45bcb008157ba5c7765ab8d952284d12fcad.png" aid="au75" width="1320" height="188" class="music-card" type="normal"/></figure><p>商品卡:</p><figure class="img-box" contenteditable="false"><img src="//uat-i0.hdslb.com/bfs/article/999065dfd84193ecbbd590a6a6fd46a374d2a840.png" aid="sp886" width="1320" height="208" class="shop-card" type="normal"/></figure><p>票务卡:</p><figure class="img-box" contenteditable="false"><img src="//uat-i0.hdslb.com/bfs/article/458aec77c8523fcb5e846b128e68804ee875cc26.png" aid="pw100" width="1320" height="208" class="shop-card" type="normal"/></figure><p><br/></p>`
size := s.contentStripSize(data)
So(size, ShouldEqual, 15)
})
Convey("truely data 2", t, func() {
data := `<p><br/></p><figure class="img-box" contenteditable="false"><img src="//i0.hdslb.com/bfs/article/690a4cdd2d652c04b32aa737f9653895b909c8da.png" width="745" height="289"/><figcaption class="caption" contenteditable="true">-</figcaption></figure><p><br/></p><p><br/></p><p><span class="font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="color-blue-04 font-size-14">海奉是一个风景优美的地方,但并不在沿海。数量众多的旅行家笔记显示,海奉是一片死火山群。那里坐落着世界上最高的山峰——奈文摩尔峰,峰顶终年积雪。其它沉睡的火山围坐在他的周围,高低不同,错落有致。火山口往往积蓄湖水,形成湖泊,当地人称之为“镜湖”。每到雨季,经过连续的降雨,湖中的水便会溢出,从山顶冲下,形成“水山爆发”的情景。山脚下是海奉人的村落,那里的房子全部以木头搭建,巧妙的避开河水的必经之路。海奉人以木工闻名,无论是精巧的木头机械还是美丽的木雕都不在话下。此外,每一个海奉人都戴着一枚木制的十字架,那是由海奉独有的铁木制成,绝不出售给外人,因而成为海奉人的标志。</span></span></p><p><span class="font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 但是故事并不发生在海奉,这些描写仅是因为主角是海奉人。</span></p><p style="text-align: left;"><span class="font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="color-blue-04 font-size-14">船还在航行。天色昏暗,雨从来没有停过。船舱紧闭,窗口透出一丝微弱的光。</span></span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “您是海奉人吗?”山本真奈美借着微弱的灯光盯着他的十字架。</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “我家乡在海奉。”任川吃着生鱼片,随手用一根铁钎拨弄油灯的灯芯。</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “您为什么离开家乡呢?”</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 任川没有回答。</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “我只是好奇,毕竟海奉是个风景如画的地方。”</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “那可是个十分可笑的理由,”任川叹了口气,“你不理解也没关系,请答应不要打断我吧。”</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; “嗯。”</span></p><p style="text-align: left;"><span class="color-blue-04 font-size-14">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`
size := s.contentStripSize(data)
So(size, ShouldEqual, 496)
})
Convey("unicode blank char should eq 0", t, func() {
So(s.contentStripSize("\u200B"), ShouldEqual, 0)
So(s.contentStripSize("\u00a0"), ShouldEqual, 0)
})
}
func Test_checkTitle(t *testing.T) {
Convey("en 80 should ok", t, func() {
title := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
res, ok := s.checkTitle(title)
So(res, ShouldEqual, title)
So(ok, ShouldBeTrue)
})
Convey("chinese 40 should ok", t, func() {
title := "好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好"
res, ok := s.checkTitle(title)
So(res, ShouldEqual, title)
So(ok, ShouldBeTrue)
})
Convey("chinese 30 and en 21 should be wrong", t, func() {
title := "好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好aaaaaaaaaaaaaaaaaaaaa"
_, ok := s.checkTitle(title)
So(ok, ShouldBeFalse)
})
}

View File

@@ -0,0 +1,121 @@
package service
import (
"context"
"fmt"
"testing"
"time"
artmdl "go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
categories = []*artmdl.Category{
&artmdl.Category{Name: "游戏", ID: 1},
&artmdl.Category{Name: "动漫", ID: 2},
}
art = artmdl.Article{
Meta: &artmdl.Meta{
Category: categories[0],
Title: "隐藏于时区记忆中的,是希望还是绝望!",
Summary: "说起日本校服,第一个浮现在我们脑海中的必然是那象征着青春阳光 蓝白色相称的水手服啦. 拉色短裙配上洁白的直袜",
BannerURL: "http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg",
TemplateID: 4,
State: artmdl.StatePending,
Author: &artmdl.Author{Mid: 8167601, Name: "爱蜜莉雅", Face: "http://i1.hdslb.com/bfs/face/5c6109964e78a84021299cdf71739e21cd7bc208.jpg"},
Reprint: 0,
ImageURLs: []string{"http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg"},
OriginImageURLs: []string{"http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg"},
PublishTime: 1495784507,
Stats: &artmdl.Stats{Favorite: 100, Like: 10, View: 500, Dislike: 1, Share: 99},
Attributes: 2,
Words: 555,
Dynamic: "dynamic",
Tags: []*artmdl.Tag{&artmdl.Tag{Name: "tag"}},
},
Content: "海奉是一个风景优美的地方,但并不在沿海。数量众多的旅行家笔记显示,海奉是一片死火山群。那里坐落着世界上最高的山峰——奈文摩尔峰,峰顶终年积雪。其它沉睡的火山围坐在他的周围,高低不同,错落有致。火山口往往积蓄湖水,形成湖泊,当地人称之为“镜湖”。每到雨季,经过连续的降雨,湖中的水便会溢出,从山顶冲下,形成“水山爆发”的情景。山脚下是海奉人的村落,那里的房子全部以木头搭建,巧妙的避开河水的必经之路。海奉人以木工闻名,无论是精巧的木头机械还是美丽的木雕都不在话下。此外,每一个海奉人都戴着一枚木制的十字架,那是由海奉独有的铁木制成,绝不出售给外人,因而成为海奉人的标志。但是故事并不发生在海奉,这些描写仅是因为主角是海奉人。船还在航行。天色昏暗,雨从来没有停过。船舱紧闭,窗口透出一丝微弱的光。“您是海奉人吗?”山本真奈美借着微弱的灯光盯着他的十字架。",
}
)
func Test_Creation(t *testing.T) {
var (
err error
aid int64
c = context.TODO()
)
Convey("creation article", t, WithService(func(s *Service) {
res := &artmdl.Article{
Meta: &artmdl.Meta{
Title: "title",
Category: &artmdl.Category{ID: 39},
Author: &artmdl.Author{Mid: art.Author.Mid},
Tags: []*artmdl.Tag{&artmdl.Tag{Name: "tag"}},
TemplateID: 1,
},
}
Convey("AddArticle", func() {
art.Title = fmt.Sprintf("隐藏于时区记忆中的,是希望还是绝望!_%v", time.Now().UnixNano())
aid, err = s.AddArticle(c, &art, 0, 0, "")
So(err, ShouldBeNil)
So(aid, ShouldBeGreaterThan, 0)
err = s.SetStat(c, aid, &artmdl.Stats{
View: 10,
Reply: 9,
Like: 5,
})
So(err, ShouldBeNil)
Convey("CreationArticle", func() {
res, err = s.CreationArticle(c, aid, art.Author.Mid)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
})
Convey("UpdateArticleDB", func() {
art2 := &artmdl.Article{Meta: &artmdl.Meta{}}
*art2 = art
art2.ID = aid
art2.ImageURLs = []string{"http://i2.hdslb.com/bfs/archive/00.jpg"}
art2.OriginImageURLs = []string{"http://i2.hdslb.com/bfs/archive/01.jpg"}
art2.Dynamic = "update 2"
err = s.updateArticleDB(c, art2)
So(err, ShouldBeNil)
res, err = s.CreationArticle(c, art2.ID, art2.Author.Mid)
So(err, ShouldBeNil)
So(res.Dynamic, ShouldEqual, art2.Dynamic)
So(res.ImageURLs, ShouldResemble, []string{"https://i0.hdslb.com/bfs/archive/00.jpg"})
So(res.OriginImageURLs, ShouldResemble, []string{"https://i0.hdslb.com/bfs/archive/01.jpg"})
})
Convey("UpdateArticle", func() {
// res.Content = art.Content
// res.ID = aid
// err = s.UpdateArticle(c, res, 0, 0, "")
// So(err, ShouldBeNil)
err = s.dao.UpdateArticleState(c, aid, artmdl.StateOpen)
So(err, ShouldBeNil)
Convey("DelArticle", func() {
err = s.DelArticle(c, aid, art.Author.Mid)
So(err, ShouldBeNil)
})
})
})
Convey("CreationUpperArticlesMeta", func() {
mid := art.Author.Mid
group := 0
category := 1
sortType := 1
pn := 1
ps := 10
res2, err := s.CreationUpperArticlesMeta(c, mid, group, category, sortType, pn, ps, "")
So(err, ShouldBeNil)
So(res2, ShouldNotBeNil)
})
}))
}

View File

@@ -0,0 +1,550 @@
package service
import (
"context"
"sort"
"time"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
filter "go-common/app/service/main/filter/model/rpc"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
xtime "go-common/library/time"
)
// const _novel = 16
const _positionStep = 1000
// CreativeUpLists up list
func (s *Service) CreativeUpLists(c context.Context, mid int64) (novel bool, lists []*model.CreativeList, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
// var count int64
// if count, err = s.dao.CreativeCountArticles(c, mid, s.novelCIDs()); err != nil {
// return
// }
// novel = count > 0
novel = true
ls, err := s.dao.CreativeUpLists(c, mid)
if err != nil {
return
}
sortLists(ls)
var ids []int64
for _, l := range ls {
ids = append(ids, l.ID)
}
arts, err := s.dao.CreativeListsArticles(c, ids)
if err != nil {
return
}
for _, l := range ls {
lists = append(lists, &model.CreativeList{List: l, Total: len(arts[l.ID])})
}
return
}
func sortLists(lists []*model.List) {
sort.Slice(lists, func(i, j int) bool {
it := int64(lists[i].Ctime)
jt := int64(lists[j].Ctime)
if int64(lists[i].UpdateTime) > it {
it = int64(lists[i].UpdateTime)
}
if int64(lists[j].UpdateTime) > jt {
jt = int64(lists[j].UpdateTime)
}
return it > jt
})
}
func (s *Service) filter(c context.Context, content string) (res string, err error) {
arg := filter.ArgFilter{Area: "article", Message: content}
var filterRes *filter.FilterRes
if filterRes, err = s.filterRPC.FilterArea(c, &arg); err != nil {
dao.PromError("creative:过滤服务")
log.Errorv(c, log.KV("log", "s.filterRPC.Filter"), log.KV("content", content), log.KV("err", err))
res = content
return
}
res = filterRes.Result
return
}
// CreativeAddList add list
func (s *Service) CreativeAddList(c context.Context, mid int64, name string, summary, imageURL string) (res *model.List, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
if lists, _ := s.dao.CreativeUpLists(c, mid); len(lists) >= s.c.Article.ListLimit {
err = ecode.ArtMaxListErr
return
}
var newName, newSummary string
newName, _ = s.filter(c, name)
if newName != name {
log.Infov(c, log.KV("log", "filter list"), log.KV("old", name), log.KV("new", newName))
}
if summary != "" {
newSummary, _ = s.filter(c, summary)
if newSummary != summary {
log.Infov(c, log.KV("log", "filter list summary"), log.KV("old", summary), log.KV("new", newSummary))
}
}
name = newName
summary = newSummary
var ok bool
if name, ok = s.checkTitle(name); !ok || name == "" {
log.Errorv(c, log.KV("log", "CreativeAddList"), log.KV("name", name), log.KV("mid", mid))
err = ecode.ArtListNameErr
return
}
id, err := s.dao.CreativeListAdd(c, mid, name, imageURL, summary, xtime.Time(0), 0)
if err != nil {
return
}
res, err = s.dao.RawList(c, id)
cache.Save(func() {
s.dao.RebuildUpListsCache(context.TODO(), mid)
})
return
}
// CreativeDelList del list
func (s *Service) CreativeDelList(c context.Context, mid int64, id int64) (err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
if _, err = s.checkList(c, mid, id); err != nil {
return
}
arts, _ := s.dao.CreativeListArticles(c, id)
err = s.dao.CreativeListDel(c, id)
if err != nil {
return
}
err = s.dao.CreativeListDelAllArticles(c, id)
// del list cache/articles cache/article list cache
cache.Save(func() {
c := context.TODO()
var aids []int64
for _, a := range arts {
aids = append(aids, a.ID)
}
s.deleteArtsListCache(c, aids...)
s.deleteListArtsCache(c, id)
s.deleteListCache(c, id)
s.dao.RebuildUpListsCache(context.TODO(), mid)
})
return
}
// func (s *Service) novelCIDs() (cids []int64) {
// for _, a := range s.categoriesReverseMap[_novel] {
// cids = append(cids, a.ID)
// }
// return
// }
func (s *Service) creativeNotAddListArticles(c context.Context, mid int64) (res []*model.ListArtMeta, err error) {
// cids := s.novelCIDs()
arts, err := s.dao.CreativeCategoryArticles(c, mid)
if err != nil {
return
}
lists, err := s.dao.CreativeUpLists(c, mid)
if err != nil {
return
}
var ids []int64
for _, l := range lists {
ids = append(ids, l.ID)
}
listsArts, err := s.dao.CreativeListsArticles(c, ids)
if err != nil {
return
}
exists := make(map[int64]bool)
for _, la := range listsArts {
for _, a := range la {
exists[a.ID] = true
}
}
for _, art := range arts {
if !exists[art.ID] {
res = append(res, art)
}
}
return
}
// CreativeCanAddArticles can added passed articles
func (s *Service) CreativeCanAddArticles(c context.Context, mid int64) (res []*model.ListArtMeta, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
arts, err := s.creativeNotAddListArticles(c, mid)
if err != nil {
return
}
for _, art := range arts {
if art.IsNormal() {
res = append(res, art)
}
}
sort.Slice(res, func(i, j int) bool {
return res[i].PublishTime > res[j].PublishTime
})
return
}
// CreativeListAllArticles get read list articles
func (s *Service) CreativeListAllArticles(c context.Context, mid, id int64) (list *model.List, arts []*model.ListArtMeta, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
list, err = s.checkList(c, mid, id)
if err != nil {
return
}
list.Read, _ = s.dao.CacheListReadCount(c, id)
arts, err = s.rawListArticles(c, id)
return
}
func (s *Service) checkList(c context.Context, mid, id int64) (list *model.List, err error) {
if id == 0 {
return
}
list, err = s.dao.RawList(c, id)
if err != nil {
return
}
if list == nil {
err = ecode.NothingFound
return
}
if list.Mid != mid {
err = ecode.ArtCreationMIDErr
return
}
return
}
// CreativeUpdateListArticles update list articles
func (s *Service) CreativeUpdateListArticles(c context.Context, listID int64, name, imageURL, summary string, onlyList bool, mid int64, aids []int64) (list *model.List, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
list, err = s.checkList(c, mid, listID)
if err != nil {
return
}
list, err = s.CreativeUpdateList(c, listID, name, imageURL, summary, list.PublishTime, list.Words)
if err != nil {
return
}
if onlyList {
cache.Save(func() {
s.updateListCache(c, listID)
})
return
}
if len(aids) > s.c.Article.ListArtsLimit {
err = ecode.ArtAddListLimitErr
return
}
existArts, err := s.dao.CreativeListArticles(c, listID)
if err != nil {
return
}
existsMap := make(map[int64]bool)
for _, a := range existArts {
existsMap[a.ID] = true
}
// 过滤非自己的文章
metas, err := s.CreativeCanAddArticles(c, mid)
if err != nil {
return
}
metasMap := make(map[int64]*model.ListArtMeta)
for _, m := range metas {
metasMap[m.ID] = m
}
var newAids []int64
for _, aid := range aids {
if existsMap[aid] || (metasMap[aid] != nil) {
newAids = append(newAids, aid)
}
}
aids = newAids
// 计算排序
updated, deleted := calculateListArtPosition(existArts, aids)
log.Info("creative: update list update(%v) deleted(%v)", len(updated), len(deleted))
var tx *sql.Tx
if tx, err = s.dao.BeginTran(c); err != nil {
log.Error("tx.BeginTran() error(%+v)", err)
return
}
defer func() {
if err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("tx.Rollback() error(%+v)", err1)
}
return
}
if err = tx.Commit(); err != nil {
dao.PromError("creative:修改文集")
log.Error("tx.Commit() error(%+v)", err)
return
}
err = s.updateListInfo(c, listID)
cache.Save(func() {
s.dao.CreativeListUpdateTime(context.TODO(), listID, time.Now())
s.RebuildListCache(context.TODO(), listID)
s.deleteArtsListCache(context.TODO(), deleted...)
})
}()
for _, id := range deleted {
if err = s.dao.TxDelListArticle(c, tx, listID, id); err != nil {
return
}
}
for _, a := range updated {
if err = s.dao.TxAddListArticle(c, tx, listID, a.ID, a.Position); err != nil {
return
}
}
return
}
func calculateListArtPosition(src []*model.ListArtMeta, dest []int64) (update []*model.ListArtMeta, delete []int64) {
srcMap := make(map[int64]*model.ListArtMeta)
for _, m := range src {
srcMap[m.ID] = m
}
destMap := make(map[int64]bool)
for _, id := range dest {
destMap[id] = true
}
var newSrc []*model.ListArtMeta
for _, m := range src {
if !destMap[m.ID] {
delete = append(delete, m.ID)
continue
}
newSrc = append(newSrc, m)
}
if len(newSrc) == len(dest) {
equal := true
for i, d := range dest {
if newSrc[i].ID != d {
equal = false
break
}
}
if equal {
return
}
}
if len(dest) == 0 {
return
}
for i, id := range dest {
pos := (i + 1) * _positionStep
if (srcMap[id] != nil) && (srcMap[id].Position == pos) {
continue
}
update = append(update, &model.ListArtMeta{ID: id, Position: pos})
}
return
}
// CreativeUpdateList update list
func (s *Service) CreativeUpdateList(c context.Context, id int64, name, imageURL, summary string, publishTime xtime.Time, words int64) (res *model.List, err error) {
var newName string
newName, _ = s.filter(c, name)
if newName != name {
log.Infov(c, log.KV("log", "filter title"), log.KV("old", name), log.KV("new", newName))
}
name = newName
var ok bool
if name, ok = s.checkTitle(name); !ok || name == "" {
log.Errorv(c, log.KV("log", "CreativeUpdateList"), log.KV("name", name), log.KV("id", id))
err = ecode.ArtListNameErr
return
}
if summary != "" {
summary, _ = s.filter(c, summary)
}
if err = s.dao.CreativeListUpdate(c, id, name, imageURL, summary, publishTime, words); err != nil {
return
}
res, err = s.dao.RawList(c, id)
if err != nil {
return
}
cache.Save(func() {
s.dao.AddCacheList(context.TODO(), res.ID, res)
})
return
}
// creativeAddArticleList set article list
func (s *Service) creativeAddArticleList(c context.Context, mid, listID, articleID int64, onlyPass bool) (err error) {
if listID == 0 {
return
}
log.Infov(c, log.KV("log", "creativeSetArticleList"), log.KV("list_id", listID), log.KV("article_id", articleID), log.KV("mid", mid))
defer func() {
if err != nil {
log.Errorv(c, log.KV("log", "creativeSetArticleList"), log.KV("list_id", listID), log.KV("article_id", articleID), log.KV("mid", mid), log.KV("err", err))
}
}()
if _, err = s.checkList(c, mid, listID); err != nil {
return
}
var can bool
if can, err = s.checkArticleCanAddList(c, mid, articleID, onlyPass); (err != nil) || !can {
log.Errorv(c, log.KV("log", "checkArticleCanAddList"), log.KV("mid", mid), log.KV("aid", articleID), log.KV("err", err), log.KV("error", err))
err = ecode.ArtArtAddListErr
return
}
arts, _ := s.dao.CreativeListArticles(c, listID)
if len(arts) >= s.c.Article.ListArtsLimit {
err = ecode.ArtAddListLimitErr
return
}
position := _positionStep
if len(arts) > 0 {
position = arts[len(arts)-1].Position + _positionStep
}
err = s.dao.AddListArticle(c, listID, articleID, position)
if err != nil {
return
}
err = s.updateListInfo(c, listID)
if err != nil {
return
}
cache.Save(func() {
s.dao.CreativeListUpdateTime(context.TODO(), listID, time.Now())
// only update passed art
s.RebuildListCache(context.TODO(), listID)
})
return
}
// CreativeUpdateArticleList update article list
func (s *Service) CreativeUpdateArticleList(c context.Context, mid, aid, listID int64, onlyPass bool) (err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
if listID > 0 {
_, err = s.checkList(c, mid, listID)
if err != nil {
return
}
}
// check article
meta, err := s.dao.AllArticleMeta(c, aid)
if err != nil {
return
}
if meta == nil {
err = ecode.NothingFound
return
}
if meta.Author.Mid != mid {
err = ecode.ArtCreationMIDErr
return
}
// get article list
lists, err := s.dao.RawArtsListID(c, []int64{aid})
if err != nil {
return
}
oldListID := lists[aid]
if oldListID == listID {
return
}
defer func() {
if err == nil {
err = s.dao.CreativeListUpdateTime(c, listID, time.Now())
}
}()
s.deleteArtsListCache(c, aid)
if oldListID > 0 {
err = s.dao.DelListArticle(c, oldListID, aid)
if err != nil {
return
}
err = s.updateListInfo(c, oldListID)
if err != nil {
return
}
cache.Save(func() {
s.RebuildListCache(context.TODO(), oldListID)
})
}
if listID > 0 {
err = s.creativeAddArticleList(c, mid, listID, aid, onlyPass)
}
return
}
func (s *Service) checkArticleCanAddList(c context.Context, mid, aid int64, onlyPass bool) (res bool, err error) {
metas, err := s.creativeNotAddListArticles(c, mid)
if err != nil {
log.Errorv(c, log.KV("log", "checkArticleCanAddList"), log.KV("mid", mid), log.KV("aid", aid), log.KV("err", err))
return
}
metasMap := make(map[int64]*model.ListArtMeta)
for _, m := range metas {
metasMap[m.ID] = m
}
if onlyPass {
res = (metasMap[aid] != nil) && metasMap[aid].IsNormal()
return
}
res = metasMap[aid] != nil
return
}
// updateListInfo update list words and publish_time
func (s *Service) updateListInfo(c context.Context, id int64) (err error) {
list, err := s.dao.RawList(c, id)
if err != nil {
return
}
if list == nil {
err = ecode.NothingFound
return
}
metas, err := s.dao.RawListArts(c, id)
if err != nil {
return
}
var words, pt int64
for _, meta := range metas {
if meta.IsNormal() {
words += meta.Words
if int64(meta.PublishTime) > pt {
pt = int64(meta.PublishTime)
}
}
}
err = s.dao.CreativeListUpdate(c, id, list.Name, list.ImageURL, list.Summary, xtime.Time(pt), words)
return
}
// RefreshList refresh list info and cache use in api
func (s *Service) RefreshList(c context.Context, id int64) (err error) {
if err = s.updateListInfo(c, id); err != nil {
return
}
return s.RebuildListCache(c, id)
}

View File

@@ -0,0 +1,123 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/model"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_CreativeListAllArticles(t *testing.T) {
Convey("get articles", t, WithCleanCache(func() {
list, res, err := s.CreativeListAllArticles(context.TODO(), 100, 5)
So(err, ShouldBeNil)
So(list, ShouldNotBeEmpty)
So(res, ShouldNotBeEmpty)
}))
}
func Test_CreativeCanAddArticles(t *testing.T) {
Convey("get articles", t, WithCleanCache(func() {
res, err := s.CreativeCanAddArticles(context.TODO(), 88888929)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
}))
}
func Test_calculateListArtPosition(t *testing.T) {
a := &model.ListArtMeta{ID: 1, Position: 1000}
b := &model.ListArtMeta{ID: 2, Position: 2000}
c := &model.ListArtMeta{ID: 3, Position: 3000}
d := &model.ListArtMeta{ID: 4, Position: 4000}
// a b c d => d a b c update d 移动1
// a b c d => c d b a update a b 移动2
// a b c => a b c d update d 末尾增加1
// a b c => a d b c update d 中间增加1
// a b c => d a b c 前面增加1
// [] => a b 新增加2
// a b c d => a b remove 2
// 可以考虑对删一 加一 移1 进行特别优化 其他全修改
Convey("a b c => a b c d", t, func() {
update, delete := calculateListArtPosition([]*model.ListArtMeta{a, b, c}, []int64{a.ID, b.ID, c.ID, d.ID})
So(delete, ShouldBeNil)
So(update, ShouldResemble, []*model.ListArtMeta{
&model.ListArtMeta{ID: d.ID, Position: 4000},
})
})
Convey("[] => a b", t, func() {
update, delete := calculateListArtPosition(nil, []int64{a.ID, b.ID})
So(delete, ShouldBeNil)
So(update, ShouldResemble, []*model.ListArtMeta{a, b})
})
Convey("a b c d => a b c ", t, func() {
update, delete := calculateListArtPosition([]*model.ListArtMeta{a, b, c, d}, []int64{a.ID, b.ID, c.ID})
So(delete, ShouldResemble, []int64{d.ID})
So(update, ShouldBeNil)
})
Convey("a b c d => d a b c", t, func() {
update, delete := calculateListArtPosition([]*model.ListArtMeta{a, b, c, d}, []int64{d.ID, a.ID, b.ID, c.ID})
So(delete, ShouldBeNil)
So(update, ShouldResemble, []*model.ListArtMeta{
&model.ListArtMeta{ID: d.ID, Position: 1000},
&model.ListArtMeta{ID: a.ID, Position: 2000},
&model.ListArtMeta{ID: b.ID, Position: 3000},
&model.ListArtMeta{ID: c.ID, Position: 4000},
})
})
}
func Test_CreativeUpdateListArticles(t *testing.T) {
Convey("update articles", t, WithCleanCache(func() {
mid := int64(88888929)
aids := []int64{929, 830, 1000}
list, err := s.CreativeUpdateListArticles(context.TODO(), 8, "newName", "", "summary", false, mid, aids)
So(err, ShouldBeNil)
list, _ = s.dao.RawList(context.TODO(), 8)
metas, _ := s.dao.CreativeListArticles(context.TODO(), 8)
So(list.Name, ShouldEqual, "newName")
So(len(metas), ShouldEqual, 2)
So(metas[0].ID, ShouldEqual, 929)
So(metas[1].ID, ShouldEqual, 830)
}))
}
func Test_CreativeUpdateArticleList(t *testing.T) {
c := context.TODO()
aid := int64(821)
listid := int64(8)
mid := int64(88888929)
Convey("set list", t, WithCleanCache(func() {
err := s.CreativeUpdateArticleList(c, mid, aid, listid, false)
So(err, ShouldBeNil)
arts, err := s.dao.RawArtsListID(c, []int64{aid})
So(err, ShouldBeNil)
So(arts[aid], ShouldEqual, listid)
// time.Sleep(time.Second)
// r, err := s.dao.ArticlesListCache(c, []int64{aid})
// So(err, ShouldBeNil)
// So(r[aid], ShouldEqual, listid)
// r2, err := s.dao.ListArtsCacheMap(c, listid)
// So(err, ShouldBeNil)
// So(r2[aid], ShouldNotBeEmpty)
Convey("remove", func() {
err := s.CreativeUpdateArticleList(c, mid, aid, 0, false)
So(err, ShouldBeNil)
arts, err := s.dao.RawArtsListID(c, []int64{aid})
So(err, ShouldBeNil)
So(arts[aid], ShouldEqual, 0)
})
Convey("update", func() {
err := s.CreativeUpdateArticleList(c, mid, aid, 17, false)
So(err, ShouldBeNil)
arts, err := s.dao.RawArtsListID(c, []int64{aid})
So(err, ShouldBeNil)
So(arts[aid], ShouldEqual, 17)
})
}))
}

View File

@@ -0,0 +1,134 @@
package service
import (
"context"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
)
func (s Service) checkDraftAuthor(c context.Context, aid, mid int64) (d *artmdl.Draft, err error) {
if d, err = s.dao.ArtDraft(c, mid, aid); err != nil {
return
}
if d == nil {
err = ecode.NothingFound
return
}
if d.Author.Mid != mid {
err = ecode.ArtCreationMIDErr
}
return
}
// ArtDraft get article draft by id.
func (s *Service) ArtDraft(c context.Context, aid, mid int64) (res *artmdl.Draft, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
if res, err = s.checkDraftAuthor(c, aid, mid); err != nil {
return
}
if res.ListID > 0 {
res.List, _ = s.dao.List(c, res.ListID)
}
return
}
// AddArtDraft add article draft .
func (s *Service) AddArtDraft(c context.Context, a *artmdl.Draft) (id int64, err error) {
log.Infov(c, log.KV("AddArtDraft", a))
if err = s.checkPrivilege(c, a.Author.Mid); err != nil {
return
}
a.Content = xssFilter(a.Content)
if err = s.preDraftCheck(c, a); err != nil {
return
}
if _, err = s.checkList(c, a.Author.Mid, a.ListID); err != nil {
return
}
// if a.ListID > 0 {
// var exist bool
// for _, cid := range s.novelCIDs() {
// if cid == a.Category.ID {
// exist = true
// break
// }
// }
// if !exist {
// a.ListID = 0
// }
// }
if a.ID > 0 {
if _, err = s.checkDraftAuthor(c, a.ID, a.Author.Mid); err != nil {
return
}
_, err = s.dao.AddArtDraft(c, a)
id = a.ID
log.Info("update draft success mid(%d) aid(%d)", a.Author.Mid, a.ID)
return
}
var total int
if total, err = s.dao.CountUpperDraft(c, a.Author.Mid); err != nil {
return
}
if total > s.c.Article.UpperDraftLimit {
err = ecode.ArtCreationDraftFull
return
}
id, err = s.dao.AddArtDraft(c, a)
return
}
// DelArtDraft deletes article draft.
func (s *Service) DelArtDraft(c context.Context, aid, mid int64) (err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
if _, err = s.checkDraftAuthor(c, aid, mid); err != nil {
return
}
err = s.dao.DelArtDraft(c, mid, aid)
return
}
// UpperDrafts batch get draft by mid.
func (s *Service) UpperDrafts(c context.Context, mid int64, pn, ps int) (res *artmdl.Drafts, err error) {
if err = s.checkPrivilege(c, mid); err != nil {
return
}
var (
total int
ds []*artmdl.Draft
start = (pn - 1) * ps
page = &artmdl.ArtPage{Pn: pn, Ps: ps}
)
res = &artmdl.Drafts{Page: page}
if total, err = s.dao.CountUpperDraft(c, mid); err != nil {
return
} else if total == 0 {
return
}
page.Total = total
if ds, err = s.dao.UpperDrafts(c, mid, start, ps); err != nil {
return
}
for _, v := range ds {
// 用户没选择分区,返回空分区信息
if v.Category.ID == 0 {
continue
}
var pid int64
if pid, err = s.CategoryToRoot(v.Category.ID); err != nil {
log.Error("s.CategoryToRoot(%d) error(%+v)", v.Category.ID, err)
err = nil
continue
}
v.Category = s.categoriesMap[pid]
v.List, _ = s.dao.List(c, v.ListID)
}
res.Drafts = ds
return
}

View File

@@ -0,0 +1,79 @@
package service
import (
"context"
"testing"
artmdl "go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
cats = []*artmdl.Category{
&artmdl.Category{Name: "游戏", ID: 1},
&artmdl.Category{Name: "动漫", ID: 2},
}
draft = artmdl.Draft{
Article: &artmdl.Article{
Meta: &artmdl.Meta{
Category: cats[0],
Title: "隐藏于时区记忆中的,是希望还是绝望!",
Summary: "说起日本校服,第一个浮现在我们脑海中的必然是那象征着青春阳光 蓝白色相称的水手服啦. 拉色短裙配上洁白的直袜",
BannerURL: "http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg",
TemplateID: 1,
State: artmdl.StatePending,
Author: &artmdl.Author{Mid: 8167601, Name: "爱蜜莉雅", Face: "http://i1.hdslb.com/bfs/face/5c6109964e78a84021299cdf71739e21cd7bc208.jpg"},
Reprint: 0,
ImageURLs: []string{"http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg", "http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg", "http://i2.hdslb.com/bfs/archive/b5727f244d5c7a34c1c0e78f49765d09ff30c129.jpg"},
PublishTime: 1495784507,
Stats: &artmdl.Stats{Favorite: 100, Like: 10, View: 500, Dislike: 1, Share: 99},
},
Content: "test content",
},
Tags: []string{"tag1", "tag2"},
}
)
func Test_Draft(t *testing.T) {
var (
err error
aid int64
c = context.TODO()
)
Convey("creation draft", t, WithService(func(s *Service) {
Convey("AddArtDraft", func() {
aid, err = s.AddArtDraft(c, &draft)
t.Logf("aid: %d", aid)
So(err, ShouldBeNil)
So(aid, ShouldBeGreaterThan, 0)
// t.Logf("result: %+v", aid)
Convey("ArtDraft", func() {
res, err := s.ArtDraft(c, aid, draft.Author.Mid)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
t.Logf("result: %+v", res.Title)
// t.Logf("result: %+v", res.Content)
Convey("DelArtDraft", func() {
err = s.DelArtDraft(c, aid, draft.Author.Mid)
So(err, ShouldBeNil)
})
})
})
Convey("UpperDrafts", func() {
mid := art.Author.Mid
pn := 1
ps := 10
res2, err := s.UpperDrafts(c, mid, pn, ps)
So(err, ShouldBeNil)
So(res2, ShouldNotBeNil)
// fmt.Println("res2", res2.Page)
// fmt.Println("res2", len(res2.Drafts))
// fmt.Printf("meta %+v:", res2.Drafts[0].Meta)
// fmt.Printf("category %+v", res2.Drafts[0].Category)
})
}))
}

View File

@@ -0,0 +1,125 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
favmdl "go-common/app/service/main/favorite/model"
"go-common/library/log"
)
// AddFavorite add article favorite .
func (s *Service) AddFavorite(c context.Context, mid, aid, fid int64, ip string) (err error) {
if err = s.checkArticle(c, aid); err != nil {
return
}
arg := &favmdl.ArgAdd{Type: favmdl.Article, Mid: mid, Oid: aid, Fid: fid}
if err = s.favRPC.Add(c, arg); err != nil {
dao.PromError("rpc:添加收藏")
log.Error("s.favRPC.Add(%+v) error(%+v)", arg, err)
}
return
}
// DelFavorite del article favorite .
func (s *Service) DelFavorite(c context.Context, mid, aid, fid int64, ip string) (err error) {
arg := &favmdl.ArgDel{Type: favmdl.Article, Mid: mid, Oid: aid, Fid: fid}
if err = s.favRPC.Del(c, arg); err != nil {
dao.PromError("rpc:删除收藏")
log.Error("s.favRPC.Del(%+v) error(%+v)", arg, err)
}
return
}
// Favorites article favorites.
func (s *Service) Favorites(c context.Context, mid, fid int64, pn, ps int, ip string) (favs *favmdl.Favorites, err error) {
arg := &favmdl.ArgFavs{Type: favmdl.Article, Mid: mid, Fid: fid, Pn: pn, Ps: ps}
if favs, err = s.favRPC.Favorites(c, arg); err != nil {
dao.PromError("rpc:获取收藏列表")
log.Error("s.favRPC.Favorites(%+v) error(%+v)", arg, err)
}
return
}
// IsFav return user is fav article
func (s *Service) IsFav(c context.Context, mid, aid int64) (res bool, err error) {
arg := &favmdl.ArgIsFav{Type: favmdl.Article, Mid: mid, Oid: aid}
if res, err = s.favRPC.IsFav(c, arg); err != nil {
dao.PromError("rpc:是否已收藏")
log.Error("s.favRPC.IsFav(%+v) error(%+v)", arg, err)
}
return
}
// Favs gets user favorite article list.
func (s *Service) Favs(c context.Context, mid, fid int64, pn, ps int, ip string) (favs []*artmdl.Favorite, page *artmdl.Page, err error) {
var (
a *artmdl.Meta
ok bool
fs *favmdl.Favorites
aids []int64
as = make(map[int64]*artmdl.Meta)
ts = make(map[int64]int64)
)
favs = make([]*artmdl.Favorite, 0)
if fs, err = s.Favorites(c, mid, fid, pn, ps, ip); err != nil {
return
}
page = &artmdl.Page{
Pn: fs.Page.Num,
Ps: fs.Page.Size,
Total: fs.Page.Count,
}
if len(fs.List) == 0 {
return
}
aids = make([]int64, 0, len(fs.List))
for _, v := range fs.List {
aids = append(aids, v.Oid)
ts[v.Oid] = v.MTime.Time().Unix()
}
if as, err = s.ArticleMetas(c, aids); err != nil {
return
}
for _, aid := range aids {
var (
valid = true
meta *artmdl.Meta
)
if a, ok = as[aid]; !ok {
meta = &artmdl.Meta{ID: aid}
valid = false
} else {
meta = a
}
favs = append(favs, &artmdl.Favorite{
Meta: meta,
FavoriteTime: ts[aid],
Valid: valid,
})
}
return
}
// ValidFavs get valid favorites
func (s *Service) ValidFavs(c context.Context, mid, fid int64, pn, ps int, ip string) (res []*artmdl.Favorite, page *artmdl.Page, err error) {
defer func() {
if res == nil {
res = make([]*artmdl.Favorite, 0)
}
}()
var favs []*artmdl.Favorite
if favs, page, err = s.Favs(c, mid, fid, pn, ps, ip); err != nil {
return
}
for _, fav := range favs {
if fav.Valid {
res = append(res, fav)
}
}
if page.Total <= ps {
page.Total -= (len(favs) - len(res))
}
return
}

View File

@@ -0,0 +1,37 @@
package service
// import (
// "context"
// "testing"
// . "github.com/smartystreets/goconvey/convey"
// )
// func Test_Favorite(t *testing.T) {
// var (
// aid = int64(9999999)
// mid = int64(88888929)
// fid = int64(0)
// pn = 1
// ps = 10
// )
// Convey("AddFavorite", t, WithService(func(s *Service) {
// err := s.AddFavorite(context.TODO(), mid, aid, fid, "")
// // 11201 means you have added it before.
// So(err, ShouldBeNil)
// Convey("Favorites", func() {
// res, page, err := s.Favs(context.TODO(), mid, fid, pn, ps, "")
// So(err, ShouldBeNil)
// So(res, ShouldNotBeEmpty)
// So(page, ShouldNotBeEmpty)
// // t.Logf("result: %+v", res)
// // t.Logf("page: %+v", page)
// Convey("DelFavorite", func() {
// err := s.DelFavorite(context.TODO(), mid, aid, fid, "")
// So(err, ShouldBeNil)
// })
// })
// }))
// }

View File

@@ -0,0 +1,73 @@
package service
import (
"context"
"fmt"
"time"
hismdl "go-common/app/interface/main/history/model"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
)
const _historyArtType = 5
const _historyListType = 7
// AddHistory .
func (s *Service) AddHistory(c context.Context, mid, cvid int64, listID int64, ip string, plat int8, from string) (err error) {
if from == "articleSlide" {
return
}
var client, typ int8
client = model.HistoryClient(plat)
var aid, cid int64
if listID != 0 {
typ = _historyListType
aid = listID
cid = cvid
} else {
typ = _historyArtType
aid = cvid
}
arg := &hismdl.ArgHistory{
Mid: mid,
RealIP: ip,
History: &hismdl.History{
Mid: mid,
Aid: aid,
TP: typ,
Cid: cid,
DT: client,
Unix: time.Now().Unix(),
},
}
if err = s.hisRPC.Add(c, arg); err != nil {
dao.PromError("rpc:添加历史记录")
log.Error("s.historyRPC.Add(%+v +v) error(%v)", arg, arg.History, err)
}
return
}
func (s *Service) historyPosition(c context.Context, mid, listID int64) (cvid int64, err error) {
arg := &hismdl.ArgPos{
Mid: mid,
Aid: listID,
TP: _historyListType,
}
history, err := s.hisRPC.Position(c, arg)
if err != nil {
if ecode.NothingFound.Equal(err) {
dao.PromError("history:获取历史记录")
log.Warnv(c, log.KV("log", fmt.Sprintf("s.historyRPC.Position(%+v) error(%v)", arg, err)))
} else {
dao.PromError("history:获取历史记录")
log.Errorv(c, log.KV("log", fmt.Sprintf("s.historyRPC.Position(%+v) error(%v)", arg, err)))
}
}
if history != nil {
cvid = history.Cid
}
return
}

View File

@@ -0,0 +1,300 @@
package service
import (
"context"
"time"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
)
var _hotspotArtTime = time.Hour * 24 * 30
// UpdateHotspots update all hotspots
func (s *Service) UpdateHotspots(force bool) (err error) {
var c = context.TODO()
hotspots, err := s.dao.Hotspots(c)
if err != nil {
return
}
if len(hotspots) == 0 {
err = s.dao.DelCacheHotspots(c)
return
}
for _, hot := range hotspots {
if err = s.genHotspot(c, hot, force); err != nil {
dao.PromError("hotspots:生成")
log.Error("hotspots s.genHotspot(%v, %v) err:%v", hot.Tag, force, err)
return
}
}
err = s.dao.AddCacheHotspots(c, hotspots)
for _, h := range hotspots {
s.dao.AddCacheHotspot(c, h.ID, h)
}
return
}
func (s *Service) genHotspot(c context.Context, hot *model.Hotspot, force bool) (err error) {
var ok bool
for _, typ := range model.HotspotTypes {
ok, err = s.dao.ExpireHotspotArtsCache(c, typ, hot.ID)
if err != nil {
return
}
if !ok || force {
break
}
}
if ok && !force {
//不重新生成 赋值计数 返回
if s, _ := s.dao.CacheHotspot(c, hot.ID); s != nil {
hot.Stats = s.Stats
}
return
}
ptime := time.Now().Add(-_hotspotArtTime)
arts, err := s.dao.SearchArts(c, ptime.Unix())
if err != nil {
return
}
// filter tags && remove top arts
tops := make(map[int64]bool)
for _, x := range hot.TopArticles {
tops[x] = true
}
var newArts []*model.SearchArt
for _, art := range arts {
for _, t := range art.Tags {
if t == hot.Tag {
hot.Stats.Read += art.StatsView
hot.Stats.Reply += art.StatsReply
if !tops[art.ID] {
newArts = append(newArts, art)
}
break
}
}
}
arts = newArts
if len(arts) == 0 {
return
}
// add cache
for _, typ := range model.HotspotTypes {
var as [][2]int64
for _, art := range arts {
as = append(as, hotspotValue(typ, art))
}
if err = s.dao.AddCacheHotspotArts(c, typ, hot.ID, as, true); err != nil {
return
}
}
return
}
func hotspotValue(typ int8, art *model.SearchArt) [2]int64 {
switch typ {
case model.HotspotTypeView:
return [2]int64{art.ID, art.StatsView}
case model.HotspotTypePtime:
return [2]int64{art.ID, art.PublishTime}
}
return [2]int64{0, 0}
}
// AddCacheHotspotArt check article in hotspots list and add cache
func (s *Service) AddCacheHotspotArt(c context.Context, art *model.SearchArt) (err error) {
if art.PublishTime < time.Now().Add(-_hotspotArtTime).Unix() {
return
}
hots, all, err := s.tagsHotspots(c, art.Tags)
if err != nil {
dao.PromError("hotspots:AddCacheHotspotArt")
return
}
if len(hots) == 0 {
return
}
for _, hot := range hots {
hot.Stats.Read += art.StatsView
hot.Stats.Reply += art.StatsReply
if err = s.addCacheHotspotArt(c, hot.ID, art); err != nil {
dao.PromError("hotspots:addCacheHotspotArt")
return
}
}
err = s.dao.AddCacheHotspots(c, all)
return
}
// DelCacheHotspotArt delete art from hotspots
func (s *Service) DelCacheHotspotArt(c context.Context, aid int64) (err error) {
hots, err := s.dao.CacheHotspots(c)
if err != nil {
dao.PromError("hotspots:DelCacheHotspotArt")
return
}
for _, hot := range hots {
for _, typ := range model.HotspotTypes {
if err = s.dao.DelHotspotArtsCache(c, typ, hot.ID, aid); err != nil {
return
}
}
}
return
}
// tagsHotspots get hotspots form tags
func (s *Service) tagsHotspots(c context.Context, tags []string) (res, all []*model.Hotspot, err error) {
all, err = s.dao.CacheHotspots(c)
if err != nil {
dao.PromError("hotspots:tagsHotspots")
return
}
for _, hot := range all {
for _, t := range tags {
if t == hot.Tag {
res = append(res, hot)
break
}
}
}
return
}
func (s *Service) addCacheHotspotArt(c context.Context, hotID int64, art *model.SearchArt) (err error) {
for _, typ := range model.HotspotTypes {
var ok bool
if ok, err = s.dao.ExpireHotspotArtsCache(c, typ, hotID); err != nil {
return
}
if ok {
if err = s.dao.AddCacheHotspotArts(c, typ, hotID, [][2]int64{hotspotValue(typ, art)}, false); err != nil {
return
}
}
}
return
}
func (s *Service) metaToSearch(c context.Context, m *model.Meta) (res *model.SearchArt) {
if m == nil {
return
}
res = &model.SearchArt{
ID: m.ID,
PublishTime: int64(m.PublishTime),
}
for _, t := range m.Tags {
res.Tags = append(res.Tags, t.Name)
}
stats, _ := s.stat(c, m.ID)
if stats != nil {
res.StatsView = stats.View
res.StatsReply = stats.Reply
}
return
}
// HotspotArts get hotspot articles
func (s *Service) HotspotArts(c context.Context, id int64, pn, ps int, lastAids []int64, sort int8, mid int64) (hotspot *model.Hotspot, res []*model.MetaWithLike, err error) {
if pn <= 0 {
pn = 1
}
var (
start = (pn - 1) * ps
// 多取一些用于去重
end = start + ps - 1 + len(lastAids)
allIDs []int64
metas map[int64]*model.Meta
aidsm map[int64]bool
withRecommend bool
)
if hotspot, err = s.dao.CacheHotspot(c, id); err != nil {
return
}
if hotspot == nil {
err = ecode.NothingFound
return
}
hotspot.Stats.Count, _ = s.dao.HotspotArtsCacheCount(c, sort, id)
if sort == model.HotspotTypeView {
withRecommend = true
}
recommendsLen := len(hotspot.TopArticles)
// 只是最新文章 无推荐
if (start >= recommendsLen) || !withRecommend {
var (
nids []int64
newArtStart = start
newArtEnd = end
)
if withRecommend {
newArtStart = start - recommendsLen
newArtEnd = end - recommendsLen
}
nids, _ = s.dao.HotspotArtsCache(c, sort, id, newArtStart, newArtEnd)
if withRecommend {
allIDs = uniqIDs(nids, hotspot.TopArticles)
} else {
allIDs = nids
}
} else {
if end < recommendsLen {
allIDs = hotspot.TopArticles[start : end+1]
} else {
// 混合推荐和最新文章
var (
nids []int64
rs = hotspot.TopArticles[start:]
)
newArtStart := 0
newArtEnd := (end - start) - len(rs)
nids, _ = s.dao.HotspotArtsCache(c, sort, id, newArtStart, newArtEnd)
nids = uniqIDs(nids, hotspot.TopArticles)
allIDs = append(rs, nids...)
}
}
if len(allIDs) == 0 {
return
}
if metas, err = s.ArticleMetas(c, allIDs); err != nil {
return
}
//过滤禁止显示的稿件
filterNoDistributeArtsMap(metas)
filterNoRegionArts(metas)
//填充数据
aidsm = make(map[int64]bool, len(lastAids))
for _, aid := range lastAids {
aidsm[aid] = true
}
for _, id := range allIDs {
if (metas == nil) || (metas[id] == nil) || aidsm[id] {
continue
}
art := &model.MetaWithLike{Meta: *metas[id]}
res = append(res, art)
}
//截断分页数据
if ps > len(res) {
ps = len(res)
}
res = res[:ps]
// fill like state
aids := []int64{}
for _, m := range res {
aids = append(aids, m.ID)
}
states, _ := s.HadLikesByMid(c, mid, aids)
if states == nil {
return
}
for _, m := range res {
m.LikeState = int(states[m.ID])
}
return
}

View File

@@ -0,0 +1,37 @@
package service
import (
"testing"
"time"
"go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Hotsports(t *testing.T) {
_hotspotArtTime = time.Hour * 24 * 365 * 10
Convey("gen data", t, func() {
err := s.UpdateHotspots(true)
So(err, ShouldBeNil)
res, err := s.dao.CacheHotspots(c)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
hot := res[0]
Convey("add art", func() {
err = s.AddCacheHotspotArt(c, &model.SearchArt{StatsView: 100, ID: 100, Tags: []string{hot.Tag}, PublishTime: time.Now().Unix()})
So(err, ShouldBeNil)
var hot2 *model.Hotspot
hot2, err = s.dao.CacheHotspot(c, hot.ID)
So(hot2.Stats.Read-hot.Stats.Read, ShouldEqual, 100)
})
Convey("get art", func() {
var arts []*model.MetaWithLike
var hotspot *model.Hotspot
hotspot, arts, err = s.HotspotArts(c, hot.ID, 1, 100, nil, 0, 0)
So(err, ShouldBeNil)
So(arts, ShouldNotBeEmpty)
So(hotspot, ShouldNotBeEmpty)
})
})
}

View File

@@ -0,0 +1,204 @@
package service
import (
"strconv"
"time"
"encoding/json"
"go-common/app/interface/openplatform/article/model"
"go-common/library/log"
binfoc "go-common/library/log/infoc"
"go-common/library/stat/prom"
)
type displayInfo struct {
ip string
mid string
now string
client string
build string
buvid string
pagetype string
pageNo string
isRec string
showlist json.RawMessage
}
type clickInfo struct {
mid string
client string
build string
buvid string
now string
from string
itemID string
itemType string
extra json.RawMessage
}
type aiClickInfo struct {
mid string
client string
build string
buvid string
time string
from string
itemID string
itemType string
actionID string
action string
extra json.RawMessage
}
// 用户在列表页停留上报
type showInfo struct {
ip string
time string
buvid string
mid string
client string
pageType string
from string
build string
extra string
}
type recItem struct {
ID int64 `json:"id"`
Page int64 `json:"page"`
Pos int64 `json:"pos"`
View int64 `json:"view"`
Fav int64 `json:"fav"`
Like int64 `json:"like"`
Reply int64 `json:"reply"`
Share int64 `json:"share"`
AvFeature string `json:"av_feature,omitempty"`
UserFeature string `json:"user_feature,omitempty"`
}
// RecommendInfoc .
func (s *Service) RecommendInfoc(mid int64, plat int8, pageType, cid, build int, buvid, ip string, metas []*model.Meta, isRcmd bool, now time.Time, pn int64, sky *model.SkyHorseResp) {
var isRc = "0"
if isRcmd {
isRc = "1"
}
skyMap := make(map[int64]string)
if sky != nil {
for _, item := range sky.Data {
skyMap[item.ID] = item.AvFeature
}
}
var list []*recItem
for i, m := range metas {
x := &recItem{ID: m.ID, Page: pn, Pos: int64(i + 1), View: m.Stats.View, Fav: m.Stats.Favorite, Like: m.Stats.Like, Reply: m.Stats.Reply, Share: m.Stats.Share}
if sky != nil && sky.UserFeature != "" {
x.UserFeature = sky.UserFeature
}
x.AvFeature = skyMap[m.ID]
list = append(list, x)
}
var sl = &struct {
List []*recItem `json:"itemlist"`
}{
List: list,
}
msg, _ := json.Marshal(sl)
s.infoc(displayInfo{ip, strconv.FormatInt(mid, 10), strconv.FormatInt(now.Unix(), 10), strconv.Itoa(int(plat)), strconv.Itoa(build), buvid, strconv.Itoa(pageType), strconv.Itoa(cid), isRc, msg})
}
// ViewInfoc .
func (s *Service) ViewInfoc(mid int64, plat int8, build int, itemType, from, buvid string, itemID int64, now time.Time, ua string) {
var extra = &struct {
UA string `json:"ua"`
}{
UA: ua,
}
msg, _ := json.Marshal(extra)
s.infoc(clickInfo{strconv.FormatInt(mid, 10), strconv.Itoa(int(plat)), strconv.Itoa(build), buvid, strconv.FormatInt(now.Unix(), 10), from, strconv.FormatInt(itemID, 10), itemType, msg})
}
// AIViewInfoc .
func (s *Service) AIViewInfoc(mid int64, plat int8, build int, itemType, from, buvid string, itemID int64, now time.Time, ua string) {
var extra = &struct {
UA string `json:"ua"`
}{
UA: ua,
}
msg, _ := json.Marshal(extra)
s.infoc(aiClickInfo{
mid: strconv.FormatInt(mid, 10),
client: model.Client(plat),
build: strconv.Itoa(build),
buvid: buvid,
time: strconv.FormatInt(now.Unix(), 10),
from: from,
itemID: strconv.FormatInt(itemID, 10),
itemType: itemType,
action: "click",
actionID: "",
extra: msg,
})
}
// ShowInfoc .
func (s *Service) ShowInfoc(ip string, now time.Time, buvid string, mid int64, client int8, pageType string, from string, build string, ua string, referer string) {
var extra = &struct {
UA string `json:"ua"`
Referer string `json:"referer"`
}{
UA: ua,
Referer: referer,
}
msg, _ := json.Marshal(extra)
s.infoc(showInfo{
ip: ip,
time: strconv.FormatInt(now.Unix(), 10),
buvid: buvid,
mid: strconv.FormatInt(mid, 10),
client: strconv.Itoa(int(client)),
pageType: pageType,
from: from,
build: build,
extra: string(msg),
})
}
func (s *Service) infoc(i interface{}) {
select {
case s.logCh <- i:
default:
log.Warn("infocproc chan full")
}
}
// writeInfoc
func (s *Service) infocproc() {
var (
displayInfoc = binfoc.New(s.c.DisplayInfoc)
clickInfoc = binfoc.New(s.c.ClickInfoc)
aiClickInfoc = binfoc.New(s.c.AIClickInfoc)
showInfoc = binfoc.New(s.c.ShowInfoc)
)
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 displayInfo:
displayInfoc.Info(l.ip, l.now, l.buvid, l.mid, l.client, l.pagetype, l.pageNo, string(l.showlist), l.isRec, l.build)
log.Info("infocproc displayInfo param(ip:%s,now:%s,buvid:%s,mid:%s,client:%s,pagetype:%spageno:%s,showlist:%s,isRec:%s,build:%s)", l.ip, l.now, l.buvid, l.mid, l.client, l.pagetype, l.pageNo, l.showlist, l.isRec, l.build)
case clickInfo:
clickInfoc.Info(l.from, l.now, l.buvid, l.mid, l.client, l.itemType, l.itemID, "", l.build)
log.Info("infocproc clickInfoc param(client:%s,buvid:%s,mid:%s,now:%s,from:%s,build:%s,itemID:%s,itemType:%s)", l.client, l.buvid, l.mid, l.now, l.from, l.build, l.itemID, l.itemType)
case aiClickInfo:
aiClickInfoc.Info(l.client, l.buvid, l.mid, l.time, l.from, l.build, l.itemID, l.itemType, l.action, l.actionID, string(l.extra))
log.Info("infocproc aiclickInfoc param(client:%s,buvid:%s,mid:%s,time:%s,from:%s,build:%s,itemID:%s,itemType:%s,action: %s, actionID: %s, extra: %s)", l.client, l.buvid, l.mid, l.time, l.from, l.build, l.itemID, l.itemType, l.action, l.actionID, string(l.extra))
case showInfo:
showInfoc.Info(l.ip, l.time, l.buvid, l.mid, l.client, l.pageType, l.from, l.build, l.extra)
log.Info("infocproc showInfoc param(%+v)", l)
}
}
}

View File

@@ -0,0 +1,76 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
thumbupmdl "go-common/app/service/main/thumbup/model"
"go-common/library/ecode"
"go-common/library/log"
)
func (s *Service) isLike(c context.Context, mid, aid int64) (res int8, err error) {
r, err := s.HadLikesByMid(c, mid, []int64{aid})
if err != nil {
return
}
res = r[aid]
return
}
// HadLikesByMid .
func (s *Service) HadLikesByMid(c context.Context, mid int64, aids []int64) (res map[int64]int8, err error) {
if mid == 0 || len(aids) == 0 {
return
}
arg := &thumbupmdl.ArgHasLike{Business: "article", MessageIDs: aids, Mid: mid}
res, err = s.thumbupRPC.HasLike(c, arg)
return
}
// Like like article
func (s *Service) Like(c context.Context, mid, aid int64, likeType int) (err error) {
var art *artmdl.Meta
if (likeType < 0) || (likeType > 4) {
err = ecode.RequestErr
return
}
if art, err = s.ArticleMeta(c, aid); err != nil || art == nil {
err = ecode.NothingFound
return
}
arg := &thumbupmdl.ArgLike{
Mid: mid,
UpMid: art.Author.Mid,
Business: "article",
MessageID: aid,
Type: int8(likeType),
}
if err = s.thumbupRPC.Like(c, arg); err != nil {
dao.PromError("like:thumbup-service")
log.Error("s.thumbupRPC.Like(%+v) err: %+v", arg, err)
}
return
}
// RecommendsWithLike recommends with like state
func (s *Service) RecommendsWithLike(c context.Context, cid int64, pn, ps int, lastAids []int64, sort int, mid int64) (res []*artmdl.RecommendArtWithLike, err error) {
var recs []*artmdl.RecommendArt
if recs, err = s.Recommends(c, cid, pn, ps, lastAids, sort); err != nil {
return
}
var aids []int64
for _, rec := range recs {
aids = append(aids, rec.ID)
}
states, _ := s.HadLikesByMid(c, mid, aids)
for _, rec := range recs {
r := &artmdl.RecommendArtWithLike{RecommendArt: *rec}
if states != nil {
r.LikeState = int(states[rec.ID])
}
res = append(res, r)
}
return
}

View File

@@ -0,0 +1,46 @@
package service
import (
"context"
"testing"
thumbupMdl "go-common/app/service/main/thumbup/model"
thumbup "go-common/app/service/main/thumbup/rpc/client"
"github.com/golang/mock/gomock"
. "github.com/smartystreets/goconvey/convey"
)
func Test_isLike(t *testing.T) {
var (
mid = int64(1)
aid = int64(2)
state = int8(1)
)
Convey("get data", t, WithMock(t, func(mockCtrl *gomock.Controller) {
mock := thumbup.NewMockThumbupRPC(mockCtrl)
s.thumbupRPC = mock
arg := &thumbupMdl.ArgHasLike{Business: "article", MessageIDs: []int64{aid}, Mid: mid}
mock.EXPECT().HasLike(gomock.Any(), arg).Return(map[int64]int8{aid: state}, nil)
res, err := s.isLike(context.TODO(), mid, aid)
So(err, ShouldBeNil)
So(res, ShouldEqual, state)
}))
}
func Test_HadLikesByMid(t *testing.T) {
var (
mid = int64(1)
aids = []int64{2, 2000}
state = map[int64]int8{2: 1, 2000: 0}
)
Convey("get data", t, WithMock(t, func(mockCtrl *gomock.Controller) {
mock := thumbup.NewMockThumbupRPC(mockCtrl)
s.thumbupRPC = mock
arg := &thumbupMdl.ArgHasLike{Business: "article", MessageIDs: aids, Mid: mid}
mock.EXPECT().HasLike(gomock.Any(), arg).Return(state, nil)
res, err := s.HadLikesByMid(context.TODO(), mid, aids)
So(err, ShouldBeNil)
So(res, ShouldResemble, state)
}))
}

View File

@@ -0,0 +1,395 @@
package service
import (
"context"
"sort"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
var (
_blankList = &model.List{ID: -1}
_blankArtList = int64(-1)
_blankListArts = []*model.ListArtMeta{&model.ListArtMeta{ID: -1}}
)
func (s *Service) rawListArticles(c context.Context, listID int64) (res []*model.ListArtMeta, err error) {
if listID <= 0 {
return
}
arts, err := s.dao.CreativeListArticles(c, listID)
if err != nil {
return
}
var ids []int64
for _, art := range arts {
ids = append(ids, art.ID)
}
metas, err := s.dao.CreativeArticles(c, ids)
if err != nil {
return
}
for _, id := range ids {
if metas[id] != nil {
res = append(res, metas[id])
}
}
return
}
// List .
func (s *Service) List(c context.Context, id int64) (res *model.List, err error) {
res, err = s.dao.List(c, id)
res.FillDefaultImage(s.c.Article.ListDefaultImage)
return
}
// ListArticles get list passed articles
func (s *Service) ListArticles(c context.Context, id, mid int64) (res *model.ListArticles, err error) {
if id <= 0 {
return
}
res = &model.ListArticles{}
res.List, err = s.List(c, id)
if err != nil {
return
}
if res.List == nil {
err = ecode.NothingFound
return
}
var lastID int64
group := &errgroup.Group{}
group.Go(func() (err error) {
arts, err := s.dao.ListArts(c, id)
if err != nil {
return
}
for _, a := range arts {
if a.IsNormal() {
res.Articles = append(res.Articles, a)
}
}
return
})
group.Go(func() (err error) {
res.Author, err = s.author(c, res.List.Mid)
return
})
group.Go(func() (err error) {
res.List.Read, err = s.dao.CacheListReadCount(c, id)
return
})
group.Go(func() (err error) {
if mid > 0 {
res.Attention, _ = s.isAttention(c, mid, res.List.Mid)
}
return
})
group.Go(func() (err error) {
if mid == 0 {
return
}
lastID, _ = s.historyPosition(c, mid, res.List.ID)
return
})
err = group.Wait()
if res.Articles == nil {
res.Articles = []*model.ListArtMeta{}
}
res.List.ArticlesCount = int64(len(res.Articles))
res.Last.Strong()
if lastID == 0 {
return
}
for _, a := range res.Articles {
if a.ID == lastID {
res.Last = *a
break
}
}
return
}
// WebListArticles .
func (s *Service) WebListArticles(c context.Context, id, mid int64) (res *model.WebListArticles, err error) {
arts, err := s.ListArticles(c, id, mid)
res = &model.WebListArticles{Attention: arts.Attention, Author: arts.Author, List: arts.List, Last: arts.Last}
var ids []int64
for _, a := range arts.Articles {
ids = append(ids, a.ID)
}
var stats map[int64]*model.Stats
var states map[int64]int8
group, ctx := errgroup.WithContext(c)
group.Go(func() (err error) {
if len(ids) == 0 {
return
}
stats, _ = s.stats(ctx, ids)
return
})
group.Go(func() (err error) {
if mid == 0 || len(ids) == 0 {
return
}
states, _ = s.HadLikesByMid(ctx, mid, ids)
return
})
group.Wait()
for _, a := range arts.Articles {
art := &model.FullListArtMeta{ListArtMeta: a}
if s.categoriesMap[art.Category.ID] != nil {
art.Category = s.categoriesMap[art.Category.ID]
art.Categories = s.categoryParents[art.Category.ID]
}
s := stats[art.ID]
if s != nil {
art.Stats = *s
}
if states != nil {
art.LikeState = states[art.ID]
}
res.Articles = append(res.Articles, art)
}
return
}
// RebuildListCache rebuild list cache
func (s *Service) rebuildArticleListCache(c context.Context, aid int64) (listID int64, err error) {
s.deleteArtsListCache(c, aid)
lists, _ := s.dao.RawArtsListID(c, []int64{aid})
listID = lists[aid]
if listID > 0 {
err = s.dao.SetArticleListCache(c, listID, []*model.ListArtMeta{&model.ListArtMeta{ID: aid}})
}
return
}
// RebuildListCache rebuild list cache
func (s *Service) RebuildListCache(c context.Context, id int64) (err error) {
err = s.updateListCache(c, id)
if err != nil {
return
}
arts, err := s.dao.RawListArts(c, id)
if err != nil {
return
}
if err = s.updateListArtsCache(c, id, arts); err != nil {
return
}
if err = s.updateArtListCache(c, id, arts); err != nil {
return
}
list, err := s.dao.RawList(c, id)
if err != nil {
return
}
if list == nil {
err = ecode.NothingFound
return
}
if err = s.dao.RebuildUpListsCache(c, list.Mid); err != nil {
return
}
if err = s.dao.RebuildListReadCountCache(c, id); err != nil {
return
}
return
}
func (s *Service) updateListCache(c context.Context, id int64) (err error) {
list, err := s.dao.RawList(c, id)
if err != nil {
return
}
err = s.dao.AddCacheList(c, id, list)
return
}
func (s *Service) updateListArtsCache(c context.Context, id int64, arts []*model.ListArtMeta) (err error) {
if arts == nil {
arts, err = s.dao.RawListArts(c, id)
if err != nil {
return
}
}
if len(arts) == 0 {
err = s.deleteListArtsCache(c, id)
return
}
err = s.dao.AddCacheListArts(c, id, arts)
return
}
func (s *Service) updateArtListCache(c context.Context, id int64, arts []*model.ListArtMeta) (err error) {
if arts == nil {
arts, err = s.dao.RawListArts(c, id)
if err != nil {
return
}
}
err = s.dao.SetArticleListCache(c, id, arts)
return
}
func (s *Service) deleteListArtsCache(c context.Context, listID int64) (err error) {
err = s.dao.AddCacheListArts(c, listID, _blankListArts)
return
}
func (s *Service) deleteListCache(c context.Context, listID int64) (err error) {
l := map[int64]*model.List{listID: _blankList}
err = s.dao.AddCacheLists(c, l)
return
}
func (s *Service) deleteArtsListCache(c context.Context, ids ...int64) (err error) {
if len(ids) == 0 {
return
}
m := make(map[int64]int64)
for _, id := range ids {
m[id] = _blankArtList
}
if err = s.dao.AddCacheArtsListID(c, m); err != nil {
log.Errorv(c, log.KV("log", "deleteArtsListCache"), log.KV("err", err), log.KV("arg", ids))
}
return
}
// ListInfo list info
func (s *Service) ListInfo(c context.Context, aid int64) (res *model.ListInfo, err error) {
lists, err := s.dao.ArtsList(c, []int64{aid})
if err != nil {
return
}
list := lists[aid]
if list == nil {
err = ecode.NothingFound
return
}
res = &model.ListInfo{List: list}
var articles []*model.ListArtMeta
arts, err := s.dao.ListArts(c, list.ID)
if err != nil {
return
}
for _, a := range arts {
if a.IsNormal() {
articles = append(articles, a)
}
}
res.Total = len(articles)
for i, l := range articles {
if l.ID == aid {
res.Now = i
if i-1 >= 0 {
res.Last = articles[i-1]
}
if (i + 1) < len(articles) {
res.Next = articles[i+1]
}
break
}
}
res.List.FillDefaultImage(s.c.Article.ListDefaultImage)
res.List.Read, err = s.dao.CacheListReadCount(c, list.ID)
return
}
// Lists .
func (s *Service) Lists(c context.Context, keys []int64) (res map[int64]*model.List, err error) {
res, err = s.dao.Lists(c, keys)
for _, l := range res {
l.FillDefaultImage(s.c.Article.ListDefaultImage)
}
return
}
// UpLists .
func (s *Service) UpLists(c context.Context, mid int64, sortType int8) (res model.UpLists, err error) {
lists, err := s.dao.UpLists(c, mid)
if err != nil {
return
}
lmap, err := s.Lists(c, lists)
if err != nil {
return
}
arts, err := s.dao.ListsArts(c, lists)
if err != nil {
return
}
counts, err := s.dao.CacheListsReadCount(c, lists)
for _, l := range lists {
if lmap[l] != nil {
list := lmap[l]
if arts[l] != nil {
list.ArticlesCount = int64(len(arts[l]))
}
list.Read = counts[l]
res.Lists = append(res.Lists, list)
}
}
if sortType == model.ListSortView {
sort.Slice(res.Lists, func(i, j int) bool { return res.Lists[i].Read > res.Lists[j].Read })
} else {
sort.Slice(res.Lists, func(i, j int) bool { return res.Lists[i].PublishTime > res.Lists[j].PublishTime })
}
res.Total = len(res.Lists)
return
}
// rebuildAllListReadCount .
func (s *Service) rebuildAllListReadCount(c context.Context) error {
i := 0
for {
lists, err := s.dao.RawAllListsEx(c, i, 1000)
if err != nil {
log.Errorv(c, log.KV("RebuildAllReadCount err", err))
return err
}
if lists == nil {
return nil
}
for _, list := range lists {
err = s.dao.RebuildListReadCountCache(c, list.ID)
if err != nil {
log.Errorv(c, log.KV("RebuildAllReadCount err", err), log.KV("id", list.ID))
} else {
log.Infov(c, log.KV("RebuildAllReadCount", list.ID))
}
}
i += 1000
}
}
// RebuildAllListReadCount .
func (s *Service) RebuildAllListReadCount(c context.Context) {
go s.rebuildAllListReadCount(context.TODO())
}
// UpArtMetasAndLists .
func (s *Service) UpArtMetasAndLists(c context.Context, mid int64, pn int, ps int, sortType int) (res model.UpArtMetasLists, err error) {
metas, err := s.UpArticleMetas(c, mid, pn, ps, sortType)
if err != nil {
return
}
res.UpArtMetas = metas
res.UpLists, err = s.UpLists(c, mid, model.ListSortPtime)
if res.UpArtMetas == nil {
res.UpArtMetas = &model.UpArtMetas{}
}
if res.UpArtMetas.Articles == nil {
res.UpArtMetas.Articles = []*model.Meta{}
}
if res.UpLists.Lists == nil {
res.UpLists.Lists = []*model.List{}
}
return
}

View File

@@ -0,0 +1,56 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_List(t *testing.T) {
Convey("get data", t, func() {
res, err := s.dao.List(context.TODO(), 1)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
})
Convey("list not exist", t, func() {
res, err := s.dao.List(context.TODO(), 999)
So(err, ShouldBeNil)
So(res, ShouldBeNil)
})
}
func Test_rawListArticles(t *testing.T) {
Convey("get data", t, func() {
res, err := s.rawListArticles(context.TODO(), 1)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
})
}
func Test_ListInfo(t *testing.T) {
Convey("get data", t, func() {
res, err := s.ListInfo(context.TODO(), 821)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
})
Convey("null data", t, func() {
res, err := s.ListInfo(context.TODO(), 999999999)
So(err, ShouldNotBeNil)
So(res, ShouldBeNil)
})
}
func Test_Lists(t *testing.T) {
Convey("get data", t, func() {
res, err := s.Lists(context.TODO(), []int64{3})
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
So(res[3].ImageURL, ShouldNotBeEmpty)
})
Convey("null data", t, func() {
res, err := s.Lists(context.TODO(), []int64{})
So(err, ShouldBeNil)
So(res, ShouldBeEmpty)
})
}

View File

@@ -0,0 +1,66 @@
package service
import (
"context"
"fmt"
artmdl "go-common/app/interface/openplatform/article/model"
)
var (
_likeMessage = int64(1)
)
// SendMessage send message to uppper
func (s *Service) SendMessage(c context.Context, aid int64, stat *artmdl.Stats) (err error) {
var (
title, msg string
meta *artmdl.Meta
max int64
)
if exist, _ := s.dao.ExpireMaxLikeCache(c, aid); exist {
max, _ = s.dao.MaxLikeCache(c, aid)
}
if (stat.Like <= max) || (!shouldNofify(stat.Like)) {
return
}
if meta, err = s.ArticleMeta(c, aid); (err != nil) || (meta == nil) {
return
}
mid := meta.Author.Mid
if len(s.c.Article.MessageMids) > 0 {
var exist bool
for _, m := range s.c.Article.MessageMids {
if m == mid {
exist = true
break
}
}
if !exist {
return
}
}
title = fmt.Sprintf("有%v人点赞了你的专栏文章", stat.Like)
msg = fmt.Sprintf("有%v个小伙伴点赞你投稿的专栏文章“#{%s}{\"http://www.bilibili.com/read/cv%d\"}”~快去看看吧!#{点击前往}{\"http://www.bilibili.com/read/cv%d\"}", stat.Like, meta.Title, aid, aid)
err = s.dao.SendMessage(c, _likeMessage, mid, aid, title, msg)
cache.Save(func() {
s.dao.SetMaxLikeCache(context.TODO(), aid, stat.Like)
})
return
}
func shouldNofify(n int64) (res bool) {
switch {
case n <= 0:
res = false
case n <= 10:
res = true
case n <= 100:
res = (n%10 == 0)
case n <= 1000:
res = (n%100 == 0)
default:
res = (n%10000 == 0)
}
return
}

View File

@@ -0,0 +1,25 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_shouldNofify(t *testing.T) {
Convey("get data", t, func() {
So(shouldNofify(0), ShouldBeFalse)
So(shouldNofify(1), ShouldBeTrue)
So(shouldNofify(5), ShouldBeTrue)
So(shouldNofify(10), ShouldBeTrue)
So(shouldNofify(15), ShouldBeFalse)
So(shouldNofify(50), ShouldBeTrue)
So(shouldNofify(55), ShouldBeFalse)
So(shouldNofify(201), ShouldBeFalse)
So(shouldNofify(300), ShouldBeTrue)
So(shouldNofify(1010), ShouldBeFalse)
So(shouldNofify(10000), ShouldBeTrue)
So(shouldNofify(20000), ShouldBeTrue)
So(shouldNofify(21000), ShouldBeFalse)
})
}

View File

@@ -0,0 +1,55 @@
package service
import (
"context"
"time"
artmdl "go-common/app/interface/openplatform/article/model"
)
const (
_platAll = 0
_platAndroid = 1
_platIOS = 2
_equal = 0
_greaterThanOrEqual = 1
_lessThanOrEqual = 2
)
func (s *Service) loadNoticeproc() {
for {
if notices, err := s.dao.Notices(context.TODO(), time.Now()); err == nil {
s.notices = notices
}
time.Sleep(time.Minute)
}
}
// Notice get notice
func (s *Service) Notice(platform string, build int) (res *artmdl.Notice) {
var plat int
if platform == "android" {
plat = _platAndroid
}
if platform == "ios" {
plat = _platIOS
}
for _, notice := range s.notices {
var ok bool
if (notice.Plat == _platAll) || (notice.Plat == plat) {
switch notice.Condition {
case _equal:
ok = build == notice.Build
case _greaterThanOrEqual:
ok = build >= notice.Build
case _lessThanOrEqual:
ok = build <= notice.Build
}
}
if ok {
notice.Content = notice.Title
return notice
}
}
return nil
}

View File

@@ -0,0 +1,31 @@
package service
import (
"testing"
"go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Notice(t *testing.T) {
a := &model.Notice{ID: 1, Plat: _platAll, Condition: _equal, Build: 20}
b := &model.Notice{ID: 2, Plat: _platIOS, Condition: _greaterThanOrEqual, Build: 30}
c := &model.Notice{ID: 3, Plat: _platAndroid, Condition: _lessThanOrEqual, Build: 50}
s.notices = []*model.Notice{a, b, c}
Convey("all plat", t, func() {
So(s.Notice("", 20), ShouldResemble, a)
So(s.Notice("", 30), ShouldBeNil)
So(s.Notice("", 10), ShouldBeNil)
})
Convey("ios plat", t, func() {
So(s.Notice("ios", 25), ShouldBeNil)
So(s.Notice("ios", 30), ShouldResemble, b)
So(s.Notice("ios", 40), ShouldResemble, b)
})
Convey("android plat", t, func() {
So(s.Notice("android", 25), ShouldResemble, c)
So(s.Notice("android", 50), ShouldResemble, c)
So(s.Notice("android", 60), ShouldBeNil)
})
}

View File

@@ -0,0 +1,455 @@
package service
import (
"context"
"strconv"
"strings"
"go-common/app/interface/openplatform/article/model"
accmdl "go-common/app/service/main/account/model"
"go-common/library/ecode"
"go-common/library/log"
)
// CreativeSubArticle submit model.
func (s *Service) CreativeSubArticle(c context.Context, mid int64, art *model.ArtParam, ak, ck, ip string) (aid int64, err error) {
identified, _ := s.IdentifyInfo(c, mid, 0, ak, ck, ip)
if err = s.CheckIdentify(identified); err != nil {
log.Error("s.accountRPC.IdentifyInfo mid(%d),ip(%s)", mid, ip)
return
}
var arg = &model.ArgArticle{
Aid: art.AID,
Mid: art.MID,
Category: art.Category,
State: art.State,
Reprint: art.Reprint,
TemplateID: art.TemplateID,
Title: art.Title,
BannerURL: art.BannerURL,
Content: art.Content,
Summary: art.Summary,
RealIP: art.RealIP,
Words: art.Words,
DynamicIntro: art.DynamicIntro,
ImageURLs: art.ImageURLs,
OriginImageURLs: art.OriginImageURLs,
ActivityID: art.ActivityID,
ListID: art.ListID,
MediaID: art.MediaID,
Spoiler: art.Spoiler,
}
if art.Tags != "" {
arg.Tags = strings.Split(art.Tags, ",")
} else {
arg.Tags = []string{}
}
var a = model.TransformArticle(arg)
if aid, err = s.AddArticle(c, a, arg.ActivityID, arg.ListID, arg.RealIP); err != nil {
return
}
return
}
// CreativeUpdateArticle update model.
func (s *Service) CreativeUpdateArticle(c context.Context, mid int64, art *model.ArtParam, ak, ck, ip string) (err error) {
identified, _ := s.IdentifyInfo(c, mid, 0, ak, ck, ip)
if err = s.CheckIdentify(identified); err != nil {
log.Error("s.accountRPC.IdentifyInfo mid(%d),ip(%s)", mid, ip)
return
}
var arg = &model.ArgArticle{
Aid: art.AID,
Mid: art.MID,
Category: art.Category,
State: art.State,
Reprint: art.Reprint,
TemplateID: art.TemplateID,
Title: art.Title,
BannerURL: art.BannerURL,
Content: art.Content,
Summary: art.Summary,
RealIP: art.RealIP,
Words: art.Words,
DynamicIntro: art.DynamicIntro,
ImageURLs: art.ImageURLs,
OriginImageURLs: art.OriginImageURLs,
ListID: art.ListID,
MediaID: art.MediaID,
Spoiler: art.Spoiler,
}
if art.Tags != "" {
arg.Tags = strings.Split(art.Tags, ",")
} else {
arg.Tags = []string{}
}
log.Info("d.art.UpdateArticle id (%d) words (%d) ImageURLs (%s) OriginImageURLs (%s)", arg.Aid, len(arg.Content), art.ImageURLs, art.OriginImageURLs)
var a = model.TransformArticle(arg)
if err = s.UpdateArticle(c, a, arg.ActivityID, arg.ListID, arg.RealIP); err != nil {
return
}
return
}
// CreativeAddDraft .
func (s *Service) CreativeAddDraft(c context.Context, mid int64, art *model.ArtParam) (aid int64, err error) {
var arg = &model.ArgArticle{
Aid: art.AID,
Mid: art.MID,
Category: art.Category,
State: art.State,
Reprint: art.Reprint,
TemplateID: art.TemplateID,
Title: art.Title,
BannerURL: art.BannerURL,
Content: art.Content,
Summary: art.Summary,
RealIP: art.RealIP,
Words: art.Words,
DynamicIntro: art.DynamicIntro,
ImageURLs: art.ImageURLs,
OriginImageURLs: art.OriginImageURLs,
ListID: art.ListID,
MediaID: art.MediaID,
Spoiler: art.Spoiler,
}
if art.Tags != "" {
arg.Tags = strings.Split(art.Tags, ",")
} else {
arg.Tags = []string{}
}
log.Info("d.art.AddDraft id (%d) words (%d) ImageURLs (%s) OriginImageURLs (%s)", arg.Aid, len(arg.Content), art.ImageURLs, art.OriginImageURLs)
d := model.TransformDraft(arg)
aid, err = s.AddArtDraft(c, d)
return
}
// CheckIdentify fn
func (s *Service) CheckIdentify(identify int) (err error) {
switch identify {
case 0:
err = nil
case 1:
err = ecode.UserCheckInvalidPhone
case 2:
err = ecode.UserCheckNoPhone
}
return
}
// IdentifyInfo .
func (s *Service) IdentifyInfo(c context.Context, mid int64, phoneOnly int8, ak, ck, ip string) (ret int, err error) {
var (
mf *accmdl.Profile
arg = &accmdl.ArgMid{
Mid: mid,
}
)
if mf, err = s.accountRPC.Profile3(c, arg); err != nil {
log.Error("d.acc.MyInfo error(%+v) | mid(%d) ck(%s) ak(%s) ip(%s) arg(%v)", err, mid, ck, ak, ip, arg)
err = ecode.CreativeAccServiceErr
return
}
//switch for FrontEnd return json format
ret = s.switchPhoneRet(int(mf.TelStatus))
if phoneOnly == 1 {
return
}
if mf.TelStatus == 1 || mf.Identification == 1 {
return 0, err
}
return
}
// 0: "已实名认证",
// 1: "根据国家实名制认证的相关要求您需要换绑一个非170/171的手机号才能继续进行操作。",
// 2: "根据国家实名制认证的相关要求,您需要绑定手机号,才能继续进行操作。",
func (s *Service) switchPhoneRet(newV int) (oldV int) {
switch newV {
case 0:
oldV = 2
case 1:
oldV = 0
case 2:
oldV = 1
}
return
}
// CreativeArticles creative articles list
func (s *Service) CreativeArticles(c context.Context, mid int64, pn, ps, sort, group, category int, ip string) (arts *model.CreativeArtList, err error) {
var res *model.CreationArts
res, err = s.CreationUpperArticlesMeta(c, mid, group, category, sort, pn, ps, ip)
arts = &model.CreativeArtList{}
if res != nil {
arts.Articles = make([]*model.CreativeMeta, 0, len(res.Articles))
arts.Page = res.Page
arts.Type = res.Type
}
if err != nil {
log.Error("s.art.Articles(mid:%d) error(%+v)", mid, err)
return
}
if (res == nil) || (res.Articles == nil) || (len(res.Articles) == 0) {
log.Info("s.art.Articles(mid:%d) res(%v)", mid, res)
return
}
for _, v := range res.Articles {
id := strconv.FormatInt(v.ID, 10)
m := &model.CreativeMeta{
ID: v.ID,
Category: v.Category,
Title: v.Title,
Summary: v.Summary,
BannerURL: v.BannerURL,
TemplateID: v.TemplateID,
State: v.State,
Reprint: v.Reprint,
Reason: v.Reason,
PTime: v.PublishTime,
Author: v.Author,
Stats: v.Stats,
CTime: v.Ctime,
MTime: v.Mtime,
EditURL: "https://member.bilibili.com/article-text/mobile?aid=" + id + "&type=2",
DynamicIntro: v.Dynamic,
ImageURLs: v.ImageURLs,
OriginImageURLs: v.OriginImageURLs,
List: v.List,
}
if m.ImageURLs == nil {
m.ImageURLs = []string{}
}
if m.OriginImageURLs == nil {
m.OriginImageURLs = []string{}
}
switch m.State {
case 0, 7:
// 开放浏览, 可编辑
m.ViewURL = "https://www.bilibili.com/read/cv" + id
m.IsPreview = 0
m.EditTimes = s.EditTimes(c, m.ID)
m.EditURL = "https://member.bilibili.com/article-text/mobile?aid=" + id + "&type=3"
case 4:
// 开放浏览
m.ViewURL = "https://www.bilibili.com/read/cv" + id
m.IsPreview = 0
case 5, 6:
// 开放浏览,重复编辑待审/重复编辑未通过
m.ViewURL = "https://www.bilibili.com/read/cv" + id
m.IsPreview = 2
m.PreViewURL = "https://www.bilibili.com/read/preview/" + id
var (
a *model.Article
e1 error
)
if a, e1 = s.ArticleVersion(c, m.ID); e1 != nil {
log.Error("s.ArticleVersion(%d) error(%+v)", m.ID, e1)
continue
}
m.Title = a.Title
m.Reason = a.Reason
m.Category = a.Category
m.TemplateID = a.TemplateID
m.ImageURLs = a.ImageURLs
m.Summary = a.Summary
m.Reprint = a.Reprint
m.BannerURL = a.BannerURL
m.OriginImageURLs = a.OriginImageURLs
if m.State == 6 {
m.EditURL = "https://member.bilibili.com/article-text/mobile?aid=" + id + "&type=3"
m.EditTimes = s.EditTimes(c, m.ID)
m.Reason, _ = s.lastReason(c, m.ID, m.State)
}
default:
// 预览
m.PreViewURL = "https://www.bilibili.com/read/preview/" + id
m.IsPreview = 1
}
tags := []string{}
m.Tags = tags
if len(v.Tags) > 0 {
for _, vv := range v.Tags {
tags = append(tags, vv.Name)
}
m.Tags = tags
}
arts.Articles = append(arts.Articles, m)
}
return
}
// CreativeDrafts get draft list.
func (s *Service) CreativeDrafts(c context.Context, mid int64, pn, ps int, ip string) (dls *model.CreativeDraftList, err error) {
var res *model.Drafts
res, err = s.UpperDrafts(c, mid, pn, ps)
if err != nil {
log.Error("s.art.Drafts(mid:%d) error(%+v)", mid, err)
return
}
if res == nil || res.Drafts == nil || len(res.Drafts) <= 0 {
log.Info("s.art.Drafts(mid:%d) res(%+v)", mid, res)
return
}
ms := make([]*model.CreativeMeta, 0, len(res.Drafts))
for _, v := range res.Drafts {
id := strconv.FormatInt(v.ID, 10)
m := &model.CreativeMeta{
ID: v.ID,
Category: v.Category,
Title: v.Title,
Summary: v.Summary,
BannerURL: v.BannerURL,
TemplateID: v.TemplateID,
State: v.State,
Reprint: v.Reprint,
Reason: v.Reason,
PTime: v.PublishTime,
Author: v.Author,
Stats: v.Stats,
CTime: v.Ctime,
MTime: v.Mtime,
DynamicIntro: v.Dynamic,
ImageURLs: v.ImageURLs,
OriginImageURLs: v.OriginImageURLs,
List: v.List,
EditURL: "https://member.bilibili.com/article-text/mobile?aid=" + id,
}
if m.ImageURLs == nil {
m.ImageURLs = []string{}
}
if m.OriginImageURLs == nil {
m.OriginImageURLs = []string{}
}
m.Tags = v.Tags
if len(v.Tags) == 0 {
m.Tags = []string{}
}
ms = append(ms, m)
}
dls = &model.CreativeDraftList{
DraftURL: "https://member.bilibili.com/creative/app/article_drafts",
}
dls.Drafts = ms
dls.Page = res.Page
return
}
// CreativeDraft get draft.
func (s *Service) CreativeDraft(c context.Context, aid, mid int64, ip string) (res *model.CreativeMeta, err error) {
var df *model.Draft
if df, err = s.ArtDraft(c, aid, mid); err != nil {
return
}
if df == nil || df.Article == nil {
err = ecode.CreativeArticleNotExist
return
}
res = &model.CreativeMeta{
ID: df.Article.ID,
Category: df.Article.Category,
Title: df.Article.Title,
Content: df.Article.Content,
Summary: df.Article.Summary,
BannerURL: df.Article.BannerURL,
TemplateID: df.Article.TemplateID,
State: df.Article.State,
Author: df.Article.Author,
Stats: df.Article.Stats,
Reprint: df.Article.Reprint,
Reason: df.Article.Reason,
PTime: df.Article.PublishTime,
CTime: df.Article.Ctime,
MTime: df.Article.Mtime,
DynamicIntro: df.Article.Dynamic,
ImageURLs: df.ImageURLs,
OriginImageURLs: df.OriginImageURLs,
List: df.List,
MediaID: df.Article.Media.MediaID,
Spoiler: df.Article.Media.Spoiler,
}
if res.ImageURLs == nil {
res.ImageURLs = []string{}
}
if res.OriginImageURLs == nil {
res.OriginImageURLs = []string{}
}
res.Tags = df.Tags
if len(df.Tags) == 0 {
res.Tags = []string{}
}
return
}
// CreativeView get article detail.
func (s *Service) CreativeView(c context.Context, aid, mid int64, ip string) (res *model.CreativeMeta, err error) {
var art *model.Article
if art, err = s.CreationArticle(c, aid, mid); err != nil {
return
}
res = &model.CreativeMeta{
ID: art.ID,
Category: art.Category,
Title: art.Title,
Content: art.Content,
Summary: art.Summary,
BannerURL: art.BannerURL,
TemplateID: art.TemplateID,
State: art.State,
Reprint: art.Reprint,
Reason: art.Reason,
PTime: art.PublishTime,
Author: art.Author,
Stats: art.Stats,
CTime: art.Ctime,
MTime: art.Mtime,
DynamicIntro: art.Dynamic,
ImageURLs: art.ImageURLs,
OriginImageURLs: art.OriginImageURLs,
List: art.List,
MediaID: art.Media.MediaID,
Spoiler: art.Media.Spoiler,
}
if res.State == model.StateOpen || res.State == model.StateReReject || res.State == model.StateRePass {
res.EditTimes = s.EditTimes(c, res.ID)
}
if res.ImageURLs == nil {
res.ImageURLs = []string{}
}
if res.OriginImageURLs == nil {
res.OriginImageURLs = []string{}
}
if len(art.Tags) > 0 {
var tags []string
for _, v := range art.Tags {
tags = append(tags, v.Name)
}
res.Tags = tags
}
return
}
// CreativeDraftCount count of upper's drafts
func (s *Service) CreativeDraftCount(c context.Context, mid int64) (count int) {
count, _ = s.dao.CountUpperDraft(c, mid)
return
}
// ArticleCapture capture a new image.
func (s *Service) ArticleCapture(c context.Context, url string) (loc string, size int, err error) {
loc, size, err = s.dao.Capture(c, url)
if err != nil {
log.Error("s.bfs.Capture error(%v)", err)
}
return
}
// SetMediaScore set media score.
func (s *Service) SetMediaScore(c context.Context, score, aid, mediaID, mid int64) (err error) {
return s.dao.SetScore(c, score, aid, mediaID, mid)
}
// DelMediaScore get media score.
func (s *Service) DelMediaScore(c context.Context, aid, mediaID, mid int64) (err error) {
return s.dao.DelScore(c, aid, mediaID, mid)
}

View File

@@ -0,0 +1,87 @@
package service
import (
"context"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
)
// RankCategories rank categoires
func (s *Service) RankCategories(c context.Context) (res []*artmdl.RankCategory) {
res = s.c.RankCategories
return
}
// Ranks get ranks
func (s *Service) Ranks(c context.Context, cid int64, mid int64, ip string) (res []*artmdl.RankMeta, note string, err error) {
var (
exist bool
addCache = true
aids []int64
rank artmdl.RankResp
metas map[int64]*artmdl.Meta
)
if !s.ranksMap[cid] {
err = ecode.RequestErr
return
}
if exist, err = s.dao.ExpireRankCache(c, cid); err != nil {
addCache = false
err = nil
}
if exist {
if rank, err = s.dao.RankCache(c, cid); err != nil {
exist = false
err = nil
addCache = false
}
}
if !exist {
if rank, err = s.dao.Rank(c, cid, ip); err != nil {
if rank, err = s.dao.RankCache(c, cid); err != nil {
return
}
} else {
if addCache && len(rank.List) > 0 {
cache.Save(func() {
s.dao.AddRankCache(context.TODO(), cid, rank)
})
}
}
}
if len(rank.List) == 0 {
return
}
for _, a := range rank.List {
aids = append(aids, a.Aid)
}
if metas, err = s.ArticleMetas(c, aids); err != nil {
return
}
var ups []int64
for _, r := range rank.List {
if metas[r.Aid] != nil {
res = append(res, &artmdl.RankMeta{Meta: metas[r.Aid], Score: r.Score})
ups = append(ups, metas[r.Aid].Author.Mid)
}
}
if (len(ups) > 0) && (mid != 0) {
if attentions, e := s.isAttentions(c, mid, ups); e == nil {
for _, r := range res {
r.Attention = attentions[r.Author.Mid]
}
}
}
if s.setting.ShowRankNote {
note = rank.Note
}
return
}
func (s *Service) loadRanks() {
for _, rank := range s.c.RankCategories {
s.ranksMap[rank.ID] = true
}
}

View File

@@ -0,0 +1,27 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_RankCategories(t *testing.T) {
Convey("get data", t, func() {
res := s.RankCategories(context.TODO())
So(res, ShouldNotBeEmpty)
})
}
func Test_Ranks(t *testing.T) {
// data := `{"code":0,"source_date":"2018-01-02","list":[{"aid":1,"mid":137952,"score":565918,"view":176708,"reply":2108,"favorites":1517,"coin":6816,"likes":10454},{"aid":2,"mid":144900177,"score":300536,"view":652823,"reply":2661,"favorites":10268,"coin":470,"likes":4130},{"aid":3,"mid":32708462,"score":241845,"view":485737,"reply":969,"favorites":7347,"coin":1290,"likes":5542},{"aid":4,"mid":124799,"score":188953,"view":46594,"reply":595,"favorites":797,"coin":1771,"likes":6268}],"num":4,"note":"统计7日内新投稿的数据综合得分"}`
// Convey("get data", t, WithCleanCache(func() {
// s.setting.ShowRankNote = true
// httpMock("GET", s.c.Article.RankHost+"/data/rank/article/all-7.json").Reply(200).JSON(data)
// res, note, err := s.Ranks(context.TODO(), model.RankWeek, 1, "")
// So(err, ShouldBeNil)
// So(res, ShouldNotBeEmpty)
// So(note, ShouldNotBeEmpty)
// }))
}

View File

@@ -0,0 +1,24 @@
package service
import (
"context"
"go-common/library/ecode"
)
// ReadPing 处理用户阅读心跳
func (s *Service) ReadPing(c context.Context, buvid string, aid int64, mid int64, ip string, cur int64, source string) (err error) {
var last int64
if last, err = s.dao.GetsetReadPing(c, buvid, aid, cur); err != nil {
err = ecode.RequestErr
return
}
if last != 0 {
return
}
if s.dao.AddReadPingSet(c, buvid, aid, mid, ip, cur, source); err != nil {
err = ecode.RequestErr
return
}
return
}

View File

@@ -0,0 +1,552 @@
package service
import (
"context"
"math/rand"
"sort"
"sync"
"time"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
var (
_recommendCategory = int64(0)
)
// Recommends list recommend arts by category id
func (s *Service) Recommends(c context.Context, cid int64, pn, ps int, lastAids []int64, sort int) (res []*model.RecommendArt, err error) {
var (
start = (pn - 1) * ps
// 多取一些用于去重
end = start + ps - 1 + len(lastAids)
rems = make(map[int64]*model.Recommend)
allIDs, aids []int64
metas map[int64]*model.Meta
aidsm map[int64]bool
withRecommend bool
)
if cid != _recommendCategory {
if (s.categoriesMap == nil) || (s.categoriesMap[cid] == nil) {
err = ecode.RequestErr
return
}
}
if sort == model.FieldDefault {
withRecommend = true
sort = model.FieldNew
}
allRec := s.recommendAids[cid]
var recommends [][]*model.Recommend
if cid == _recommendCategory {
recommends = s.genRecommendArtFromPool(s.RecommendsMap[cid], s.c.Article.RecommendRegionLen)
} else {
recommends = s.RecommendsMap[cid]
}
recommendsLen := len(recommends)
// 只是最新文章 无推荐
if (start >= recommendsLen) || !withRecommend {
if (cid == _recommendCategory) && !s.setting.ShowRecommendNewArticles {
return
}
var (
nids []int64
newArtStart = start
newArtEnd = end
)
if withRecommend {
newArtStart = start - recommendsLen
newArtEnd = end - recommendsLen
}
nids, _ = s.dao.SortCache(c, cid, sort, newArtStart, newArtEnd)
if withRecommend {
allIDs = uniqIDs(nids, allRec)
} else {
allIDs = nids
}
} else {
aids, rems = s.dealRecommends(recommends)
if end < recommendsLen {
allIDs = aids[start : end+1]
} else {
if (cid == _recommendCategory) && !s.setting.ShowRecommendNewArticles {
allIDs = aids[start:]
} else {
// 混合推荐和最新文章
var (
nids []int64
rs = aids[start:]
)
newArtStart := 0
newArtEnd := (end - start) - len(rs)
nids, _ = s.dao.SortCache(c, cid, sort, newArtStart, newArtEnd)
nids = uniqIDs(nids, allRec)
allIDs = append(rs, nids...)
}
}
}
if len(allIDs) == 0 {
return
}
if metas, err = s.ArticleMetas(c, allIDs); err != nil {
return
}
//过滤禁止显示的稿件
filterNoDistributeArtsMap(metas)
filterNoRegionArts(metas)
//填充数据
aidsm = make(map[int64]bool, len(lastAids))
for _, aid := range lastAids {
aidsm[aid] = true
}
for _, id := range allIDs {
if (metas == nil) || (metas[id] == nil) || aidsm[id] {
continue
}
art := &model.RecommendArt{Meta: *metas[id]}
if rems[id] != nil {
art.Recommend = *rems[id]
}
res = append(res, art)
}
//截断分页数据
if ps > len(res) {
ps = len(res)
}
res = res[:ps]
if cid == _recommendCategory {
sortRecs(res)
}
return
}
func (s *Service) dealRecommends(recommends [][]*model.Recommend) (aids []int64, rems map[int64]*model.Recommend) {
rems = make(map[int64]*model.Recommend)
for _, recs := range recommends {
rec := &model.Recommend{}
*rec = *recs[s.randPosition(len(recs))]
aids = append(aids, rec.ArticleID)
// 不在推荐大图时间 去掉大图
if rec.RecImageURL != "" {
var now = time.Now().Unix()
if (now < rec.RecImageStartTime) || (now > rec.RecImageEndTime) {
rec.RecImageURL = ""
rec.RecFlag = false
}
}
if rec.RecFlag {
rec.RecText = "编辑推荐"
}
rems[rec.ArticleID] = rec
// 推荐文章id置空
rec.ArticleID = 0
}
return
}
// 过滤禁止分区投稿
func filterNoRegionArts(as map[int64]*model.Meta) {
for id, a := range as {
if (a != nil) && a.AttrVal(model.AttrBitNoRegion) {
delete(as, id)
}
}
}
// 按照发布时间排序
func sortRecs(res []*model.RecommendArt) {
if len(res) == 0 {
return
}
var len int
for i, v := range res {
if v.Rec {
len = i
}
}
sort.Slice(res[:len+1], func(i, j int) bool { return res[i].PublishTime > res[j].PublishTime })
}
// array a - array b
func uniqIDs(a []int64, b []int64) (res []int64) {
bm := make(map[int64]bool)
for _, v := range b {
bm[v] = true
}
for _, v := range a {
if !bm[v] {
res = append(res, v)
}
}
return
}
// UpdateRecommends update recommends
func (s *Service) UpdateRecommends(c context.Context) (err error) {
var (
recommendsMap = make(map[int64][][]*model.Recommend)
recommendAids = make(map[int64][]int64)
mutex = &sync.Mutex{}
)
group, ctx := errgroup.WithContext(c)
group.Go(func() error {
recommends, err1 := s.dao.RecommendByCategory(ctx, _recommendCategory)
if err1 != nil {
return err1
}
// 推荐分区无位置 为推荐池
rs := [][]*model.Recommend{recommends}
mutex.Lock()
recommendsMap[_recommendCategory] = rs
mutex.Unlock()
return nil
})
for _, category := range s.categoriesMap {
category := category
group.Go(func() error {
recommends, err1 := s.dao.RecommendByCategory(ctx, category.ID)
if err1 != nil {
return err1
}
rs := calculateRecommends(recommends)
mutex.Lock()
recommendsMap[category.ID] = rs
mutex.Unlock()
return nil
})
}
if err = group.Wait(); err != nil {
return
}
s.RecommendsMap = recommendsMap
for cid, v := range recommendsMap {
for _, vv := range v {
for _, vvv := range vv {
recommendAids[cid] = append(recommendAids[cid], vvv.ArticleID)
}
}
}
s.recommendAids = recommendAids
log.Info("s.UpdateRecommends success! len:(%v)", len(recommendsMap))
return
}
func calculateRecommends(rs []*model.Recommend) (res [][]*model.Recommend) {
m := make(map[int][]*model.Recommend)
// 位置去重+ 随机选择
for _, r := range rs {
if r == nil {
continue
}
if len(m[r.Position]) == 0 {
m[r.Position] = append(m[r.Position], r)
} else {
var endTime bool
for _, x := range m[r.Position] {
if x.EndTime != 0 {
endTime = true
break
}
}
if endTime {
if r.EndTime == 0 {
continue
} else {
m[r.Position] = append(m[r.Position], r)
}
} else {
if r.EndTime == 0 {
m[r.Position] = append(m[r.Position], r)
} else {
m[r.Position] = []*model.Recommend{r}
}
}
}
}
for _, recommends := range m {
res = append(res, recommends)
}
sort.Sort(model.Recommends(res))
return
}
func (s *Service) randPosition(max int) (res int) {
res = rand.Intn(max)
return
}
func (s *Service) genRecommendArtFromPool(rs [][]*model.Recommend, recLen int) (res [][]*model.Recommend) {
var pool []*model.Recommend
if len(rs) > 0 {
pool = rs[0]
}
if len(pool) == 0 {
return
}
recs := append([]*model.Recommend{}, pool...)
for i := range recs {
j := rand.Intn(i + 1)
recs[i], recs[j] = recs[j], recs[i]
}
if len(recs) < recLen {
recLen = len(recs)
}
for _, r := range recs[:recLen] {
res = append(res, []*model.Recommend{r})
}
return
}
// DelRecommendArtCache delete recommend article cache
func (s *Service) DelRecommendArtCache(c context.Context, aid, cid int64) (err error) {
s.DelRecommendArt(_recommendCategory, aid)
if cid, err = s.CategoryToRoot(cid); err != nil {
dao.PromError("cache:删除文章推荐缓存")
log.Error("s.DelRecommendArtCache.RootCategory(c, %v, %v) err: %+v", aid, cid, err)
return
}
s.DelRecommendArt(cid, aid)
return
}
// DelRecommendArt delete recommend article
func (s *Service) DelRecommendArt(categoryID int64, aid int64) {
select {
case s.recommendChan <- [2]int64{categoryID, aid}:
default:
dao.PromError("recommends:删除推荐文章 chan已满")
log.Error("s.DelRecommendArt(%v, %v) chan full!", categoryID, aid)
}
}
func (s *Service) deleteRecommendproc() {
for {
info, ok := <-s.recommendChan
if !ok {
return
}
if s.RecommendsMap == nil {
continue
}
categoryID, aid := info[0], info[1]
newRecommendsMap := map[int64][][]*model.Recommend{}
for cid, rss := range s.RecommendsMap {
if cid != categoryID {
newRecommendsMap[cid] = rss
continue
}
var newRecommends [][]*model.Recommend
for _, rs := range rss {
var newRs []*model.Recommend
for _, r := range rs {
if r.ArticleID != aid {
newRs = append(newRs, r)
}
}
if len(newRs) > 0 {
newRecommends = append(newRecommends, newRs)
}
}
newRecommendsMap[cid] = newRecommends
}
s.RecommendsMap = newRecommendsMap
}
}
// RecommendHome recommend home
func (s *Service) RecommendHome(c context.Context, plat int8, build int, pn, ps int, aids []int64, mid int64, ip string, t time.Time, buvid string) (res *model.RecommendHome, sky *model.SkyHorseResp, err error) {
res = &model.RecommendHome{IP: ip, Categories: s.primaryCategories}
plus, sky, err := s.RecommendPlus(c, _recommendCategory, plat, build, pn, ps, aids, mid, t, model.FieldDefault, buvid)
if plus != nil {
res.RecommendPlus = *plus
}
return
}
// RecommendPlus recommend plus
func (s *Service) RecommendPlus(c context.Context, cid int64, plat int8, build int, pn, ps int, aids []int64, mid int64, t time.Time, sort int, buvid string) (res *model.RecommendPlus, sky *model.SkyHorseResp, err error) {
res = &model.RecommendPlus{Banners: []*model.Banner{}, Articles: []*model.RecommendArtWithLike{}, Ranks: []*model.RankMeta{}, Hotspots: []*model.Hotspot{}}
var group *errgroup.Group
group, _ = errgroup.WithContext(c)
group.Go(func() error {
var arts []*model.RecommendArtWithLike
if arts, sky, err = s.SkyHorse(c, cid, pn, ps, aids, sort, mid, build, buvid, plat); err == nil {
res.Articles = arts
}
return nil
})
group.Go(func() error {
if bs, e := s.Banners(c, plat, build, t); (e == nil) && (len(bs) > 0) {
res.Banners = bs
}
return nil
})
group.Go(func() error {
if s.setting.ShowHotspot {
if hs, _ := s.dao.CacheHotspots(c); len(hs) > 0 {
res.Hotspots = hs
}
}
return nil
})
group.Go(func() error {
if !s.setting.ShowAppHomeRank {
return nil
}
if ranks, _, err := s.Ranks(c, model.RankWeek, mid, ""); (err == nil) && (len(ranks) > 0) {
if len(ranks) > 3 {
ranks = ranks[:3]
}
res.Ranks = ranks
}
return nil
})
group.Wait()
return
}
// AllRecommends all recommends articles
func (s *Service) AllRecommends(c context.Context, pn, ps int) (count int64, res []*model.Meta, err error) {
if pn < 1 {
pn = 1
}
t := time.Now()
count, _ = s.dao.AllRecommendCount(c, t)
res = []*model.Meta{}
ids, err := s.dao.AllRecommends(c, t, pn, ps)
if err != nil {
return
}
if len(ids) == 0 {
return
}
metas, err := s.ArticleMetas(c, ids)
if err != nil {
return
}
for _, id := range ids {
if metas[id] != nil {
res = append(res, metas[id])
}
}
return
}
// SkyHorse .
func (s *Service) SkyHorse(c context.Context, cid int64, pn, ps int, lastAids []int64, sort int, mid int64, build int, buvid string, plat int8) (res []*model.RecommendArtWithLike, sky *model.SkyHorseResp, err error) {
if (cid != _recommendCategory) || !s.skyHorseGray(buvid, mid) {
res, err = s.RecommendsWithLike(c, cid, pn, ps, lastAids, sort, mid)
return
}
var aids []int64
var metas map[int64]*model.Meta
var rems map[int64]*model.Recommend
if pn == 1 {
size := ps
if size > s.c.Article.SkyHorseRecommendRegionLen {
size = s.c.Article.SkyHorseRecommendRegionLen
}
recommends := s.genRecommendArtFromPool(s.RecommendsMap[_recommendCategory], size)
aids, rems = s.dealRecommends(recommends)
}
if len(aids) < ps {
sky, err = s.dao.SkyHorse(c, mid, build, buvid, plat, ps-len(aids))
if (err != nil) || (len(sky.Data) == 0) {
res, err = s.RecommendsWithLike(c, cid, pn, ps, lastAids, sort, mid)
sky = nil
return
}
for _, item := range sky.Data {
if rems[item.ID] == nil {
aids = append(aids, item.ID)
}
}
}
if metas, err = s.ArticleMetas(c, aids); err != nil {
return
}
//过滤禁止显示的稿件
filterNoDistributeArtsMap(metas)
filterNoRegionArts(metas)
states, _ := s.HadLikesByMid(c, mid, aids)
for _, aid := range aids {
if metas[aid] == nil {
continue
}
art := model.RecommendArt{Meta: *metas[aid]}
r := &model.RecommendArtWithLike{RecommendArt: art}
if states != nil {
r.LikeState = int(states[aid])
}
if rems[aid] != nil {
r.Recommend = *rems[aid]
}
res = append(res, r)
}
return
}
func (s *Service) skyHorseGray(buvid string, mid int64) bool {
if (mid == 0) && (buvid == "") {
return false
}
for _, id := range s.c.Article.SkyHorseGrayUsers {
if mid == id {
return true
}
}
for _, id := range s.c.Article.SkyHorseGray {
if mid%10 == id {
return true
}
}
return false
}
func (s *Service) groupRecommend(c context.Context) (err error) {
var (
m = make(map[int64]map[int64]bool)
mutex = &sync.Mutex{}
)
for _, recommends := range s.RecommendsMap {
var (
rs []*model.Recommend
arts map[int64]*model.Meta
aids = []int64{}
)
if len(recommends) > 0 {
rs = recommends[0]
}
for _, r := range rs {
aids = append(aids, r.ArticleID)
}
if arts, err = s.ArticleMetas(c, aids); err != nil || arts == nil {
return
}
for _, art := range arts {
if _, ok := m[art.Category.ID]; !ok {
m[art.Category.ID] = make(map[int64]bool)
}
m[art.Category.ID][art.ID] = true
}
}
mutex.Lock()
s.RecommendsGroups = m
mutex.Unlock()
return
}
func (s *Service) getRecommentsGroups(c context.Context, cid int64, aid int64) (res []int64) {
for i := range s.RecommendsGroups[cid] {
if i != aid {
res = append(res, i)
}
}
return
}

View File

@@ -0,0 +1,211 @@
package service
import (
"context"
"testing"
"time"
"go-common/app/interface/openplatform/article/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
r1 = &model.Recommend{ArticleID: 1, RecImageURL: "xx", RecImageStartTime: 0, RecImageEndTime: 1998603966, Rec: true, RecFlag: true}
r2 = &model.Recommend{ArticleID: 2, RecImageURL: "xx", RecImageStartTime: 0, RecImageEndTime: 1398603966, Rec: true}
r3 = &model.Recommend{ArticleID: 3, Rec: true}
r4 = &model.Recommend{ArticleID: 4, Rec: true}
rs = [][]*model.Recommend{
[]*model.Recommend{r1},
[]*model.Recommend{r2},
[]*model.Recommend{r3},
[]*model.Recommend{r4},
}
cid = int64(4)
recommendAids = map[int64][]int64{
0: []int64{r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID},
cid: []int64{r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID},
}
)
func Test_Recommends_1(t *testing.T) {
Convey("get data from page 1", t, WithCleanCache(func() {
s.setting.ShowRecommendNewArticles = true
//s.updateNewArts(context.TODO(), cid)
s.RecommendsMap = map[int64][][]*model.Recommend{cid: rs}
res, err := s.Recommends(context.TODO(), cid, 1, 3, []int64{}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
// 不改变原始值
So(r1, ShouldResemble, &model.Recommend{ArticleID: 1, RecImageURL: "xx", RecImageStartTime: 0, RecImageEndTime: 1998603966, Rec: true, RecFlag: true})
So(len(s.RecommendsMap[cid]), ShouldEqual, 4)
So(res[0].Recommend, ShouldResemble, model.Recommend{ArticleID: 0, RecImageURL: "xx", RecImageStartTime: 0, RecImageEndTime: 1998603966, Rec: true, RecFlag: true, RecText: "编辑推荐"})
So(res[0].ID, ShouldEqual, 1)
So(res[1].Recommend, ShouldResemble, model.Recommend{ArticleID: 0, RecImageURL: "", RecImageStartTime: 0, RecImageEndTime: 1398603966, Rec: true, RecText: ""})
So(res[1].ID, ShouldEqual, 2)
So(res[2].ID, ShouldEqual, 3)
}))
Convey("get data from page 1 with aids", t, WithCleanCache(func() {
s.setting.ShowRecommendNewArticles = true
//s.updateNewArts(context.TODO(), cid)
s.RecommendsMap = map[int64][][]*model.Recommend{cid: rs}
res, err := s.Recommends(context.TODO(), cid, 1, 2, []int64{1, 2}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 2)
So(res[0].ID, ShouldEqual, 3)
So(res[1].ID, ShouldEqual, 4)
}))
}
func Test_Recommends_2(t *testing.T) {
Convey("get data from page 2", t, WithCleanCache(func() {
//s.updateNewArts(context.TODO(), cid)
s.RecommendsMap = map[int64][][]*model.Recommend{cid: rs}
s.recommendAids = recommendAids
Convey("show new art", func() {
s.setting.ShowRecommendNewArticles = true
res, err := s.Recommends(context.TODO(), cid, 2, 3, []int64{}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
So(res[0].ID, ShouldEqual, 4)
So(res[1].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
So(res[2].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
})
}))
}
func Test_Recommends_recomend_category(t *testing.T) {
Convey("get data from page 1", t, WithCleanCache(func() {
//s.updateNewArts(context.TODO(), 0)
rss := [][]*model.Recommend{[]*model.Recommend{r1, r2, r3, r4}}
s.RecommendsMap = map[int64][][]*model.Recommend{0: rss}
s.recommendAids = recommendAids
Convey("show new art", func() {
s.setting.ShowRecommendNewArticles = true
res, err := s.Recommends(context.TODO(), 0, 2, 3, []int64{}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
So(res[0].ID, ShouldBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
So(res[1].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
So(res[2].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
})
Convey("hide new art", func() {
s.setting.ShowRecommendNewArticles = false
res, err := s.Recommends(context.TODO(), 0, 2, 3, []int64{}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
So(res[0].ID, ShouldBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
})
}))
}
func Test_Recommends_3(t *testing.T) {
Convey("get data from page 3", t, WithCleanCache(func() {
//s.updateNewArts(context.TODO(), cid)
s.RecommendsMap = map[int64][][]*model.Recommend{cid: rs}
s.recommendAids = recommendAids
Convey("show new art", func() {
s.setting.ShowRecommendNewArticles = true
res, err := s.Recommends(context.TODO(), cid, 3, 3, []int64{}, model.FieldDefault)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
So(res[0].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
So(res[1].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
So(res[2].ID, ShouldNotBeIn, r1.ArticleID, r2.ArticleID, r3.ArticleID, r4.ArticleID)
})
}))
Convey("other category no data", t, WithCleanCache(func() {
//s.updateNewArts(context.TODO(), cid)
s.RecommendsMap = map[int64][][]*model.Recommend{cid: rs}
res, err := s.Recommends(context.TODO(), 100, 1, 10, []int64{}, model.FieldDefault)
So(err, ShouldNotBeNil)
So(res, ShouldBeNil)
}))
}
func Test_CalculateRecommends(t *testing.T) {
r10 := &model.Recommend{ArticleID: 1, Position: 2, EndTime: 1}
r12 := &model.Recommend{ArticleID: 2, Position: 2, EndTime: 2}
r13 := &model.Recommend{ArticleID: 2, Position: 2, EndTime: 0}
r14 := &model.Recommend{ArticleID: 2, Position: 2, EndTime: 0}
r20 := &model.Recommend{ArticleID: 1, Position: 1, EndTime: 1}
Convey("diffrent position", t, func() {
res := calculateRecommends([]*model.Recommend{r20, r10})
exp := [][]*model.Recommend{[]*model.Recommend{r10}, []*model.Recommend{r20}}
So(res, ShouldResemble, exp)
})
Convey("same position", t, func() {
res := calculateRecommends([]*model.Recommend{r12, r10})
exp1 := [][]*model.Recommend{[]*model.Recommend{r10, r12}}
exp2 := [][]*model.Recommend{[]*model.Recommend{r12, r10}}
So(res, ShouldBeIn, exp1, exp2)
})
Convey("one no endtime", t, func() {
res := calculateRecommends([]*model.Recommend{r13, r10, r20})
exp := [][]*model.Recommend{[]*model.Recommend{r10}, []*model.Recommend{r20}}
So(res, ShouldResemble, exp)
})
Convey("all no endtime", t, func() {
res := calculateRecommends([]*model.Recommend{r13, r14})
exp1 := [][]*model.Recommend{[]*model.Recommend{r13, r14}}
exp2 := [][]*model.Recommend{[]*model.Recommend{r14, r13}}
So(res, ShouldBeIn, exp1, exp2)
})
Convey("no endtime and have endtime ", t, func() {
res := calculateRecommends([]*model.Recommend{r13, r14, r12})
exp := [][]*model.Recommend{[]*model.Recommend{r12}}
So(res, ShouldResemble, exp)
})
}
func Test_DelRecommendArt(t *testing.T) {
Convey("del recommend", t, WithService(func(s *Service) {
s.RecommendsMap = map[int64][][]*model.Recommend{0: rs}
So(s.RecommendsMap, ShouldNotBeNil)
So(len(s.RecommendsMap[0]), ShouldEqual, 4)
s.DelRecommendArt(0, 1)
time.Sleep(50 * time.Millisecond)
So(s.RecommendsMap[0][0][0], ShouldResemble, r2)
}))
}
func Test_genRecommendArtFromPool(t *testing.T) {
Convey("should generate arts", t, WithService(func(s *Service) {
res := s.genRecommendArtFromPool([][]*model.Recommend{[]*model.Recommend{r1, r2, r3, r4}}, s.c.Article.RecommendRegionLen)
So(len(res), ShouldEqual, 4)
}))
}
func Test_sortRecs(t *testing.T) {
Convey("should sort recommends by ptime", t, WithCleanCache(func() {
a1 := &model.RecommendArt{Meta: model.Meta{ID: 1, PublishTime: 1}}
a1.Rec = true
a2 := &model.RecommendArt{Meta: model.Meta{ID: 2, PublishTime: 2}}
a2.Rec = true
a3 := &model.RecommendArt{Meta: model.Meta{ID: 3, PublishTime: 3}}
a3.Rec = true
res := []*model.RecommendArt{a1, a3, a2}
sortRecs(res)
So(res, ShouldResemble, []*model.RecommendArt{a3, a2, a1})
}))
}
func Test_skyHorseGray(t *testing.T) {
Convey("mid", t, func() {
s.c.Article.SkyHorseGray = []int64{}
s.c.Article.SkyHorseGrayUsers = []int64{123}
So(s.skyHorseGray("1", 123), ShouldBeTrue)
So(s.skyHorseGray("", 12), ShouldBeFalse)
So(s.skyHorseGray("", 0), ShouldBeFalse)
So(s.skyHorseGray("1", 0), ShouldBeFalse)
})
Convey("gray", t, func() {
s.c.Article.SkyHorseGray = []int64{3}
s.c.Article.SkyHorseGrayUsers = []int64{}
So(s.skyHorseGray("1", 123), ShouldBeTrue)
So(s.skyHorseGray("", 3), ShouldBeTrue)
So(s.skyHorseGray("", 5), ShouldBeFalse)
So(s.skyHorseGray("", 0), ShouldBeFalse)
So(s.skyHorseGray("1", 0), ShouldBeFalse)
})
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"regexp"
"strings"
"time"
search "go-common/app/interface/openplatform/article/model/search"
"go-common/library/log"
)
const _sourceType = "article"
// Segment .
func (s *Service) Segment(c context.Context, id int32, content string, withTag int, remarks string) (keywords []string, err error) {
var (
source = _sourceType
trackid = int32(time.Now().Unix())
res *search.TagboxResponse
)
if withTag == 1 {
content = strings.Replace(content, "&nbsp;", " ", -1)
rule := "\\<[\\S\\s]+?\\>"
reg, _ := regexp.Compile(rule)
content = reg.ReplaceAllString(content, "")
}
req := &search.TagboxRequest{
Id: &id,
SourceType: &source,
Content: &content,
Trackid: &trackid,
Remarks: &remarks,
}
if res, err = s.searchRPC.Segment(c, req); err != nil {
log.Error("s.Segment error(%+v), params(%+v)", err, req)
return
}
if *res.ExecCode != int32(0) {
log.Error("creation: s.segment id(%d), code(%d)", id, res.ExecCode)
return
}
keywords = res.GetKeywords()
return
}

View File

@@ -0,0 +1,22 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/conf"
)
var _sentinel = &conf.Sentinel{
EnableSentinel: 1,
DurationSample: 100,
MonitorCountSample: 100,
MonitorRateSample: 100,
DebugSample: 100,
}
// Sentinel .
func (s *Service) Sentinel(c context.Context) *conf.Sentinel {
if s.c.Sentinel == nil {
return _sentinel
}
return s.c.Sentinel
}

View File

@@ -0,0 +1,198 @@
package service
import (
"context"
"math/rand"
"time"
hisrpc "go-common/app/interface/main/history/rpc/client"
tagrpc "go-common/app/interface/main/tag/rpc/client"
"go-common/app/interface/openplatform/article/conf"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
search "go-common/app/interface/openplatform/article/model/search"
account "go-common/app/service/main/account/model"
accrpc "go-common/app/service/main/account/rpc/client"
arcrpc "go-common/app/service/main/archive/api/gorpc"
arcmdl "go-common/app/service/main/archive/model/archive"
coinrpc "go-common/app/service/main/coin/api/gorpc"
favrpc "go-common/app/service/main/favorite/api/gorpc"
filterrpc "go-common/app/service/main/filter/rpc/client"
resrpc "go-common/app/service/main/resource/rpc/client"
thumbuprpc "go-common/app/service/main/thumbup/rpc/client"
xcache "go-common/library/cache"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/log/anticheat"
"go-common/library/net/rpc/warden"
)
const _segmentAddr = "127.0.01:6755"
// cache proc
var cache *xcache.Cache
// Service service
type Service struct {
c *conf.Config
dao *dao.Dao
// rpc
accountRPC *accrpc.Service3
tagRPC *tagrpc.Service
favRPC *favrpc.Service
thumbupRPC thumbuprpc.ThumbupRPC
arcRPC *arcrpc.Service2
coinRPC coinrpc.RPC
resRPC *resrpc.Service
filterRPC *filterrpc.Service
hisRPC *hisrpc.Service
searchRPC search.TagboxServiceClient
categoriesMap map[int64]*artmdl.Category
categoriesReverseMap map[int64][]*artmdl.Category
categoryParents map[int64][]*artmdl.Category
primaryCategories []*artmdl.Category
Categories artmdl.Categories
RecommendsMap map[int64][][]*artmdl.Recommend
RecommendsGroups map[int64]map[int64]bool
recommendChan chan [2]int64
recommendAids map[int64][]int64
setting *artmdl.Setting
activities map[int64]*artmdl.Activity
// infoc
logCh chan interface{}
//banner
bannersMap map[int8][]*artmdl.Banner
actBannersMap map[int8][]*artmdl.Banner
// rank
ranksMap map[int64]bool
sortLimitTime int64
notices []*artmdl.Notice
CheatInfoc *anticheat.AntiCheat
}
// New new
func New(c *conf.Config) *Service {
rand.Seed(time.Now().Unix())
s := &Service{
c: c,
dao: dao.New(c),
accountRPC: accrpc.New3(c.AccountRPC),
tagRPC: tagrpc.New2(c.TagRPC),
favRPC: favrpc.New2(c.FavRPC),
arcRPC: arcrpc.New2(c.ArcRPC),
coinRPC: coinrpc.New(c.CoinRPC),
resRPC: resrpc.New(c.ResRPC),
thumbupRPC: thumbuprpc.New(c.ThumbupRPC),
filterRPC: filterrpc.New(c.FilterRPC),
hisRPC: hisrpc.New(c.HistoryRPC),
searchRPC: searchRPC(c.SearchRPC),
categoriesMap: make(map[int64]*artmdl.Category),
categoriesReverseMap: make(map[int64][]*artmdl.Category),
categoryParents: make(map[int64][]*artmdl.Category),
logCh: make(chan interface{}, 1024),
recommendChan: make(chan [2]int64, 10240),
recommendAids: make(map[int64][]int64),
ranksMap: make(map[int64]bool),
sortLimitTime: int64(time.Duration(c.Article.SortLimitTime) / time.Second),
CheatInfoc: anticheat.New(c.CheatInfoc),
RecommendsGroups: make(map[int64]map[int64]bool),
}
s.loadCategories()
s.loadSettings()
s.loadRanks()
go s.loadCategoriesproc()
go s.loadSettingsproc()
go s.loadNoticeproc()
go s.infocproc()
go s.loadRecommendsproc()
go s.loadBannersproc()
go s.loadActBannersproc()
go s.deleteRecommendproc()
go s.loadActivityproc()
return s
}
func (s *Service) loadRecommendsproc() {
for {
now := time.Now().Unix()
c := context.TODO()
if (s.RecommendsMap == nil) || (now%s.dao.UpdateRecommendsInterval == 0) {
err := s.UpdateRecommends(c)
if err != nil {
dao.PromError("service:更新推荐数据")
time.Sleep(time.Second)
continue
}
if err = s.groupRecommend(c); err != nil {
log.Error("s.groupRecommend error(%+v)", err)
}
}
// 这里不是每秒钟一更新
time.Sleep(time.Second)
}
}
// Close close dao.
func (s *Service) Close() {
s.dao.Close()
}
// Ping check connection success.
func (s *Service) Ping(c context.Context) (err error) {
err = s.dao.Ping(c)
return
}
// UserDisabled check user is disabled
func (s *Service) UserDisabled(c context.Context, mid int64) (res bool, level int, err error) {
arg := account.ArgMid{Mid: mid}
card, err := s.accountRPC.Card3(c, &arg)
if (err == ecode.UserNotExist) || (err == ecode.MemberNotExist) {
return false, 0, nil
}
if err != nil {
dao.PromError("service:用户封禁状态")
log.Error("s.accountRPC.Card2(%+v) err: %+v", arg, err)
return
}
if card.Silence == 1 {
res = true
}
level = int(card.Level)
return
}
func (s *Service) isUpper(c context.Context, mid int64) (res bool, err error) {
arg := &arcmdl.ArgUpCount2{Mid: mid}
var count int
if count, err = s.arcRPC.UpCount2(c, arg); err != nil {
dao.PromError("service:up主投稿")
log.Error("s.arcRPC.UpCount2(%v) err: %+v", mid, err)
return
}
if count > 0 {
res = true
}
return
}
func (s *Service) loadActivityproc() {
for {
if acts, err := s.dao.Activity(context.TODO()); err == nil {
s.activities = acts
}
time.Sleep(time.Minute)
}
}
func init() {
cache = xcache.New(1, 1024)
}
func searchRPC(cfg *warden.ClientConfig) search.TagboxServiceClient {
cc, err := warden.NewClient(cfg).Dial(context.Background(), "discovery://default/search.tagbox")
if err != nil {
panic(err)
}
return search.NewTagboxServiceClient(cc)
}

View File

@@ -0,0 +1,91 @@
package service
import (
"context"
"flag"
"fmt"
"net"
"path/filepath"
"testing"
"time"
"go-common/app/interface/openplatform/article/conf"
"go-common/library/cache/redis"
"github.com/golang/mock/gomock"
. "github.com/smartystreets/goconvey/convey"
)
var (
dataID = int64(175)
noDataID = int64(1000000000)
dataMID = int64(27515309)
s *Service
c = context.TODO()
)
func CleanCache() {
pool := redis.NewPool(conf.Conf.Redis)
pool.Get(c).Do("FLUSHDB")
conn, _ := net.Dial("tcp", conf.Conf.Memcache.Addr)
fmt.Fprintf(conn, "flush_all\n")
conn.Close()
}
func init() {
dir, _ := filepath.Abs("../cmd/convey-test.toml")
flag.Set("conf", dir)
conf.Init()
s = New(conf.Conf)
time.Sleep(time.Second)
}
func WithService(f func(s *Service)) func() {
return func() {
Reset(func() { CleanCache() })
f(s)
}
}
func WithMock(t *testing.T, f func(mock *gomock.Controller)) func() {
return func() {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
f(mockCtrl)
}
}
// func httpMock(method, url string) *gock.Request {
// r := gock.New(url)
// r.Method = strings.ToUpper(method)
// return r
// }
func WithCleanCache(f func()) func() {
return func() {
Reset(func() { CleanCache() })
f()
}
}
/* mysql
INSERT INTO `bilibili_article`.`article_likes_aid_01`(`article_id`, `mid`, `type`) VALUES (2, 1, 1)
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (473, 175, '2017-06-29 12:42:05', '2017-06-19 19:04:51', 39, '夏至未至', '在感情的围城里,他是个彻底的失败者。对于鲍小姐,他失败与无力抵抗;对于苏小姐,他失败于优柔寡断;对于唐小姐,他失败于无所作为;对于孙小姐,他失败于不能决断。虽不知为何他能受到如此多女子的青睐,但他终究', '/bfs/test/e7b129f2ba8fa59337cbcea2f651b0dd4919fae3.jpg', 4, 0, 175, 1, '/bfs/test/e7b129f2ba8fa59337cbcea2f651b0dd4919fae3.jpg', 1497595087, 0, 0, '');
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (474, 176, '2017-06-27 16:47:25', '2017-06-19 19:12:12', 39, '鲍小姐12', '在感情的围城里,他是个彻底的失败者。对于鲍小姐,他失败与无力抵抗;对于苏小姐,他失败于优柔寡断;对于唐小姐,他失败于无所作为;对于孙小姐,他失败于不能决断。虽不知为何他能受到如此多女子的青睐,但他终', '/bfs/archive/48f3e2f7b955c190d218ed1e42868469aebaa5a0.png@0-284-1999-1185a_75q.webp', 4, 0, 1, 0, '/bfs/test/1ce440a94a3e2b4f0d81e44d093715cc1e4eae26.jpg', 1498553268, 0, 0, '');
INSERT INTO `bilibili_article`.`article_likes_mid_01`(`id`, `mtime`, `ctime`, `deleted_time`, `mid`, `article_id`, `type`) VALUES (1, '2017-11-29 15:07:08', '2017-11-29 15:07:08', 0, 1, 2, 1);
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (165, 1, '2017-06-29 12:42:05', '2017-06-19 19:04:51', 39, '夏至未至', '在感情的围城里,他是个彻底的失败者。对于鲍小姐,他失败与无力抵抗;对于苏小姐,他失败于优柔寡断;对于唐小姐,他失败于无所作为;对于孙小姐,他失败于不能决断。虽不知为何他能受到如此多女子的青睐,但他终究', '/bfs/test/e7b129f2ba8fa59337cbcea2f651b0dd4919fae3.jpg', 4, 0, 27515309, 1, '/bfs/test/e7b129f2ba8fa59337cbcea2f651b0dd4919fae3.jpg', 1497595087, 0, 0, '');
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (476, 2, '2017-06-22 00:11:19', '2017-06-19 19:13:48', 29, '《弹幕音乐绘》从没玩过STG的人也能体验到弹幕的乐趣', 'AlphaGO战胜柯洁、Google翻译准确度大幅提升、自动驾驶技术日趋成熟这些无一不在告诉我们人工智能技术正飞速发展。那么如果让人工智能来玩STG会怎么样呢\nSTG也称为弹幕游戏。往往由于满屏幕', '/bfs/test/4d1516ddad93cdb1fc31210b0e2f3526f65d0d6b.jpg', 4, 0, 482, 0, '/bfs/test/4d1516ddad93cdb1fc31210b0e2f3526f65d0d6b.jpg', 1497871027, 0, 0, '');
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (477, 3, '2017-10-19 10:34:07', '2017-06-19 19:21:28', 30, '从没玩过STG的人也能体验到弹幕的乐趣1', 'AlphaGO战胜柯洁、Google翻译准确度大幅提升、自动驾驶技术日趋成熟这些无一不在告诉我们人工智能技术正飞速发展。那么如果让人工智能来玩STG会怎么样呢\n\n视频链接\nSTG也称为弹幕游戏。', '/bfs/test/7797d025e1ded27521f10be42d247e5bc0c85ac1.jpg', 4, 0, 482, 1, '/bfs/test/7797d025e1ded27521f10be42d247e5bc0c85ac1.jpg', 1497871310, 0, 522, '');
INSERT INTO `bilibili_article`.`filtered_articles`(`id`, `article_id`, `mtime`, `ctime`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `attributes`, `words`, `dynamic_intro`) VALUES (478, 4, '2017-06-22 00:11:19', '2017-06-19 19:25:31', 38, 'Product managers for the digital world', 'The role of the product manager is expanding due to the growing importance of data in decision makin', '/bfs/test/fafe56c7488e7def062ae9d77c0fb507b119ea1d.jpg', 4, 0, 2089809, 0, '/bfs/test/fafe56c7488e7def062ae9d77c0fb507b119ea1d.jpg', 1497871547, 0, 0, '');
INSERT INTO `bilibili_article`.`article_notices`(`id`, `title`, `url`, `stime`, `etime`, `state`, `ctime`, `mtime`) VALUES (2, 'wuhao test2 edit', 'http://www.bilibilii.com', '2017-12-12 00:00:00', '2017-12-29 00:00:00', 1, '2017-12-12 18:26:12', '2017-12-15 15:28:47');
INSERT INTO `bilibili_article`.`articles`(`id`, `mtime`, `ctime`, `deleted_time`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `reason`, `attributes`, `words`, `dynamic_intro`, `origin_image_urls`) VALUES (599, '2017-12-13 16:23:39', '2017-09-12 17:55:56', 0, 38, '规划局个乖宝宝?啊啊啊', '巴巴大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和', '/bfs/article/09fad83873aeec34fe7885ffe71c896c4565a8ca.jpg', 4, -2, 88888929, 0, '/bfs/article/09fad83873aeec34fe7885ffe71c896c4565a8ca.jpg', 0, '', 0, 218, '', '/bfs/article/09fad83873aeec34fe7885ffe71c896c4565a8ca.jpg');
INSERT INTO `bilibili_article`.`articles`(`id`, `mtime`, `ctime`, `deleted_time`, `category_id`, `title`, `summary`, `banner_url`, `template_id`, `state`, `mid`, `reprint`, `image_urls`, `publish_time`, `reason`, `attributes`, `words`, `dynamic_intro`, `origin_image_urls`) VALUES (600, '2017-12-13 16:23:39', '2017-09-12 18:11:45', 0, 25, '啦啦啦', '大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美美的团团圆圆一样大热天团团圆圆和和美', '/bfs/article/61ba2a38fce91de39608cf4f57309b53839534a8.jpg', 4, -2, 88888929, 0, '/bfs/article/61ba2a38fce91de39608cf4f57309b53839534a8.jpg', 0, '', 0, 216, '', '/bfs/article/61ba2a38fce91de39608cf4f57309b53839534a8.jpg');
INSERT INTO `bilibili_article`.`lists`(`id`, `mtime`, `ctime`, `deleted_time`, `image_url`, `mid`, `name`, `update_time`) VALUES (1, '2018-01-26 15:44:53', '2018-01-26 15:44:53', 0, '', 100, 'name', '0000-00-00 00:00:00');
INSERT INTO `bilibili_article`.`article_lists`(`id`, `mtime`, `ctime`, `deleted_time`, `article_id`, `list_id`, `position`) VALUES (1, '2018-01-27 13:39:03', '2018-01-27 13:39:03', 0, 165, 1, 0);
INSERT INTO `bilibili_article`.`article_lists`(`id`, `mtime`, `ctime`, `deleted_time`, `article_id`, `list_id`, `position`) VALUES (2, '2018-01-27 13:39:09', '2018-01-27 13:39:09', 0, 476, 1, 1);
INSERT INTO `bilibili_article`.`lists`(`id`, `mtime`, `ctime`, `deleted_time`, `image_url`, `mid`, `name`, `update_time`) VALUES (8, '2018-01-27 18:32:05', '2018-01-26 16:06:57', 0, '', 88888929, 'name', '2018-01-26 16:06:57');
INSERT INTO `bilibili_article`.`article_lists`(`id`, `mtime`, `ctime`, `deleted_time`, `article_id`, `list_id`, `position`) VALUES (9, '2018-01-28 14:57:53', '2018-01-28 14:57:53', 0, 165, 5, 0);
INSERT INTO `bilibili_article`.`article_lists`(`id`, `mtime`, `ctime`, `deleted_time`, `article_id`, `list_id`, `position`) VALUES (10, '2018-01-28 14:58:36', '2018-01-28 14:58:36', 0, 476, 5, 0);
*/

View File

@@ -0,0 +1,109 @@
package service
import (
"context"
"strconv"
"time"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/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 = &artmdl.Setting{}
}
for name, value := range settings {
switch name {
case "apply_open":
var open bool
if open, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ApplyOpen = open
}
case "apply_limit":
var limit int64
if limit, err = strconv.ParseInt(value, 10, 64); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseInt(%v:%v) err: %+v", name, value, err)
} else {
s.setting.ApplyLimit = limit
}
case "frozen_duration":
var duration int64
if duration, err = strconv.ParseInt(value, 10, 64); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseInt(%v:%v) err: %+v", name, value, err)
} else {
s.setting.ApplyFrozenDuration = duration
}
case "show_rec_new_arts":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowRecommendNewArticles = show
}
case "show_rank_note":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowRankNote = show
}
case "show_app_home_rank":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowAppHomeRank = show
}
case "show_later_watch":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowLaterWatch = show
}
case "show_small_window":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowSmallWindow = show
}
case "hotspot":
var show bool
if show, err = strconv.ParseBool(value); err != nil {
dao.PromError("service:配置项无效")
log.Error("strconv.ParseBool(%v: %v) err: %+v", name, value, err)
} else {
s.setting.ShowHotspot = show
}
}
}
return
}
}
func (s *Service) loadSettingsproc() {
for {
time.Sleep(time.Minute)
s.loadSettings()
}
}

View File

@@ -0,0 +1,19 @@
package service
import (
"context"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
)
// AddShare adds share stats count.
func (s *Service) AddShare(c context.Context, id int64, mid int64, ip string) (err error) {
var res *artmdl.Meta
if res, err = s.ArticleMeta(c, id); (err != nil) || (res == nil) || (!res.IsNormal()) {
err = ecode.NothingFound
return
}
s.dao.PubShare(c, mid, id, ip)
return
}

View File

@@ -0,0 +1,624 @@
package service
import (
"context"
"strconv"
"strings"
"go-common/app/interface/openplatform/article/model"
"go-common/library/log"
)
var (
_slideCount = 20
_left = 0
_right = 1
)
// var _slideRecommends = 2
// ViewList .
func (s *Service) ViewList(c context.Context, aid int64, buvid string, from string, ip string, build int, plat int8, mid int64) (pre int64, next int64) {
var (
cid int64
res *model.Article
err error
arts = &model.ArticleViewList{From: from, Buvid: buvid, Plat: plat, Build: build, Mid: mid}
)
if res, err = s.Article(c, aid); err != nil {
res = nil
return
}
if res == nil {
res = nil
return
}
if strings.Contains(from, "_") {
fs := strings.Split(from, "_")
if len(fs) < 2 {
return
}
cid, _ = strconv.ParseInt(fs[1], 10, 64)
switch fs[0] {
case "category":
//分区 天马
if cid == 0 {
arts.From = "recommend"
pre, next = s.listFromRecommends(c, aid, arts)
// arts.From = "skyhorse"
// pre, next = s.listFromSkyHorseEx(c, aid, cid, arts, int64(res.PublishTime), res.Author.Mid)
} else {
arts.From = "default"
pre, next = s.listByAuthor(c, aid, res.Author.Mid, int64(res.PublishTime), arts)
}
case "rank":
//排行榜
pre, next = s.listFromRank(c, aid, cid, arts, ip)
case "readlist":
//文集
pre, next = s.listFromReadList(c, aid, arts)
default:
//default
arts.From = "default"
pre, next = s.listByAuthor(c, aid, res.Author.Mid, int64(res.PublishTime), arts)
}
} else {
switch from {
// case "mainCard":
// //天马
// arts.From = "skyhorse"
// pre, next = s.listFromSkyHorseEx(c, aid, 0, arts, int64(res.PublishTime), res.Author.Mid)
case "favorite":
// TODO
pre, next = s.validFavsList(c, aid, arts, true)
case "records":
// TODO
// pre, next = 0, 0
// pre, next = s.historyCursor(c, aid, arts, true)
case "readlist":
//文集
pre, next = s.listFromReadList(c, aid, arts)
case "articleSlide":
//滑动
pre, next, arts = s.listFromCache(c, aid, arts.Buvid)
default:
//作者其他的文章
arts.From = "default"
pre, next = s.listByAuthor(c, aid, res.Author.Mid, int64(res.PublishTime), arts)
}
}
s.dao.AddCacheListArtsId(c, buvid, arts)
return
}
// func (s *Service) listFromSkyHorseEx(c context.Context, aid int64, cid int64, arts *model.ArticleViewList, pt int64, authorMid int64) (pre int64, next int64) {
// pre, next = s.listFromSkyHorse(c, aid, cid, arts)
// if pre == 0 && next == 0 {
// arts.From = "default"
// pre, next = s.listByAuthor(c, aid, authorMid, pt, arts)
// }
// return
// }
func (s *Service) listFromCache(c context.Context, aid int64, buvid string) (pre int64, next int64, arts *model.ArticleViewList) {
var (
res *model.Article
err error
position = -1
)
if arts, err = s.dao.CacheListArtsId(c, buvid); err != nil {
log.Error("s.dao.CacheListArtsId, error(%+v)", err)
return
}
if arts == nil || arts.Aids == nil {
return
}
for i, id := range arts.Aids {
if id == aid {
position = i
break
}
}
if position == -1 {
return
}
switch arts.From {
case "recommend":
if position == 0 {
if len(arts.Aids) > 1 {
next = arts.Aids[1]
}
pre = s.newRecommend(c, arts, _left)
// pre = s.newSkyHorse(c, arts, _left)
arts.Position = position
return
}
if position == len(arts.Aids)-1 {
if len(arts.Aids) > 1 {
pre = arts.Aids[len(arts.Aids)-2]
}
next = s.newRecommend(c, arts, _right)
// next = s.newSkyHorse(c, arts, _right)
arts.Position = position
return
}
case "default":
if position == 0 || position == len(arts.Aids)-1 {
if res, err = s.Article(c, aid); err != nil || res == nil {
return
}
pre, next = s.listByAuthor(c, aid, res.Author.Mid, int64(res.PublishTime), arts)
arts.Position = position
return
}
next = arts.Aids[position-1]
pre = arts.Aids[position+1]
return
case "records":
return
// if position == len(arts.Aids)-1 {
// if position > 0 {
// pre = arts.Aids[position-1]
// }
// _, next = s.historyCursor(c, aid, arts, false)
// return
// }
case "favorite":
if position == len(arts.Aids)-1 {
if position > 0 {
pre = arts.Aids[position-1]
}
_, next = s.validFavsList(c, aid, arts, false)
return
}
}
if position > 0 {
pre = arts.Aids[position-1]
}
if position < len(arts.Aids)-1 {
next = arts.Aids[position+1]
}
return
}
func (s *Service) listFromReadList(c context.Context, aid int64, arts *model.ArticleViewList) (pre int64, next int64) {
var (
listID int64
list *model.List
artsMetas []*model.ListArtMeta
ok bool
)
lists, err := s.dao.ArtsList(c, []int64{aid})
if err != nil {
log.Error("s.dao.ArtsList, error(%+v)", err)
return
}
if list, ok = lists[aid]; !ok {
return
}
listID = list.ID
if artsMetas, err = s.dao.ListArts(c, listID); err != nil {
log.Error("s.dao.ListArts, error(%+v)", err)
return
}
if artsMetas == nil {
return
}
for i, art := range artsMetas {
if art.ID == aid {
arts.Position = i
}
arts.Aids = append(arts.Aids, art.ID)
}
if arts.Position > 0 {
pre = artsMetas[arts.Position-1].ID
}
if arts.Position < len(artsMetas)-1 {
next = artsMetas[arts.Position+1].ID
}
return
}
func (s *Service) listByAuthor(c context.Context, aid int64, mid int64, pt int64, arts *model.ArticleViewList) (pre int64, next int64) {
var (
beforeAids, afterAids, tmpAids []int64
aidTimes [][2]int64
exist bool
j int
err error
metas map[int64]*model.Meta
aids []int64
addCache = true
position = -1
)
if exist, err = s.dao.ExpireUpperCache(c, mid); err != nil {
addCache = false
err = nil
} else if exist {
if beforeAids, afterAids, err = s.dao.MoreArtsCaches(c, mid, int64(pt), _slideCount); err != nil {
addCache = false
exist = false
}
if len(beforeAids)+len(afterAids) == 0 {
return
}
for i := len(beforeAids) - 1; i >= 0; i-- {
tmpAids = append(tmpAids, beforeAids[i])
}
tmpAids = append(tmpAids, aid)
tmpAids = append(tmpAids, afterAids...)
} else {
if aidTimes, err = s.dao.UpperPassed(c, mid); err != nil {
log.Error("s.dao.UpperPassed, error(%+v)", err)
return
}
if addCache {
cache.Save(func() {
s.dao.AddUpperCaches(context.TODO(), map[int64][][2]int64{mid: aidTimes})
})
}
for i := len(aidTimes) - 1; i >= 0; i-- {
aidTime := aidTimes[i]
tmpAids = append(tmpAids, aidTime[0])
}
}
if metas, err = s.ArticleMetas(c, tmpAids); err != nil {
log.Error("s.ArticleMetas, error(%+v)", err)
return
}
//过滤禁止显示的稿件
filterNoDistributeArtsMap(metas)
if len(metas) == 0 {
return
}
for _, id := range tmpAids {
if _, ok := metas[id]; !ok {
continue
}
if id == aid {
position = j
}
j++
aids = append(aids, id)
}
if position == -1 {
return
}
if position > 0 {
next = aids[position-1]
}
if position < len(aids)-1 {
pre = aids[position+1]
}
arts.Position = position
arts.Aids = aids
return
}
func (s *Service) listFromRank(c context.Context, aid int64, cid int64, arts *model.ArticleViewList, ip string) (pre int64, next int64) {
var (
exist bool
err error
aids []int64
rank model.RankResp
addCache = true
position = -1
)
s.dao.DelCacheListArtsId(c, arts.Buvid)
if !s.ranksMap[cid] {
return
}
if exist, err = s.dao.ExpireRankCache(c, cid); err != nil {
addCache = false
err = nil
}
if exist {
if rank, err = s.dao.RankCache(c, cid); err != nil {
exist = false
err = nil
addCache = false
}
} else {
if rank, err = s.dao.Rank(c, cid, ip); err != nil {
if rank, err = s.dao.RankCache(c, cid); err != nil {
log.Error("s.dao.RankCache, error(%+v)", err)
return
}
} else {
if addCache && len(rank.List) > 0 {
cache.Save(func() {
s.dao.AddRankCache(context.TODO(), cid, rank)
})
}
}
}
if len(rank.List) == 0 {
return
}
for i, a := range rank.List {
aids = append(aids, a.Aid)
if a.Aid == aid {
position = i
}
}
if position == -1 {
return
}
arts.Position = position
arts.Aids = aids
if position > 0 {
pre = aids[position-1]
}
if position < len(aids)-1 {
next = aids[position+1]
}
return
}
// func (s *Service) listFromSkyHorse(c context.Context, aid int64, cid int64, arts *model.ArticleViewList) (pre int64, next int64) {
// var (
// beforeAids, afterAids, tmpAids, aids []int64
// err error
// position = -1
// half = _slideRecommends / 2
// )
// if tmpAids, err = s.filterAidsFromSkyHorse(c, aid, arts, _slideRecommends); err != nil || len(tmpAids) == 0 {
// return
// }
// if len(tmpAids) < half {
// half = len(tmpAids) / 2
// }
// beforeAids = make([]int64, half)
// afterAids = make([]int64, len(tmpAids)-half)
// copy(beforeAids, tmpAids[:half])
// copy(afterAids, tmpAids[half:])
// position = half
// aids = append([]int64{}, beforeAids...)
// aids = append(aids, aid)
// aids = append(aids, afterAids...)
// arts.Position = position
// arts.Aids = aids
// if len(beforeAids) > 0 {
// pre = beforeAids[len(beforeAids)-1]
// }
// if len(afterAids) > 0 {
// next = afterAids[0]
// }
// return
// }
// func (s *Service) newSkyHorse(c context.Context, arts *model.ArticleViewList, side int) (aid int64) {
// var (
// aids []int64
// err error
// )
// if aids, err = s.filterAidsFromSkyHorse(c, arts.Aids[arts.Position], arts, _slideRecommends/2); err != nil || len(aids) == 0 {
// return
// }
// aids = uniqIDs(aids, arts.Aids)
// if len(aids) == 0 {
// return
// }
// if side == _left {
// aid = aids[len(aids)-1]
// arts.Aids = append(aids, arts.Aids...)
// arts.Position += len(aids)
// } else {
// aid = aids[0]
// arts.Aids = append(arts.Aids, aids...)
// }
// return
// }
// func (s *Service) filterAidsFromSkyHorse(c context.Context, aid int64, arts *model.ArticleViewList, size int) (aids []int64, err error) {
// var (
// tmpIds []int64
// sky *model.SkyHorseResp
// metas map[int64]*model.Meta
// )
// if sky, err = s.dao.SkyHorse(c, arts.Mid, arts.Build, arts.Buvid, arts.Plat, size); err != nil {
// return
// }
// if len(sky.Data) == 0 {
// return
// }
// for _, item := range sky.Data {
// tmpIds = append(tmpIds, item.ID)
// }
// if metas, err = s.ArticleMetas(c, tmpIds); err != nil {
// log.Error("s.ArticleMetas, error(%+v)", err)
// return
// }
// //过滤禁止显示的稿件
// filterNoDistributeArtsMap(metas)
// filterNoRegionArts(metas)
// if len(metas) == 0 {
// return
// }
// for _, meta := range metas {
// if meta.ID == aid {
// continue
// }
// aids = append(aids, meta.ID)
// }
// return
// }
func (s *Service) validFavsList(c context.Context, aid int64, arts *model.ArticleViewList, ok bool) (pre int64, next int64) {
arts.Position++
var (
favs []*model.Favorite
err error
page = &model.Page{Total: arts.Position*_slideCount + 1}
)
for ok && page.Total > arts.Position*_slideCount {
if favs, page, err = s.Favs(c, arts.Mid, 0, arts.Position, _slideCount, ""); err != nil {
return
}
for _, fav := range favs {
if !fav.Valid {
continue
}
arts.Aids = append(arts.Aids, fav.ID)
}
for i, id := range arts.Aids {
if id != aid {
continue
}
if i > 0 {
pre = arts.Aids[i-1]
}
if i < len(arts.Aids)-1 {
next = arts.Aids[i+1]
ok = false
}
}
arts.Position++
}
if next > 0 {
return
}
ok = true
for ok && page.Total > arts.Position*_slideCount {
if favs, page, err = s.Favs(c, arts.Mid, 0, arts.Position, _slideCount, ""); err != nil {
return
}
for _, fav := range favs {
if !fav.Valid {
continue
}
if next == 0 {
next = fav.ID
}
ok = false
arts.Aids = append(arts.Aids, fav.ID)
}
arts.Position++
}
return
}
// func (s *Service) historyCursor(c context.Context, aid int64, arts *model.ArticleViewList, ok bool) (pre int64, next int64) {
// var (
// res []*history.Resource
// err error
// viewAt = int64(arts.Position)
// aids []int64
// )
// for ok {
// arg := &history.ArgCursor{Mid: arts.Mid, Businesses: []string{"article", "article-list"}, Ps: _slideCount, ViewAt: viewAt}
// if res, err = s.hisRPC.HistoryCursor(c, arg); err != nil || len(res) < 2 {
// return
// }
// for _, r := range res {
// viewAt = r.Unix
// id := r.Oid
// if r.TP == history.TypeCorpus {
// id = r.Cid
// }
// aids = append(aids, id)
// }
// for i, id := range aids {
// if id != aid {
// continue
// }
// ok = false
// if i > 0 {
// pre = aids[i-1]
// }
// if i < len(aids)-1 {
// next = aids[i+1]
// }
// }
// arts.Aids = append(arts.Aids, aids...)
// arts.Position = int(viewAt)
// }
// if next > 0 {
// return
// }
// arg := &history.ArgCursor{Mid: arts.Mid, Businesses: []string{"article", "article-list"}, Ps: _slideCount, ViewAt: viewAt}
// if res, err = s.hisRPC.HistoryCursor(c, arg); err != nil || len(res) == 0 {
// return
// }
// for _, r := range res {
// viewAt = r.Unix
// id := r.Oid
// if r.TP == history.TypeCorpus {
// id = r.Cid
// }
// if next == 0 {
// next = id
// }
// arts.Aids = append(arts.Aids, id)
// }
// arts.Position = int(viewAt)
// return
// }
func (s *Service) listFromRecommends(c context.Context, aid int64, arts *model.ArticleViewList) (pre int64, next int64) {
recommends := s.genRecommendArtFromPool(s.RecommendsMap[_recommendCategory], _slideCount)
tmpAids, _ := s.dealRecommends(recommends)
var aids []int64
for _, id := range tmpAids {
if id != aid {
aids = append(aids, id)
}
}
if len(aids) == 0 {
return
}
if len(aids) == 1 {
next = aids[0]
arts.Aids = append([]int64{aid}, aids[0])
return
}
half := len(aids) / 2
beforeAids := make([]int64, half)
afterAids := make([]int64, len(aids)-half)
copy(beforeAids, aids[:half])
copy(afterAids, aids[half:])
aids = append([]int64{}, beforeAids...)
aids = append(aids, aid)
aids = append(aids, afterAids...)
arts.Position = half
arts.Aids = aids
pre = arts.Aids[half-1]
next = arts.Aids[half+1]
return
}
func (s *Service) newRecommend(c context.Context, arts *model.ArticleViewList, side int) (res int64) {
var (
m = make(map[int64]bool)
nids []int64
)
recommends := s.genRecommendArtFromPool(s.RecommendsMap[_recommendCategory], _slideCount)
aids, _ := s.dealRecommends(recommends)
if len(aids) == 0 {
return
}
for _, aid := range arts.Aids {
m[aid] = true
}
for _, aid := range aids {
if !m[aid] {
nids = append(nids, aid)
}
}
if len(nids) == 0 {
return
}
if side == _left {
res = nids[len(nids)-1]
arts.Aids = append(nids, arts.Aids...)
}
if side == _right {
res = nids[0]
arts.Aids = append(arts.Aids, nids...)
}
return
}

View File

@@ -0,0 +1,125 @@
package service
import (
"context"
"time"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/log"
xtime "go-common/library/time"
)
// UpdateSortCache update sort cache
func (s *Service) UpdateSortCache(c context.Context, aid int64, changed [][2]int64, ip string) (err error) {
var meta *artmdl.Meta
if meta, err = s.ArticleMeta(c, aid); (err != nil) || (meta == nil) {
return
}
cids := []int64{meta.Category.ID, meta.Category.ParentID}
var exist bool
for _, cid := range cids {
for _, ch := range changed {
field := int(ch[0])
value := ch[1]
if exist, err = s.dao.ExpireSortCache(c, cid, field); err != nil {
dao.PromError("sort:更新文章排序缓存")
return
}
if exist && s.shouldAddSort(meta.PublishTime, field) {
if err = s.dao.AddSortCache(c, cid, field, aid, value); err != nil {
return
}
}
}
}
return
}
func (s *Service) shouldAddSort(t xtime.Time, field int) bool {
if (field != artmdl.FieldLike) && (field != artmdl.FieldReply) && (field != artmdl.FieldFav) && (field != artmdl.FieldView) {
return true
}
limitTime := time.Now().Unix() - s.sortLimitTime
return int64(t) > limitTime
}
func (s *Service) delArtSortCache(c context.Context, aid int64) (err error) {
var (
root, cid int64
)
if root, cid, err = s.RootCategory(c, aid); err != nil {
dao.PromError("sort:删除文章缓存查找分类")
log.Error("s.RootCategory(%d,%d) error(%+v)", aid, cid, err)
return
}
cids := []int64{root, cid, _recommendCategory}
err = s.delArtSortCacheFromCid(c, aid, cids...)
return
}
func (s *Service) delArtSortCacheFromCid(c context.Context, aid int64, cids ...int64) (err error) {
for _, cid := range cids {
for _, field := range artmdl.SortFields {
if err = s.dao.DelSortCache(c, cid, field, aid); err != nil {
return
}
}
}
return
}
func (s *Service) addArtSortCache(c context.Context, art *artmdl.Meta) (err error) {
if art == nil {
return
}
cids := []int64{art.Category.ID, _recommendCategory}
var oldRoot int64
if oldRoot, err = s.CategoryToRoot(art.Category.ID); err != nil {
dao.PromError("sort:增加文章缓存查找分类")
log.Error("s.CategoryToRoot(%d,%d) error(%+v)", art.ID, art.Category.ID, err)
err = nil
} else {
cids = append(cids, oldRoot)
}
if art.Stats == nil {
if stat, e := s.stat(c, art.ID); (e == nil) && (stat != nil) {
art.Stats = stat
} else {
art.Stats = new(artmdl.Stats)
}
}
var exist bool
for _, cid := range cids {
for _, field := range artmdl.SortFields {
if exist, err = s.dao.ExpireSortCache(c, cid, field); err != nil {
dao.PromError("sort:增加最新文章")
return
}
if !exist || !s.shouldAddSort(art.PublishTime, field) {
continue
}
var value int64
switch field {
case artmdl.FieldNew:
value = int64(art.PublishTime)
case artmdl.FieldLike:
value = art.Stats.Like
case artmdl.FieldReply:
value = art.Stats.Reply
case artmdl.FieldFav:
value = art.Stats.Favorite
case artmdl.FieldView:
value = art.Stats.View
default:
dao.PromError("sort:新增最新文章-排序分类错误")
log.Error("addArtSortCache sort field error: %v", field)
}
if err = s.dao.AddSortCache(c, cid, field, art.ID, value); err != nil {
dao.PromError("sort:新增最新文章")
}
}
}
return
}

View File

@@ -0,0 +1,86 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/sync/errgroup"
)
// SetStat sets article's stat.
func (s *Service) SetStat(c context.Context, aid int64, stat *artmdl.Stats) (err error) {
group, ctx := errgroup.WithContext(c)
group.Go(func() error {
return s.dao.AddArticleStatsCache(ctx, aid, stat)
})
group.Go(func() error {
s.SendMessage(ctx, aid, stat)
return nil
})
err = group.Wait()
return
}
func (s *Service) stat(c context.Context, aid int64) (res *artmdl.Stats, err error) {
var addCache = true
if res, err = s.dao.ArticleStatsCache(c, aid); err != nil {
err = nil
addCache = false
} else if res != nil {
return
}
res, err = s.dao.ArticleStats(c, aid)
if res != nil && addCache {
cache.Save(func() {
s.dao.AddArticleStatsCache(context.TODO(), aid, res)
})
}
return
}
func (s *Service) stats(c context.Context, aids []int64) (res map[int64]*artmdl.Stats, err error) {
var (
cachedArtStats map[int64]*artmdl.Stats
missed []int64
missedArtsStats map[int64]*artmdl.Stats
addCache = true
)
if cachedArtStats, missed, err = s.dao.ArticlesStatsCache(c, aids); err != nil {
addCache = false
missed = aids
err = nil
}
if len(missed) > 0 {
if missedArtsStats, err = s.dao.ArticlesStats(c, missed); err != nil {
addCache = false
dao.PromError("stat:文章计数")
err = nil
}
}
res = make(map[int64]*artmdl.Stats)
for aid, art := range cachedArtStats {
res[aid] = art
}
if missedArtsStats == nil {
missedArtsStats = make(map[int64]*artmdl.Stats)
} else {
for aid, art := range missedArtsStats {
res[aid] = art
}
}
if addCache {
for _, aid := range aids {
if _, ok := res[aid]; !ok {
missedArtsStats[aid] = new(artmdl.Stats)
}
}
cache.Save(func() {
for id, stats := range missedArtsStats {
s.dao.AddArticleStatsCache(context.TODO(), id, stats)
}
})
}
return
}

View File

@@ -0,0 +1 @@
package service

View File

@@ -0,0 +1,68 @@
package service
import (
"context"
"go-common/library/log"
"strings"
"go-common/app/interface/main/tag/model"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
)
// bindTags add tags for article by upper.
func (s *Service) bindTags(c context.Context, mid, aid int64, tags []string, ip string) (err error) {
arg := &model.ArgBind{Type: model.PicResType, Mid: mid, Oid: aid, Names: tags}
if err = s.tagRPC.UpBind(c, arg); err != nil {
dao.PromError("rpc:bind tag")
log.Error("s.tagRPC.UpBind(%v) error(%+v)", arg, err)
}
return
}
// Tags gets article tags.
func (s *Service) Tags(c context.Context, aid int64, skipAct bool) (res []*artmdl.Tag, err error) {
var (
tags map[int64][]*model.Tag
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] {
if skipAct && t.Type == 4 {
continue
}
tag := &artmdl.Tag{Tid: t.ID, Name: t.Name}
res = append(res, tag)
}
return
}
// BindTags bind tags with activity
func (s *Service) BindTags(c context.Context, mid, aid int64, tags []string, ip string, activityID int64) (err error) {
activity := s.activities[activityID]
if (activityID) > 0 && (activity != nil) && (activity.Tags != "") {
actTags := strings.Split(activity.Tags, ",")
tags = mergeActivityTags(tags, actTags)
}
return s.bindTags(c, mid, aid, tags, ip)
}
func mergeActivityTags(tags, actTags []string) (res []string) {
m := map[string]bool{}
for _, t := range tags {
m[t] = true
res = append(res, t)
}
for _, t := range actTags {
if !m[t] {
res = append(res, t)
}
}
return
}

View File

@@ -0,0 +1,55 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
// func Test_Tag(t *testing.T) {
// Convey("BindTags", t, WithService(func(s *Service) {
// err := s.BindTags(context.TODO(), 1, 1, []string{"tag1", "tag2", "tag3"}, "")
// So(err, ShouldBeNil)
// }))
// Convey("Tags", t, WithService(func(s *Service) {
// aids := []int64{1}
// res, err := s.Tags(context.TODO(), aids)
// So(err, ShouldBeNil)
// So(res, ShouldNotBeEmpty)
// len := len(res[1])
// t.Logf("result: %+v, tags length: %d", res, len)
// So(len, ShouldEqual, 3)
// }))
// Convey("BindTags 2", t, WithService(func(s *Service) {
// err := s.BindTags(context.TODO(), 1, 1, []string{"tag4", "tag5"}, "")
// So(err, ShouldBeNil)
// }))
// Convey("Tags 2", t, WithService(func(s *Service) {
// aids := []int64{1}
// res, err := s.Tags(context.TODO(), aids)
// So(err, ShouldBeNil)
// So(res, ShouldNotBeEmpty)
// len := len(res[1])
// t.Logf("result: %+v, tags length: %d", res, len)
// So(len, ShouldEqual, 2)
// }))
// }
func Test_mergeActivityTags(t *testing.T) {
Convey("no act tag add tags", t, func() {
tags := mergeActivityTags([]string{"tag1", "tag2", "tag3"}, []string{"act1", "act2"})
So(tags, ShouldResemble, []string{"tag1", "tag2", "tag3", "act1", "act2"})
})
Convey("1 tag add tags", t, func() {
tags := mergeActivityTags([]string{"tag1", "tag2", "tag3", "act1"}, []string{"act2"})
So(tags, ShouldResemble, []string{"tag1", "tag2", "tag3", "act1", "act2"})
})
Convey("already has tags ", t, func() {
tags := mergeActivityTags([]string{"tag1", "tag2", "tag3", "act1", "act2"}, []string{"act1", "act2"})
So(tags, ShouldResemble, []string{"tag1", "tag2", "tag3", "act1", "act2"})
})
Convey("act tags blank ", t, func() {
tags := mergeActivityTags([]string{"tag1", "tag2", "tag3"}, []string{})
So(tags, ShouldResemble, []string{"tag1", "tag2", "tag3"})
})
}

View File

@@ -0,0 +1,179 @@
package service
import (
"context"
"sort"
"sync"
"go-common/app/interface/openplatform/article/dao"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
// UpArticleMetas list up article metas
func (s *Service) UpArticleMetas(c context.Context, mid int64, pn int, ps int, sortType int) (res *artmdl.UpArtMetas, err error) {
var (
ups map[int64][]*artmdl.Meta
start = (pn - 1) * ps
end = start + ps - 1 // from cache, end-1
metas []*artmdl.Meta
)
if sortType == artmdl.FieldDefault {
ups, err = s.UpsArticleMetas(c, []int64{mid}, start, end)
} else {
ups, err = s.UpsArticleMetas(c, []int64{mid}, 0, -1)
}
if err != nil {
return
}
metas = ups[mid]
if sortType != artmdl.FieldDefault {
end++
switch sortType {
case artmdl.FieldFav:
sort.Slice(metas, func(i, j int) bool { return metas[i].Stats.Favorite > metas[j].Stats.Favorite })
case artmdl.FieldView:
sort.Slice(metas, func(i, j int) bool { return metas[i].Stats.View > metas[j].Stats.View })
}
if start > len(metas) {
start = len(metas)
}
if end > len(metas) {
end = len(metas)
}
metas = metas[start:end]
}
res = new(artmdl.UpArtMetas)
res.Articles = filterNoDistributeArts(metas)
res.Pn = pn
res.Ps = ps
if res.Count, err = s.UpperArtsCount(c, mid); err != nil {
dao.PromError("upper:获取作者文章数")
}
return
}
// UpsArticleMetas list up article metas
func (s *Service) UpsArticleMetas(c context.Context, mids []int64, start int, end int) (res map[int64][]*artmdl.Meta, err error) {
var (
group = &errgroup.Group{}
mutex = &sync.Mutex{}
)
res = make(map[int64][]*artmdl.Meta)
upArtIDs, _ := s.upArtIDs(c, mids, start, end)
for mid, ids := range upArtIDs {
mid := mid
ids := ids
group.Go(func() (err error) {
var (
artsm map[int64]*artmdl.Meta
arts []*artmdl.Meta
)
artsm, _ = s.FeedArticleMetas(c, ids)
for _, art := range artsm {
arts = append(arts, art)
}
mutex.Lock()
sort.Sort(artmdl.Metas(arts))
res[mid] = arts
mutex.Unlock()
return
})
}
group.Wait()
return
}
func (s *Service) upArtIDs(c context.Context, mids []int64, start, end int) (res map[int64][]int64, err error) {
var (
exists map[int64]bool
addCache = true
missMids = make([]int64, 0, len(mids))
cacheMids = make([]int64, 0, len(mids))
group = &errgroup.Group{}
cacheUpArtIDs map[int64][]int64
missUpArts map[int64][][2]int64
// missUpArtIDs map[int64][][2]int64
)
res = make(map[int64][]int64)
if exists, err = s.dao.ExpireUppersCache(c, mids); err != nil {
addCache = false
err = nil
}
for _, mid := range mids {
if !exists[mid] {
missMids = append(missMids, mid)
} else {
cacheMids = append(cacheMids, mid)
}
}
// from cache
group.Go(func() (err error) {
if cacheUpArtIDs, err = s.dao.UppersCaches(c, cacheMids, start, end); err != nil {
dao.PromError("upper:获取up主文章列表")
}
return
})
group.Go(func() (err error) {
if len(missMids) > 0 {
missUpArts, err = s.dao.UppersPassed(c, missMids)
}
return
})
group.Wait()
for mid, ids := range cacheUpArtIDs {
res[mid] = ids
}
for mid, arts := range missUpArts {
var ids []int64
for _, art := range arts {
ids = append(ids, art[0])
}
if (start == 0) && (end == -1) {
res[mid] = ids
continue
}
if len(ids) <= start {
res[mid] = []int64{}
continue
}
if len(ids) < end {
res[mid] = ids[start:]
} else {
res[mid] = ids[start:end]
}
}
if addCache && (len(missUpArts) > 0) {
s.dao.AddUpperCaches(c, missUpArts)
}
return
}
// UpperArtsCount count upper article
func (s *Service) UpperArtsCount(c context.Context, mid int64) (res int, err error) {
var (
exists map[int64]bool
arts map[int64][][2]int64
)
if exists, err = s.dao.ExpireUppersCache(c, []int64{mid}); err != nil {
err = nil
return
}
if exists[mid] {
if res, err = s.dao.UpperArtsCountCache(c, mid); err == nil {
return
}
log.Error("s.dao.UpperArtsCountCache(%v) err: %+v", mid, err)
}
if arts, err = s.dao.UppersPassed(c, []int64{mid}); err != nil {
dao.PromError("upper:获取作者文章列表")
return
}
res = len(arts[mid])
cache.Save(func() {
s.dao.AddUpperCaches(context.TODO(), arts)
})
return
}

View File

@@ -0,0 +1,39 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UpArticleMetas(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
res, err := s.UpArticleMetas(context.TODO(), dataMID, 1, 20, 0)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
}))
Convey("no data", t, WithService(func(s *Service) {
res, err := s.UpArticleMetas(context.TODO(), dataMID, 20000, 20, 0)
So(err, ShouldBeNil)
So(res.Articles, ShouldBeEmpty)
}))
}
func Test_UpsArticles(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
res, err := s.UpsArticleMetas(context.TODO(), []int64{dataMID}, 1, 20)
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
}))
Convey("get count", t, WithService(func(s *Service) {
res, err := s.UpperArtsCount(context.TODO(), dataMID)
So(err, ShouldBeNil)
So(res, ShouldBeGreaterThan, 0)
}))
Convey("no data", t, WithService(func(s *Service) {
res, err := s.UpsArticleMetas(context.TODO(), []int64{dataID}, 20000, 20)
So(err, ShouldBeNil)
So(res[dataID], ShouldBeEmpty)
}))
}

View File

@@ -0,0 +1,33 @@
package service
import (
"context"
"go-common/app/interface/openplatform/article/model"
"go-common/library/ecode"
)
// UserNoticeState .
func (s *Service) UserNoticeState(c context.Context, mid int64) (res model.NoticeState, err error) {
state, err := s.dao.NoticeState(c, mid)
if err != nil {
return
}
res = model.NewNoticeState(state)
return
}
// UpdateUserNoticeState .
func (s *Service) UpdateUserNoticeState(c context.Context, mid int64, typ string) (err error) {
state, err := s.UserNoticeState(c, mid)
if err != nil {
return
}
if _, ok := state[typ]; !ok {
err = ecode.RequestErr
return
}
state[typ] = true
err = s.dao.UpdateNoticeState(c, mid, state.ToInt64())
return
}

View File

@@ -0,0 +1,31 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UpdateUserNoticeState(t *testing.T) {
Convey("update state", t, func() {
err := s.UpdateUserNoticeState(context.TODO(), 100, "lead")
So(err, ShouldBeNil)
Convey("get lead data", func() {
res, err := s.UserNoticeState(context.TODO(), 100)
So(err, ShouldBeNil)
So(res["lead"], ShouldBeTrue)
})
Convey("update new and get lead data", func() {
err := s.UpdateUserNoticeState(context.TODO(), 100, "new")
res, err := s.UserNoticeState(context.TODO(), 100)
So(err, ShouldBeNil)
So(res["lead"], ShouldBeTrue)
So(res["new"], ShouldBeTrue)
})
})
Convey("update invalid state", t, func() {
err := s.UpdateUserNoticeState(context.TODO(), 100, "invalid")
So(err, ShouldNotBeNil)
})
}

View File

@@ -0,0 +1,139 @@
package service
import (
"context"
"strconv"
"go-common/app/interface/openplatform/article/dao"
"go-common/app/interface/openplatform/article/model"
account "go-common/app/service/main/account/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
// ViewInfo get view info
func (s *Service) ViewInfo(c context.Context, mid, id int64, ip string, cheat *model.CheatInfo, plat int8, from string) (res *model.ViewInfo, err error) {
var art *model.Meta
group := &errgroup.Group{}
if art, err = s.ArticleMeta(c, id); (err != nil) || (art == nil) || (!art.IsNormal()) {
err = ecode.NothingFound
return
}
res = &model.ViewInfo{
Title: art.Title,
BannerURL: art.BannerURL,
Mid: art.Author.Mid,
ImageURLs: art.ImageURLs,
OriginImageURLs: art.OriginImageURLs,
ShowLaterWatch: s.setting.ShowLaterWatch,
ShowSmallWindow: s.setting.ShowSmallWindow,
}
res.Shareable = !art.AttrVal(model.AttrBitNoDistribute)
if mid > 0 {
res.IsAuthor, _, _ = s.IsAuthor(c, mid)
group.Go(func() error {
res.Like, _ = s.isLike(c, mid, id)
return nil
})
group.Go(func() error {
if art.Author != nil {
res.Attention, _ = s.isAttention(c, mid, art.Author.Mid)
}
return nil
})
group.Go(func() error {
res.Favorite, _ = s.IsFav(c, mid, id)
return nil
})
group.Go(func() error {
res.Coin, _ = s.Coin(c, mid, id, ip)
return nil
})
}
group.Go(func() error {
if stat, e := s.stat(c, id); (e == nil) && (stat != nil) {
res.Stats = *stat
res.Stats.Dynamic, _ = s.dao.DynamicCount(c, id)
}
return nil
})
group.Go(func() error {
var lid int64
lists, _ := s.dao.ArtsList(c, []int64{id})
if lists[id] != nil {
res.InList = true
lid = lists[id].ID
}
if mid > 0 {
s.AddHistory(c, mid, id, lid, ip, plat, from)
}
return nil
})
group.Go(func() error {
info, _ := s.authorDetail(c, art.Author.Mid)
if info != nil {
res.AuthorName = info.Name
}
return nil
})
cache.Save(func() {
if mid == 0 || from == "articleSlide" {
return
}
if info, _ := s.accountInfo(context.TODO(), mid); info != nil {
cheat.Lv = strconv.FormatInt(int64(info.Level), 10)
}
s.dao.PubView(context.TODO(), mid, id, ip, cheat)
})
group.Wait()
return
}
func (s *Service) isAttention(c context.Context, mid, up int64) (ok bool, err error) {
arg := account.ArgRelation{Mid: mid, Owner: up}
relation, err := s.accountRPC.Relation3(c, &arg)
if err != nil {
dao.PromError("view:获取关注列表")
log.Error("s.accountRPC.Relation2(%+v) err: %+v", arg, err)
return
}
ok = relation.Following
return
}
func (s *Service) isAttentions(c context.Context, mid int64, ups []int64) (res map[int64]bool, err error) {
arg := account.ArgRelations{Mid: mid, Owners: ups}
relations, err := s.accountRPC.Relations3(c, &arg)
if err != nil {
dao.PromError("view:批量获取关注列表")
log.Error("s.accountRPC.Relations3(%+v) err: %+v", arg, err)
return
}
res = make(map[int64]bool)
for id, r := range relations {
res[id] = r.Following
}
return
}
func (s *Service) isBlacks(c context.Context, mid int64, ups []int64) (res map[int64]struct{}, err error) {
arg := account.ArgMid{Mid: mid}
res, err = s.accountRPC.Blacks3(c, &arg)
if err != nil {
dao.PromError("view:获取黑名单列表")
log.Error("s.accountRPC.Blacks3(%+v) err: %+v", arg, err)
return
}
return
}
func (s *Service) checkArticle(c context.Context, id int64) (err error) {
var art *model.Meta
if art, err = s.ArticleMeta(c, id); (err != nil) || (art == nil) {
err = ecode.NothingFound
return
}
return
}

View File

@@ -0,0 +1 @@
package service

View File

@@ -0,0 +1,29 @@
package service
import (
"regexp"
"github.com/microcosm-cc/bluemonday"
)
// uat-i0 i0 ...
var bfsRegexp = regexp.MustCompile(`//.{1,6}\.hdslb+\.com/.+(?:jpg|gif|png|webp|jpeg)$`)
func xssFilter(content string) string {
p := bluemonday.NewPolicy()
p.AllowElements("b", "br", "del")
p.AllowAttrs("target", "href").OnElements("a")
p.AllowAttrs("class").OnElements("caption", "dl", "dd", "dt", "h2", "h3", "h4", "h5", "h6", "li", "ol", "strong", "ul")
p.AllowAttrs("class", "style").OnElements("h1", "p", "span")
p.AllowAttrs("class", "cite").OnElements("blockquote")
p.AllowAttrs("class", "contenteditable").OnElements("figure", "figcaption", "code")
p.AllowAttrs("class", "contenteditable", "aid", "style").OnElements("div")
p.AllowAttrs("color", "size", "face").OnElements("font")
p.AllowAttrs("class", "contenteditable", "data-lang").OnElements("pre")
p.AllowAttrs("src", "alt", "title", "width", "aid", "class", "height", "id", "_src", "type", "data-size", "data-vote-id").OnElements("img")
p.RequireParseableURLs(true)
p.AllowRelativeURLs(true) // support //i0.hdslb.com
p.AllowURLSchemes("http", "https", "bilibili")
p.AllowAttrs("src").Matching(bfsRegexp).OnElements("img")
return p.Sanitize(content)
}

File diff suppressed because one or more lines are too long