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,108 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"cursor_test.go",
"list_test.go",
"reply_test.go",
"service_test.go",
"subject_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/reply/conf:go_default_library",
"//app/interface/main/reply/model/reply:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"business.go",
"chan.go",
"cursor.go",
"list.go",
"notice.go",
"reply.go",
"reply_admin.go",
"reply_record.go",
"reply_report.go",
"rpc.go",
"service.go",
"subject.go",
"xreply.go",
],
importpath = "go-common/app/interface/main/reply/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/reply/conf:go_default_library",
"//app/interface/main/reply/dao/bigdata:go_default_library",
"//app/interface/main/reply/dao/drawyoo:go_default_library",
"//app/interface/main/reply/dao/fans:go_default_library",
"//app/interface/main/reply/dao/reply:go_default_library",
"//app/interface/main/reply/dao/search:go_default_library",
"//app/interface/main/reply/dao/vip:go_default_library",
"//app/interface/main/reply/dao/workflow:go_default_library",
"//app/interface/main/reply/model/adminlog:go_default_library",
"//app/interface/main/reply/model/drawyoo:go_default_library",
"//app/interface/main/reply/model/reply:go_default_library",
"//app/interface/main/reply/model/xreply:go_default_library",
"//app/interface/openplatform/article/model:go_default_library",
"//app/interface/openplatform/article/rpc/client:go_default_library",
"//app/service/main/account/api: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/assist/model/assist:go_default_library",
"//app/service/main/assist/rpc/client:go_default_library",
"//app/service/main/figure/model:go_default_library",
"//app/service/main/figure/rpc/client:go_default_library",
"//app/service/main/filter/api/grpc/v1:go_default_library",
"//app/service/main/location/model:go_default_library",
"//app/service/main/location/rpc/client:go_default_library",
"//app/service/main/relation/model:go_default_library",
"//app/service/main/reply-feed/api:go_default_library",
"//app/service/main/seq-server/model:go_default_library",
"//app/service/main/seq-server/rpc/client:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/rpc/client:go_default_library",
"//app/service/main/ugcpay/api/grpc/v1:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/log/infoc:go_default_library",
"//library/net/ip:go_default_library",
"//library/net/metadata:go_default_library",
"//library/queue/databus/report:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/sync/errgroup.v2:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//library/time:go_default_library",
"//library/xstr:go_default_library",
"//vendor/github.com/mvdan/xurls: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,25 @@
package service
import (
"context"
"go-common/app/interface/main/reply/model/reply"
)
// ListBusiness return all non-deleted business record.
func (s *Service) ListBusiness(c context.Context) (business []*reply.Business, err error) {
return s.dao.Business.ListBusiness(c)
}
// loadBusiness load business
func (s *Service) loadBusiness() (err error) {
var business []*reply.Business
if business, err = s.ListBusiness(context.Background()); err != nil {
return
}
for _, b := range business {
s.typeMapping[b.Type] = b.Alias
s.aliasMapping[b.Alias] = b.Type
}
return
}

View File

@@ -0,0 +1,38 @@
package service
import (
"context"
"go-common/app/interface/main/reply/model/reply"
"go-common/library/log"
)
const (
_replyChanBuf = 10240
_topRpChanBuf = 128
)
type replyChan struct {
rps []*reply.Reply
}
type topRpChan struct {
oid int64
tp int8
rp *reply.Reply
}
func (s *Service) cacheproc() {
for {
select {
case msg := <-s.replyChan:
if err := s.dao.Mc.AddReply(context.Background(), msg.rps...); err != nil {
log.Error("s.mcache.AddReply error(%v)", err)
}
case msg := <-s.topRpChan:
if err := s.dao.Mc.AddTop(context.Background(), msg.oid, msg.tp, msg.rp); err != nil {
log.Error("s.mcache.AddTop error(%v)", err)
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,164 @@
package service
import (
"context"
"reflect"
"testing"
model "go-common/app/interface/main/reply/model/reply"
)
func TestRemove(t *testing.T) {
cases := []struct {
inputIds []int64
id int64
expected []int64
}{
{
inputIds: []int64{5, 3, 2, 1},
id: 6,
expected: []int64{5, 3, 2, 1},
},
{
inputIds: []int64{6, 5, 3, 2, 1},
id: 6,
expected: []int64{5, 3, 2, 1},
},
{
inputIds: []int64{5, 3, 1, 6, 1, 2},
id: 1,
expected: []int64{5, 3, 6, 2},
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
got := Remove(c.inputIds, c.id)
if !reflect.DeepEqual(c.expected, got) {
t.Errorf("err sort, want %v, got %v", c.expected, got)
}
})
}
}
func TestUnique(t *testing.T) {
cases := []struct {
inputIds []int64
expected []int64
}{
{
inputIds: []int64{1, 2, 1, 2, 3, 43},
expected: []int64{1, 2, 3, 43},
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
got := model.SortArr(Unique(c.inputIds), model.OrderASC)
if !reflect.DeepEqual(c.expected, got) {
t.Errorf("err sort, want %v, got %v", c.expected, got)
}
})
}
}
func TestNewCursorByReplyID(t *testing.T) {
s := &Service{}
s.NewCursorByReplyID(context.Background(), 1123, int8(1), 112, 20, model.OrderDESC)
}
func TestGetRootReplyListByCursor(t *testing.T) {
s := &Service{}
s.GetRootReplyListByCursor(context.Background(), &model.CursorParams{})
}
func TestInsertInto(t *testing.T) {
cases := []struct {
inputIds []int64
id int64
size int
comp model.Comp
expected []int64
}{
{
inputIds: []int64{},
id: 100,
size: 5,
expected: []int64{100},
comp: model.OrderDESC,
},
{
inputIds: []int64{1, 2, 3, 5},
size: 5,
id: 4,
expected: []int64{1, 2, 3, 4, 5},
comp: model.OrderASC,
},
{
inputIds: []int64{2, 5, 1, 3},
size: 5,
id: 4,
expected: []int64{5, 4, 3, 2, 1},
comp: model.OrderDESC,
},
{
inputIds: []int64{1, 2, 3, 5},
size: 5,
id: 4,
expected: []int64{5, 4, 3, 2, 1},
comp: model.OrderDESC,
},
{
inputIds: []int64{1, 2, 3, 4, 5},
size: 5,
id: 4,
expected: []int64{1, 2, 3, 4, 5},
comp: model.OrderDESC,
},
{
inputIds: []int64{1, 2, 3, 4, 5},
size: 5,
id: 4,
expected: []int64{1, 2, 3, 4, 5},
comp: model.OrderASC,
},
{
inputIds: []int64{5, 4, 3, 1},
id: 2,
size: 5,
expected: []int64{5, 4, 3, 2, 1},
comp: model.OrderDESC,
},
{
inputIds: []int64{5},
id: 4,
size: 2,
expected: []int64{5, 4},
comp: model.OrderDESC,
},
{
inputIds: []int64{5},
id: 4,
size: 2,
expected: []int64{4, 5},
comp: model.OrderASC,
},
{
inputIds: []int64{5},
id: 4,
size: 1,
expected: []int64{5},
comp: model.OrderDESC,
},
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
got := InsertInto(c.inputIds, c.id, c.size, c.comp)
if !reflect.DeepEqual(c.expected, got) {
t.Errorf("err sorted insert, want %v, got %v", c.expected, got)
}
})
}
}

View File

@@ -0,0 +1,909 @@
package service
import (
"context"
"encoding/json"
"sort"
"strconv"
model "go-common/app/interface/main/reply/model/reply"
accmdl "go-common/app/service/main/account/api"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/net/metadata"
"go-common/library/sync/errgroup.v2"
)
const (
// hot count web
_webHotCnt = 3
// 热评列表中至少点赞数
_hotLikes = 3
// 取5条因为有可能管理员置顶和up置顶同时存在
_hotFilters = 5
)
func withinFloor(rpIDs []int64, rpID int64, pn, ps int, asc bool) bool {
if len(rpIDs) == 0 || (pn == 1 && len(rpIDs) < ps) {
return true
}
first := rpIDs[0]
last := rpIDs[len(rpIDs)-1]
if asc {
if (first < rpID && last > rpID) || (len(rpIDs) < ps && last < rpID) {
return true
}
} else {
if (pn == 1 && first < rpID) || (first > rpID && last < rpID) || (len(rpIDs) < ps && last > rpID) {
return true
}
}
return false
}
// SecondReplies return second replies.
func (s *Service) SecondReplies(c context.Context, mid, oid, rootID, jumpID int64, tp int8, pn, ps int, escape bool) (seconds []*model.Reply, root *model.Reply, upMid int64, toPn int, err error) {
var (
ok bool
sub *model.Subject
jump *model.Reply
)
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if sub, err = s.Subject(c, oid, tp); err != nil {
return
}
// jump to the child reply page list
if jumpID > 0 {
if jump, err = s.ReplyContent(c, oid, jumpID, tp); err != nil {
return
}
if jump.Root == 0 {
root = jump
pn = 1
} else {
if root, err = s.ReplyContent(c, oid, jump.Root, tp); err != nil {
return
}
if pos := s.getReplyPosByRoot(c, root, jump); pos > ps {
pn = (pos-1)/ps + 1
} else {
pn = 1
}
}
} else {
if root, err = s.ReplyContent(c, oid, rootID, tp); err != nil {
return
}
if root.Root != 0 {
if root, err = s.ReplyContent(c, oid, root.Root, tp); err != nil {
return
}
}
}
if root.IsDeleted() {
err = ecode.ReplyDeleted
return
}
upMid = sub.Mid
toPn = pn
// get reply second reply content
rootMap := make(map[int64]*model.Reply, 1)
rootMap[root.RpID] = root
secondMap, _, _ := s.secondReplies(c, sub, rootMap, mid, pn, ps)
if seconds, ok = secondMap[root.RpID]; !ok {
seconds = _emptyReplies
}
// get reply dependency info
rs := make([]*model.Reply, 0, len(seconds)+1)
rs = append(rs, root)
rs = append(rs, seconds...)
if err = s.buildReply(c, sub, rs, mid, escape); err != nil {
return
}
return
}
// JumpReplies jump to page by reply id.
func (s *Service) JumpReplies(c context.Context, mid, oid, rpID int64, tp int8, ps, sndPs int, escape bool) (roots, hots []*model.Reply, topAdmin, topUpper *model.Reply, sub *model.Subject, pn, sndPn, total int, err error) {
var (
rootPos, sndPos int
rootRp, rp *model.Reply
fixedSeconds []*model.Reply
)
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if sub, err = s.Subject(c, oid, tp); err != nil {
return
}
if rp, err = s.ReplyContent(c, oid, rpID, tp); err != nil {
return
}
if rp.Root == 0 && rp.Parent == 0 {
rootPos = s.getReplyPos(c, sub, rp)
} else {
if rootRp, err = s.ReplyContent(c, oid, rp.Root, tp); err != nil {
return
}
rootPos = s.getReplyPos(c, sub, rootRp)
sndPos = s.getReplyPosByRoot(c, rootRp, rp)
}
// root page number
pn = (rootPos-1)/ps + 1
// second page number
if sndPos > sndPs {
sndPn = (sndPos-1)/sndPs + 1
} else {
sndPn = 1
}
// get reply content
roots, seconds, total, err := s.rootReplies(c, sub, mid, model.SortByFloor, pn, ps, 1, sndPs)
if err != nil {
return
}
if rootRp != nil && rootRp.RCount > 0 {
if fixedSeconds, err = s.repliesByRoot(c, oid, rootRp.RpID, tp, sndPn, sndPs); err != nil {
return
}
for _, rp := range roots {
if rp.RpID == rootRp.RpID {
rp.Replies = fixedSeconds
}
}
}
// top and hots
topAdmin, topUpper, hots, hseconds, err := s.topAndHots(c, sub, mid, true, true)
if err != nil {
log.Error("s.topAndHots(%d,%d,%d) error(%v)", oid, tp, mid, err)
err = nil // degrade
}
rs := make([]*model.Reply, 0, len(roots)+len(seconds)+len(hseconds)+len(fixedSeconds)+2)
rs = append(rs, roots...)
rs = append(rs, seconds...)
rs = append(rs, hseconds...)
rs = append(rs, hots...)
rs = append(rs, fixedSeconds...)
if topAdmin != nil {
rs = append(rs, topAdmin)
}
if topUpper != nil {
rs = append(rs, topUpper)
}
if err = s.buildReply(c, sub, rs, mid, escape); err != nil {
return
}
return
}
// RootReplies return a page list of reply.
func (s *Service) RootReplies(c context.Context, params *model.PageParams) (page *model.PageResult, err error) {
if !model.LegalSubjectType(params.Type) {
err = ecode.ReplyIllegalSubType
return
}
sub, err := s.Subject(c, params.Oid, params.Type)
if err != nil {
return
}
topAdmin, topUpper, hots, hseconds, err := s.topAndHots(c, sub, params.Mid, params.NeedHot, params.NeedSecond)
if err != nil {
log.Error("s.topAndHots(%+v) error(%v)", params, err)
err = nil // degrade
}
roots, seconds, total, err := s.rootReplies(c, sub, params.Mid, params.Sort, params.PageNum, params.PageSize, 1, s.sndDefCnt)
if err != nil {
return
}
rs := make([]*model.Reply, 0, len(roots)+len(hots)+len(hseconds)+len(seconds)+2)
rs = append(rs, hots...)
rs = append(rs, roots...)
rs = append(rs, hseconds...)
rs = append(rs, seconds...)
if topAdmin != nil {
rs = append(rs, topAdmin)
}
if topUpper != nil {
rs = append(rs, topUpper)
}
if err = s.buildReply(c, sub, rs, params.Mid, params.Escape); err != nil {
return
}
page = &model.PageResult{
Subject: sub,
TopAdmin: topAdmin,
TopUpper: topUpper,
Hots: hots,
Roots: roots,
Total: total,
AllCount: sub.ACount,
}
return
}
func (s *Service) topAndHots(c context.Context, sub *model.Subject, mid int64, needNot, needSnd bool) (topAdmin, topUpper *model.Reply, hots, seconds []*model.Reply, err error) {
var (
ok bool
hotIDs []int64
rootMap map[int64]*model.Reply
secondMap map[int64][]*model.Reply
)
// get hot replies
if needNot {
if hotIDs, _, err = s.rootReplyIDs(c, sub, model.SortByLike, 1, s.hotNumWeb(sub.Oid, sub.Type)+2, false); err != nil {
return
}
if rootMap, err = s.repliesMap(c, sub.Oid, sub.Type, hotIDs); err != nil {
return
}
}
// get top replies
if topAdmin, err = s.topReply(c, sub, model.SubAttrAdminTop); err != nil {
return
}
if topUpper, err = s.topReply(c, sub, model.SubAttrUpperTop); err != nil {
return
}
if rootMap == nil {
rootMap = make(map[int64]*model.Reply)
}
if topAdmin != nil {
rootMap[topAdmin.RpID] = topAdmin
}
if topUpper != nil {
if !topUpper.IsNormal() && sub.Mid != mid {
topUpper = nil
} else {
rootMap[topUpper.RpID] = topUpper
}
}
// get second replies
if needSnd {
if secondMap, seconds, err = s.secondReplies(c, sub, rootMap, mid, 1, s.sndDefCnt); err != nil {
return
}
if topAdmin != nil {
if topAdmin.Replies, ok = secondMap[topAdmin.RpID]; !ok {
topAdmin.Replies = _emptyReplies
}
}
if topUpper != nil {
if topUpper.Replies, ok = secondMap[topUpper.RpID]; !ok {
topUpper.Replies = _emptyReplies
}
}
}
if len(hotIDs) == 0 {
hots = _emptyReplies
return
}
hotSize := s.hotNumWeb(sub.Oid, sub.Type)
for _, rootID := range hotIDs {
if hotSize != _hotSizeWeb && len(hots) >= _hotSizeWeb {
break
} else if len(hots) >= hotSize {
break
}
if rp, ok := rootMap[rootID]; ok && rp.Like >= _hotLikes && !rp.IsTop() {
if rp.Replies, ok = secondMap[rp.RpID]; !ok {
rp.Replies = _emptyReplies
}
hots = append(hots, rp)
}
}
return
}
func (s *Service) rootReplies(c context.Context, sub *model.Subject, mid int64, msort int8, pn, ps, secondPn, secondPs int) (roots, seconds []*model.Reply, total int, err error) {
var (
rootMap map[int64]*model.Reply
)
// get root replies
rootIDs, total, err := s.rootReplyIDs(c, sub, msort, pn, ps, true)
if err != nil {
return
}
if len(rootIDs) > 0 {
if rootMap, err = s.repliesMap(c, sub.Oid, sub.Type, rootIDs); err != nil {
return
}
}
// get pending audit replies
if msort == model.SortByFloor && sub.AttrVal(model.SubAttrAudit) == model.AttrYes {
var (
pendingTotal int
pendingIDs []int64
rootPendingMap map[int64]*model.Reply
)
if rootPendingMap, _, pendingTotal, err = s.userAuditReplies(c, mid, sub.Oid, sub.Type); err != nil {
err = nil // degrade
}
if rootMap == nil {
rootMap = make(map[int64]*model.Reply)
}
for _, rp := range rootPendingMap {
if withinFloor(rootIDs, rp.RpID, pn, ps, false) {
rootMap[rp.RpID] = rp
pendingIDs = append(pendingIDs, rp.RpID)
}
}
if len(pendingIDs) > 0 {
rootIDs = append(rootIDs, pendingIDs...)
sort.Sort(model.DescFloors(rootIDs))
}
sub.ACount += pendingTotal
}
if len(rootIDs) == 0 {
roots = _emptyReplies
return
}
// get second replies
secondMap, seconds, err := s.secondReplies(c, sub, rootMap, mid, secondPn, secondPs)
if err != nil {
return
}
for _, rootID := range rootIDs {
if rp, ok := rootMap[rootID]; ok {
if rp.Replies, ok = secondMap[rp.RpID]; !ok {
rp.Replies = _emptyReplies
}
if msort != model.SortByFloor {
//if not sort by floor,can't contain the top comment
if rp.IsTop() {
continue
}
}
roots = append(roots, rp)
}
}
if roots == nil {
roots = _emptyReplies
}
return
}
func (s *Service) secondReplies(c context.Context, sub *model.Subject, rootMap map[int64]*model.Reply, mid int64, pn, ps int) (res map[int64][]*model.Reply, rs []*model.Reply, err error) {
var (
rootIDs, secondIDs []int64
secondIdxMap map[int64][]int64
secondMap map[int64]*model.Reply
)
for rootID, info := range rootMap {
if info.RCount > 0 {
rootIDs = append(rootIDs, rootID)
}
}
if len(rootIDs) > 0 {
if secondIdxMap, secondIDs, err = s.getIdsByRoots(c, sub.Oid, rootIDs, sub.Type, pn, ps); err != nil {
return
}
if secondMap, err = s.repliesMap(c, sub.Oid, sub.Type, secondIDs); err != nil {
return
}
}
// get pending audit replies
if sub.AttrVal(model.SubAttrAudit) == model.AttrYes {
var secondPendings map[int64][]*model.Reply
if _, secondPendings, _, err = s.userAuditReplies(c, mid, sub.Oid, sub.Type); err != nil {
err = nil // degrade
}
if secondIdxMap == nil {
secondIdxMap = make(map[int64][]int64)
}
if secondMap == nil {
secondMap = make(map[int64]*model.Reply)
}
for rootID, rs := range secondPendings {
var pendingIDs []int64
if r, ok := rootMap[rootID]; ok {
for _, r := range rs {
if withinFloor(secondIdxMap[rootID], r.RpID, pn, ps, true) {
secondMap[r.RpID] = r
pendingIDs = append(pendingIDs, r.RpID)
}
}
r.RCount += len(rs)
}
if len(pendingIDs) > 0 {
secondIdxMap[rootID] = append(secondIdxMap[rootID], pendingIDs...)
sort.Sort(model.AscFloors(secondIdxMap[rootID]))
}
}
}
res = make(map[int64][]*model.Reply, len(secondIdxMap))
for root, idxs := range secondIdxMap {
seconds := make([]*model.Reply, 0, len(idxs))
for _, rpid := range idxs {
if r, ok := secondMap[rpid]; ok {
seconds = append(seconds, r)
}
}
res[root] = seconds
rs = append(rs, seconds...)
}
return
}
// FilDelReply delete reply which is deleted
func (s *Service) FilDelReply(rps []*model.Reply) (filtedRps []*model.Reply) {
for _, rp := range rps {
if !rp.IsDeleted() {
var childs []*model.Reply
for _, child := range rp.Replies {
if !child.IsDeleted() {
childs = append(childs, child)
}
}
rp.Replies = childs
filtedRps = append(filtedRps, rp)
}
}
return
}
// EmojiReplaceI EmojiReplace international
func (s *Service) EmojiReplaceI(mobiAPP string, build int64, roots ...*model.Reply) {
if mobiAPP == "android_i" && build > 1125000 && build < 2005000 {
for _, root := range roots {
if root != nil {
if root.Content != nil {
if emoCodes := _emojiCode.FindAllString(root.Content.Message, -1); len(emoCodes) > 0 {
root.Content.Message = RepressEmotions(root.Content.Message, emoCodes)
}
}
for _, rp := range root.Replies {
if rp.Content != nil {
if emoCodes := _emojiCode.FindAllString(rp.Content.Message, -1); len(emoCodes) > 0 {
rp.Content.Message = RepressEmotions(rp.Content.Message, emoCodes)
}
}
}
}
}
}
}
// EmojiReplace EmojiReplace
func (s *Service) EmojiReplace(plat int8, build int64, roots ...*model.Reply) {
if (plat == model.PlatIPad || plat == model.PlatPadHd || plat == model.PlatIPhone) && build <= 8170 {
for _, root := range roots {
if root != nil {
for _, rp := range root.Replies {
if rp.Content != nil {
if emoCodes := _emojiCode.FindAllString(rp.Content.Message, -1); len(emoCodes) > 0 {
rp.Content.Message = RepressEmotions(rp.Content.Message, emoCodes)
}
}
}
}
}
}
}
func (s *Service) buildReply(c context.Context, sub *model.Subject, replies []*model.Reply, mid int64, escape bool) (err error) {
var (
ok bool
assistMap map[int64]int
actionMap map[int64]int8
blackedMap map[int64]bool
attetionMap map[int64]*accmdl.RelationReply
rpIDs = make([]int64, 0, len(replies))
mids = make([]int64, 0, len(replies))
uniqMids = make(map[int64]struct{}, len(replies))
fansMap map[int64]*model.FansDetail
accMap map[int64]*accmdl.Card
)
if len(replies) == 0 {
return
}
for _, rp := range replies {
if rp.Content != nil {
for _, mid := range rp.Content.Ats {
uniqMids[mid] = struct{}{}
}
}
uniqMids[rp.Mid] = struct{}{}
rpIDs = append(rpIDs, rp.RpID)
}
for mid := range uniqMids {
mids = append(mids, mid)
}
g := errgroup.WithContext(c)
if mid > 0 {
g.Go(func(c context.Context) error {
actionMap, _ = s.actions(c, mid, sub.Oid, rpIDs)
return nil
})
g.Go(func(c context.Context) error {
attetionMap, _ = s.getAttentions(c, mid, mids)
return nil
})
g.Go(func(c context.Context) error {
blackedMap, _ = s.GetBlacklist(c, mid)
return nil
})
}
g.Go(func(c context.Context) error {
accMap, _ = s.getAccInfo(c, mids)
return nil
})
if !s.IsWhiteAid(sub.Oid, sub.Type) {
g.Go(func(c context.Context) error {
fansMap, _ = s.FetchFans(c, mids, sub.Mid)
return nil
})
g.Go(func(c context.Context) error {
assistMap = s.getAssistList(c, sub.Mid)
return nil
})
}
g.Wait()
// set reply info
for _, r := range replies {
r.FillFolder()
r.FillStr(escape)
if r.Content != nil {
r.Content.FillAts(accMap)
}
r.Action = actionMap[r.RpID]
// member info and degrade
r.Member = new(model.Member)
var card *accmdl.Card
if card, ok = accMap[r.Mid]; ok {
r.Member.Info = new(model.Info)
r.Member.Info.FromCard(card)
} else {
r.Member.Info = new(model.Info)
*r.Member.Info = *s.defMember
r.Member.Info.Mid = strconv.FormatInt(r.Mid, 10)
}
if r.Member.FansDetail, ok = fansMap[r.Mid]; ok {
r.FansGrade = r.Member.FansDetail.Status
}
if _, ok = blackedMap[r.Mid]; ok {
r.State = model.ReplyStateBlacklist
}
if _, ok = assistMap[r.Mid]; ok {
r.Assist = 1
}
if attetion, ok := attetionMap[r.Mid]; ok {
if attetion.Following {
r.Member.Following = 1
}
}
// temporary fix situation: rcount < 0
if r.RCount < 0 {
r.RCount = 0
}
}
return
}
func (s *Service) rootReplyIDs(c context.Context, sub *model.Subject, sort int8, pn, ps int, loadCount bool) (rpIDs []int64, count int, err error) {
var (
ok bool
start = (pn - 1) * ps
end = start + ps - 1
)
if count = sub.RCount; start >= count {
return
}
if sort == model.SortByLike {
mid := metadata.Int64(c, metadata.Mid)
res, err := s.replyHotFeed(c, mid, sub.Oid, int(sub.Type), pn, ps)
if err == nil && res.RpIDs != nil && len(res.RpIDs) > 0 {
log.Info("reply-feed(test): reply abtest mid(%d) oid(%d) type (%d) test name(%s) rpIDs(%v)", mid, sub.Oid, sub.Type, res.Name, res.RpIDs)
rpIDs = res.RpIDs
if loadCount {
count = int(res.Count)
}
return rpIDs, count, nil
}
if err != nil {
log.Error("reply-feed error(%v)", err)
err = nil
} else {
log.Info("reply-feed(origin): reply abtest mid(%d) oid(%d) type (%d) test name(%s) rpIDs(%v)", mid, sub.Oid, sub.Type, res.Name, res.RpIDs)
}
}
// expire the index cache
if ok, err = s.dao.Redis.ExpireIndex(c, sub.Oid, sub.Type, sort); err != nil {
log.Error("s.dao.Redis.ExpireIndex(%d,%d,%d) error(%v)", sub.Oid, sub.Type, sort, err)
return
}
if !ok {
// here we can get that Serviceub.RCount > 0
switch sort {
case model.SortByFloor:
s.dao.Databus.RecoverFloorIdx(c, sub.Oid, sub.Type, end+1, false)
rpIDs, err = s.dao.Reply.GetIdsSortFloor(c, sub.Oid, sub.Type, start, ps)
case model.SortByCount:
s.dao.Databus.RecoverIndex(c, sub.Oid, sub.Type, sort)
rpIDs, err = s.dao.Reply.GetIdsSortCount(c, sub.Oid, sub.Type, start, ps)
case model.SortByLike:
s.dao.Databus.RecoverIndex(c, sub.Oid, sub.Type, sort)
if rpIDs, err = s.dao.Reply.GetIdsSortLike(c, sub.Oid, sub.Type, start, ps); err != nil {
return
}
if loadCount {
count, err = s.dao.Reply.CountLike(c, sub.Oid, sub.Type)
}
}
if err != nil {
log.Error("s.rootIDs(%d,%d,%d,%d,%d) error(%v)", sub.Oid, sub.Type, sort, start, ps, err)
return
}
} else {
var isEnd bool
if rpIDs, isEnd, err = s.dao.Redis.Range(c, sub.Oid, sub.Type, sort, start, end); err != nil {
log.Error("s.dao.Redis.Range(%d,%d,%d,%d,%d) error(%v)", sub.Oid, sub.Type, sort, start, end, err)
return
}
if (sort == model.SortByLike || sort == model.SortByCount) && loadCount {
if count, err = s.dao.Redis.CountReplies(c, sub.Oid, sub.Type, sort); err != nil {
log.Error("s.dao.Redis.CountLike(%d,%d,%d) error(%v)", sub.Oid, sub.Type, sort, err)
}
}
if sort == model.SortByFloor && len(rpIDs) < ps && !isEnd {
//The addition and deletion of comments may result in the display of duplicate entries
rpIDs, err = s.dao.Reply.GetIdsSortFloor(c, sub.Oid, sub.Type, start, ps)
if err != nil {
log.Error("s.rootIDs(%d,%d,%d,%d,%d) error(%v)", sub.Oid, sub.Type, sort, start, ps, err)
return
}
s.dao.Databus.RecoverFloorIdx(c, sub.Oid, sub.Type, end+1, false)
}
}
return
}
// topReply return top replies from cache.
func (s *Service) topReply(c context.Context, sub *model.Subject, top uint32) (rp *model.Reply, err error) {
if top != model.ReplyAttrUpperTop && top != model.ReplyAttrAdminTop {
return
}
if sub.AttrVal(top) == model.AttrYes && sub.Meta != "" {
var meta model.SubjectMeta
err = json.Unmarshal([]byte(sub.Meta), &meta)
if err != nil {
log.Error("s.topReply(%d,%d,%d) unmarshal error(%v)", sub.Oid, sub.Type, top, err)
return
}
var rpid int64
if top == model.SubAttrAdminTop && meta.AdminTop != 0 {
rpid = meta.AdminTop
} else if top == model.SubAttrUpperTop && meta.UpperTop != 0 {
rpid = meta.UpperTop
}
if rpid != 0 {
rp, err = s.ReplyContent(c, sub.Oid, rpid, sub.Type)
if err != nil {
log.Error("s.GetReply(%d,%d,%d) error(%v)", sub.Oid, sub.Type, rpid, err)
return
}
if rp == nil {
log.Error("s.GetReply(%d,%d,%d) is nil", sub.Oid, sub.Type, rpid)
}
return
}
}
if sub.AttrVal(top) == model.AttrYes {
if rp, err = s.dao.Mc.GetTop(c, sub.Oid, sub.Type, top); err != nil {
log.Error("s.dao.Mc.GetTop(%d,%d,%d) error(%v)", sub.Oid, sub.Type, top, err)
return
}
if rp == nil {
s.dao.Databus.AddTop(c, sub.Oid, sub.Type, top)
}
}
return
}
func (s *Service) userAuditReplies(c context.Context, mid, oid int64, tp int8) (rootMap map[int64]*model.Reply, secondMap map[int64][]*model.Reply, total int, err error) {
rpIDs, err := s.dao.Redis.UserAuditReplies(c, mid, oid, tp)
if err != nil {
log.Error("s.dao.Redis.Range(%d,%d,%d) error(%v)", oid, tp, mid, err)
return
}
rpMap, err := s.repliesMap(c, oid, tp, rpIDs)
if err != nil {
return
}
total = len(rpMap)
rootMap = make(map[int64]*model.Reply)
secondMap = make(map[int64][]*model.Reply)
for _, rp := range rpMap {
if rp.Root == 0 {
if !rp.IsTop() {
rootMap[rp.RpID] = rp
}
} else {
secondMap[rp.Root] = append(secondMap[rp.Root], rp)
}
}
return
}
// repliesMap multi get reply from cache or db when missed and fill content.
func (s *Service) repliesMap(c context.Context, oid int64, tp int8, rpIDs []int64) (res map[int64]*model.Reply, err error) {
if len(rpIDs) == 0 {
return
}
res, missIDs, err := s.dao.Mc.GetMultiReply(c, rpIDs)
if err != nil {
log.Error("s.dao.Mc.GetMultiReply(%d,%d,%d) error(%v)", oid, tp, rpIDs, err)
err = nil
res = make(map[int64]*model.Reply, len(rpIDs))
missIDs = rpIDs
}
if len(missIDs) > 0 {
var (
mrp map[int64]*model.Reply
mrc map[int64]*model.Content
)
if mrp, err = s.dao.Reply.GetByIds(c, oid, tp, missIDs); err != nil {
log.Error("s.reply.GetByIds(%d,%d,%d) error(%v)", oid, tp, rpIDs, err)
return
}
if mrc, err = s.dao.Content.GetByIds(c, oid, missIDs); err != nil {
log.Error("s.content.GetByIds(%d,%d) error(%v)", oid, rpIDs, err)
return
}
rs := make([]*model.Reply, 0, len(missIDs))
for _, rpID := range missIDs {
if rp, ok := mrp[rpID]; ok {
rp.Content = mrc[rpID]
res[rpID] = rp
rs = append(rs, rp.Clone())
}
}
// asynchronized add reply cache
select {
case s.replyChan <- replyChan{rps: rs}:
default:
log.Warn("s.replyChan is full")
}
}
return
}
// ReplyContent get reply and content.
func (s *Service) ReplyContent(c context.Context, oid, rpID int64, tp int8) (r *model.Reply, err error) {
if r, err = s.dao.Mc.GetReply(c, rpID); err != nil {
log.Error("replyCacheDao.GetReply(%d, %d, %d) error(%v)", oid, rpID, tp, err)
}
if r == nil {
if r, err = s.dao.Reply.Get(c, oid, rpID); err != nil {
log.Error("s.reply.GetReply(%d, %d) error(%v)", oid, rpID, err)
return nil, err
}
if r == nil {
return nil, ecode.ReplyNotExist
}
if r.Content, err = s.dao.Content.Get(c, oid, rpID); err != nil {
return nil, err
}
if err = s.dao.Mc.AddReply(c, r); err != nil {
log.Error("mc.AddReply(%d,%d,%d) error(%v)", oid, rpID, tp, err)
err = nil
}
}
if r.Oid != oid || r.Type != tp {
log.Warn("reply dismatches with parameter, oid: %d, rpID: %d, tp: %d, actual: %d, %d, %d", oid, rpID, tp, r.Oid, r.RpID, r.Type)
return nil, ecode.RequestErr
}
return r, nil
}
func (s *Service) repliesByRoot(c context.Context, oid, root int64, tp int8, pn, ps int) (res []*model.Reply, err error) {
var (
cache bool
rpIDs []int64
start = (pn - 1) * ps
end = start + ps - 1
)
if cache, err = s.dao.Redis.ExpireIndexByRoot(c, root); err != nil {
return
}
if cache {
if rpIDs, err = s.dao.Redis.RangeByRoot(c, root, start, end); err != nil {
log.Error("s.dao.Redis.RangeByRoots() err(%v)", err)
return
}
} else {
if rpIDs, err = s.dao.Reply.GetIdsByRoot(c, oid, root, tp, start, ps); err != nil {
log.Error("s.dao.Reply.GetIdsByRoot(oid %d,tp %d,root %d) err(%v)", oid, tp, root, err)
}
s.dao.Databus.RecoverIndexByRoot(c, oid, root, tp)
}
rs, err := s.repliesMap(c, oid, tp, rpIDs)
if err != nil {
return
}
for _, rpID := range rpIDs {
if rp, ok := rs[rpID]; ok {
res = append(res, rp)
}
}
return
}
// ReplyHots return the hot replies.
func (s *Service) ReplyHots(c context.Context, oid int64, typ int8, pn, ps int) (sub *model.Subject, res []*model.Reply, err error) {
if !model.LegalSubjectType(typ) {
err = ecode.ReplyIllegalSubType
return
}
if sub, err = s.Subject(c, oid, typ); err != nil {
log.Error("s.Subject(%d,%d) error(%v)", oid, typ, err)
return
}
hotIDs, _, err := s.rootReplyIDs(c, sub, model.SortByLike, pn, ps, false)
if err != nil {
return
}
rootMap, err := s.repliesMap(c, sub.Oid, sub.Type, hotIDs)
if err != nil {
return
}
for _, rpID := range hotIDs {
if rp, ok := rootMap[rpID]; ok {
res = append(res, rp)
}
}
if len(res) == 0 {
res = _emptyReplies
}
return
}
// Dialog ...
func (s *Service) Dialog(c context.Context, mid, oid int64, tp int8, root, dialog int64, pn, ps int, escape bool) (rps []*model.Reply, err error) {
var (
start = (pn - 1) * ps
end = start + ps - 1
ok bool
rpIDs []int64
)
if ok, err = s.dao.Redis.ExpireDialogIndex(c, dialog); err != nil {
log.Error("s.dao.Redis.ExpireDialogIndex error (%v)", err)
return
}
if ok {
rpIDs, err = s.dao.Redis.RangeRpsByDialog(c, dialog, start, end)
} else {
s.dao.Databus.RecoverDialogIdx(c, oid, tp, root, dialog)
rpIDs, err = s.dao.Reply.GetIDsByDialog(c, oid, tp, root, dialog, start, ps)
}
if err != nil {
log.Error("range replies by dialog from redis or db error (%v)", err)
return
}
rpMap, err := s.repliesMap(c, oid, tp, rpIDs)
if err != nil {
return
}
for _, rpID := range rpIDs {
if r, ok := rpMap[rpID]; ok {
rps = append(rps, r)
}
}
sub, err := s.Subject(c, oid, tp)
if err != nil {
return
}
for _, rp := range rps {
rp.DialogStr = strconv.FormatInt(rp.Dialog, 10)
}
if err = s.buildReply(c, sub, rps, mid, escape); err != nil {
return
}
return
}

View File

@@ -0,0 +1,51 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestWithinFloor(t *testing.T) {
var (
ids = []int64{3, 5, 7, 9, 11, 13, 15}
)
// within asc
Convey("withinFloor asc without page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 1, 1, 5, true)
So(r, ShouldBeFalse)
}))
Convey("withinFloor asc within page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 4, 1, 5, true)
So(r, ShouldBeTrue)
}))
Convey("withinFloor asc within page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 16, 1, 20, true)
So(r, ShouldBeTrue)
}))
Convey("withinFloor asc without page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 16, 1, 5, true)
So(r, ShouldBeFalse)
}))
// within desc
Convey("withinFloor desc within page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 16, 1, 5, false)
So(r, ShouldBeTrue)
}))
Convey("withinFloor desc within page 2", t, WithService(func(s *Service) {
r := withinFloor(ids, 16, 2, 5, false)
So(r, ShouldBeFalse)
}))
Convey("withinFloor desc within page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 2, 3, 20, false)
So(r, ShouldBeTrue)
}))
Convey("withinFloor desc without page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 1, 1, 5, false)
So(r, ShouldBeFalse)
}))
Convey("withinFloor desc within page 1", t, WithService(func(s *Service) {
r := withinFloor(ids, 1, 1, 20, true)
So(r, ShouldBeTrue)
}))
}

View File

@@ -0,0 +1,60 @@
package service
import (
"context"
"strconv"
"strings"
"time"
model "go-common/app/interface/main/reply/model/reply"
"go-common/library/log"
"go-common/library/net/metadata"
)
// RplyNotice return a reply notice from memory.
func (s *Service) RplyNotice(c context.Context, plat int8, build int64, app, buvid string) (n *model.Notice) {
n = new(model.Notice)
// NOTE skip crash build, wrong plat ipad and ipad HD
if plat == model.PlatIPad && (build >= 10400 && build <= 10420) {
return
}
if (app == "iphone" || app == "iphone_i") && plat == model.PlatIPad && (build >= 4270 && build <= 4350) {
return
}
if nts, ok := s.notice[plat]; ok {
for _, notice := range nts {
if notice.CheckBuild(plat, build, app) {
*n = *notice
break
}
}
}
// 如果是空 返回给客户端null
if n.ID == 0 {
n = nil
return
}
if plat != model.PlatWeb && strings.Contains(n.Link, "https://ad-bili-data.biligame.com/api/mobile") {
n.Link = strings.Replace(n.Link, "__MID__", strconv.FormatInt(metadata.Int64(c, metadata.Mid), 10), 1)
n.Link = strings.Replace(n.Link, "__IP__", metadata.String(c, metadata.RemoteIP), 1)
n.Link = strings.Replace(n.Link, "__BUVID__", buvid, 1)
n.Link = strings.Replace(n.Link, "__TS__", strconv.FormatInt(time.Now().Unix(), 10), 1)
}
return
}
// loadRplNotice load reply notice
func (s *Service) loadRplNotice() (err error) {
nts, err := s.dao.Notice.ReplyNotice(context.Background())
if err != nil {
log.Error("s.ReplynNotice err(%v)", err)
return
}
tmp := make(map[int8][]*model.Notice, len(nts))
for _, nt := range nts {
tmp[nt.Plat] = append(tmp[nt.Plat], nt)
}
//map的是引用类型底层实现是指针所以不必对s.notice加锁
s.notice = tmp
return
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,442 @@
package service
import (
"context"
"fmt"
"time"
"go-common/app/interface/main/reply/conf"
"go-common/app/interface/main/reply/model/reply"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/net/metadata"
"go-common/library/queue/databus/report"
xtime "go-common/library/time"
)
// AdminGetSubject get subject state
func (s *Service) AdminGetSubject(c context.Context, oid int64, tp int8) (sub *reply.Subject, err error) {
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if sub, err = s.dao.Subject.Get(c, oid, tp); err != nil {
log.Error("s.dao.Subject.Get(%d,%d) error(%v)", oid, tp, err)
return
}
if sub == nil {
err = ecode.NothingFound
}
return
}
// AdminSubRegist register subject of new archive
func (s *Service) AdminSubRegist(c context.Context, oid, mid int64, tp, state int8, appkey string) (err error) {
var has bool
tps, ok := conf.Conf.AppkeyType[appkey]
if !ok || len(tps) == 0 {
err = ecode.ReplyIllegalSubType
return
}
if tp != 0 {
for _, t := range tps {
if t == tp {
has = true
break
}
}
} else {
has = true
tp = tps[0]
}
if !has {
err = ecode.ReplyIllegalSubType
return
}
now := time.Now()
sub := &reply.Subject{
Oid: oid,
Type: tp,
Mid: mid,
State: state,
CTime: xtime.Time(now.Unix()),
MTime: xtime.Time(now.Unix()),
}
if sub.ID, err = s.dao.Subject.Insert(c, sub); err != nil {
log.Error("s.dao.Subject.Insert(%v) error(%v)", sub, err)
}
s.cache.Do(c, func(ctx context.Context) {
if err = s.dao.Mc.DeleteSubject(ctx, oid, tp); err != nil {
log.Error("s.dao.Mc.DeleteSubject(%d, %d) state:%d error(%v)", oid, tp, state, err)
}
})
return
}
// AdminSubjectState change subject state by admin.
func (s *Service) AdminSubjectState(c context.Context, adid, oid, mid int64, tp, state int8, remark string) (err error) {
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
now := time.Now()
if err = reply.CheckSubState(state); err != nil {
log.Error("checkstate err(%v)", err)
return
}
if mid <= 0 {
var sub *reply.Subject
sub, err = s.dao.Subject.Get(c, oid, tp)
if err != nil {
log.Error("s.dao.subject (%d,%d)error(%v)", oid, tp, err)
return
}
if sub == nil {
err = ecode.NothingFound
return
}
mid = sub.Mid
}
if _, err = s.setSubject(c, oid, tp, state, mid); err != nil {
log.Error("s.addSubject(%d, %d, %d, %d) error(%v)", oid, tp, state, mid, err)
return
}
s.cache.Do(c, func(ctx context.Context) {
if err = s.dao.Mc.DeleteSubject(ctx, oid, tp); err != nil {
log.Error("s.dao.Mc.DeleteSubject(%d, %d) state:%d error(%v)", oid, tp, state, err)
}
})
s.dao.Admin.Insert(c, adid, oid, 0, tp, fmt.Sprintf("修改主题状态为: %d", state), remark, reply.AdminIsNotNew, reply.AdminIsNotReport, reply.AdminOperSubState, now)
return
}
// AdminSubjectMid set the subject mid info.
func (s Service) AdminSubjectMid(c context.Context, adid, mid, oid int64, tp int8, remark string) (err error) {
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
now := time.Now()
if _, err = s.dao.Subject.UpMid(c, mid, oid, tp, now); err != nil {
log.Error("replySubDao.UpMid(%d, %d, %d) error(%v)", mid, oid, tp, err)
return
}
s.cache.Do(c, func(ctx context.Context) {
if err = s.dao.Mc.DeleteSubject(ctx, oid, tp); err != nil {
log.Error("s.dao.Mc.DeleteSubject(%d, %d) mid:%d error(%v)", oid, tp, mid, err)
}
})
s.dao.Admin.Insert(c, adid, oid, 0, tp, fmt.Sprintf("修改主题mid为: %d", mid), remark, reply.AdminIsNotNew, reply.AdminIsNotReport, reply.AdminOperSubMid, now)
return
}
// Delete delete reply by upper or self.
func (s *Service) Delete(c context.Context, mid, oid, rpID int64, tp int8, ak, ck, platform string, build int64, buvid string) (err error) {
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rp, _ := s.reply(c, mid, oid, rpID, tp)
if rp == nil {
return
}
subject, err := s.getSubject(c, oid, tp)
if err != nil {
return
}
var (
assist, operation bool
state = reply.ReplyStateUserDel
)
// check permissiononly upper and self can del
if !s.IsWhiteAid(subject.Oid, subject.Type) {
assist, operation = s.CheckAssist(c, subject.Mid, mid)
if !operation {
assist = false
}
if assist {
state = reply.ReplyStateAssistDel
}
}
if subject.Mid == mid {
state = reply.ReplyStateUpDel
}
if subject.Mid == mid || mid == rp.Mid || assist {
if rp.IsDeleted() {
s.dao.Redis.DelIndex(c, rp)
err = ecode.ReplyDeleted
return
} else if rp.AttrVal(reply.ReplyAttrAdminTop) == 1 {
err = ecode.ReplyDelTopForbidden
return
}
} else {
err = ecode.AccessDenied
return
}
s.dao.Databus.Delete(c, mid, oid, rpID, time.Now().Unix(), tp, assist)
remoteIP := metadata.String(c, metadata.RemoteIP)
report.User(&report.UserInfo{
Mid: rp.Mid,
Platform: platform,
Build: build,
Buvid: buvid,
Business: 41,
Type: int(rp.Type),
Oid: rp.Oid,
Action: reply.ReportReplyDel,
Ctime: time.Now(),
IP: remoteIP,
Index: []interface{}{
rp.RpID,
rp.State,
state,
},
})
return
}
// AdminEdit edit reply content by admin.
func (s *Service) AdminEdit(c context.Context, adid, oid, rpID int64, tp int8, msg, remark string) (err error) {
now := time.Now()
var rp *reply.Reply
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if rp, err = s.reply(c, 0, oid, rpID, tp); err != nil {
return
}
if rp.IsDeleted() {
err = ecode.ReplyDeleted
return
}
if _, err = s.dao.Content.UpMessage(c, oid, rpID, msg, now); err != nil {
log.Error("s.content.UpMessage(%d, %d, %s, %v), err is (%v)", oid, rpID, msg, now, err)
return
}
if err = s.dao.Mc.DeleteReply(c, rpID); err != nil {
log.Error("s.dao.Mc.DeleteReply(%d, %d, %s, %v), err is (%v)", oid, rpID, msg, now, err)
}
// admin log
if _, err = s.dao.Admin.UpIsNotNew(c, rpID, now); err != nil {
log.Error("s.admin.UpIsNotNew(%d, %d, %s, %v), err is (%v)", oid, rpID, msg, now, err)
}
if _, err = s.dao.Admin.Insert(c, adid, oid, rpID, tp, "已修改评论内容", remark, reply.AdminIsNew, reply.AdminIsNotReport, reply.AdminOperEdit, now); err != nil {
log.Error("s.admin.Insert(%d, %d, %s, %v), err is (%v)", oid, rpID, msg, now, err)
}
// dao.Kafka
s.dao.Databus.AdminEdit(c, oid, rpID, tp)
return
}
// AdminDelete delete reply by admin.
func (s *Service) AdminDelete(c context.Context, adid, oid, rpID, ftime int64, tp int8, moral int, notify bool, adname, remark string, reason, freason int8) (err error) {
// check subject
now := time.Now()
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rp, _ := s.reply(c, 0, oid, rpID, tp)
if rp == nil {
log.Error("s.Reply(oid:%v,tp:%v,:rpID:%v)", oid, tp, rpID, err)
return
} else if rp.AttrVal(reply.ReplyAttrAdminTop) == 1 {
err = ecode.ReplyDelTopForbidden
return
}
s.dao.Databus.AdminDelete(c, adid, oid, rpID, ftime, moral, notify, adname, remark, now.Unix(), tp, reason, freason)
return
}
// AdminPass recover reply by admin.
func (s *Service) AdminPass(c context.Context, adid, oid, rpID int64, tp int8, remark string) (err error) {
// check subject
now := time.Now()
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
s.dao.Databus.AdminPass(c, adid, oid, rpID, remark, now.Unix(), tp)
return
}
// AdminRecover recover reply by admin.
func (s *Service) AdminRecover(c context.Context, adid, oid, rpID int64, tp int8, remark string) (err error) {
// check subject
now := time.Now()
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
s.dao.Databus.AdminRecover(c, adid, oid, rpID, remark, now.Unix(), tp)
return
}
// AdminDeleteByReport delete report reply by admin.
func (s *Service) AdminDeleteByReport(c context.Context, adid, oid, rpID, ftime int64, tp int8, moral int, notify bool, adname, remark string, audit int8, reason int8, content string, freason int8) (err error) {
var (
rp *reply.Reply
now = time.Now()
)
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rpt, err := s.dao.Report.Get(c, oid, rpID)
if err != nil {
log.Error("s.report.GetReport(%d, %d) error(%v)", oid, rpID, err)
return
}
if rpt == nil {
err = ecode.ReplyReportNotExist
return
}
if rp, err = s.dao.Reply.Get(c, oid, rpID); err != nil {
log.Error("s.reply.GetReply(%d, %d) error(%v)", oid, rpID, err)
return
} else if rp == nil {
err = ecode.ReplyNotExist
return
} else if rp.AttrVal(reply.ReplyAttrAdminTop) == 1 {
err = ecode.ReplyDelTopForbidden
return
}
s.dao.Databus.AdminDeleteByReport(c, adid, oid, rpID, rpt.Mid, ftime, moral, notify, adname, remark, now.Unix(), tp, audit, reason, content, freason)
return
}
// AdminReportStateSet set report state by admin.
func (s *Service) AdminReportStateSet(c context.Context, adid, oid, rpID int64, tp, state int8) (err error) {
now := time.Now()
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rpt, err := s.dao.Report.Get(c, oid, rpID)
if err != nil {
log.Error("s.report.GetReport(%d, %d) met error (%v)", oid, rpID, err)
return
}
if rpt == nil {
err = ecode.ReplyReportNotExist
return
}
// dao.Kafka
s.dao.Databus.AdminStateSet(c, adid, oid, rpID, now.Unix(), tp, state)
return
}
// AdminReportTransfer transfer report by admin.
func (s *Service) AdminReportTransfer(c context.Context, adid, oid, rpID int64, tp, audit int8) (err error) {
now := time.Now()
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rpt, err := s.dao.Report.Get(c, oid, rpID)
if err != nil {
log.Error("s.report.GetReport(%d, %d) met error (%v)", oid, rpID, err)
return
}
if rpt == nil {
err = ecode.ReplyReportNotExist
return
}
// dao.Kafka
s.dao.Databus.AdminTransfer(c, adid, oid, rpID, now.Unix(), tp, audit)
return
}
// AdminReportIgnore ignore report by admin.
func (s *Service) AdminReportIgnore(c context.Context, adid, oid, rpID int64, tp, audit int8, remark string) (err error) {
now := time.Now()
// check subject
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
rpt, err := s.dao.Report.Get(c, oid, rpID)
if err != nil {
log.Error("s.report.GetReport(%d, %d) met error (%v)", oid, rpID, err)
return
}
if rpt == nil {
err = ecode.ReplyReportNotExist
return
}
// dao.Kafka
s.dao.Databus.AdminIgnore(c, adid, oid, rpID, now.Unix(), tp, audit)
return
}
// AdminAddTop add top reply by admin
func (s *Service) AdminAddTop(c context.Context, adid, oid, rpID int64, tp, act int8) (err error) {
var (
ts = time.Now().Unix()
r *reply.Reply
)
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
sub, err := s.Subject(c, oid, tp)
if err != nil {
log.Error("s.Subject(oid %v) err(%v)", oid, err)
return
}
if r, err = s.GetTop(c, sub, oid, tp, reply.ReplyAttrAdminTop); err != nil {
log.Error("s.GetTop(%d,%d) err(%v)", oid, tp, err)
return
}
if r != nil && act == 1 {
log.Warn("oid(%d) type(%d) already have top ", oid, tp)
err = ecode.ReplyHaveTop
return
}
if r == nil && act == 0 {
log.Warn("oid(%d) type(%d) do not have top ", oid, tp)
err = ecode.ReplyNotExist
return
}
// TODO: only need reply,no not need content and user info
if r, err = s.reply(c, 0, oid, rpID, tp); err != nil {
log.Error("s.GetReply err (%v)", err)
return
}
if r == nil {
log.Warn("oid(%d) type(%d) rpID(%d) do not exist ", oid, tp, rpID)
err = ecode.ReplyNotExist
return
}
if r.AttrVal(reply.ReplyAttrUpperTop) == 1 {
err = ecode.ReplyHaveTop
return
}
if r.Root != 0 {
log.Warn("oir(%d) type(%d) rpID(%d) not root reply", oid, tp, rpID)
err = ecode.ReplyNotRootReply
return
}
s.dao.Databus.AdminAddTop(c, adid, oid, rpID, ts, act, tp)
return
}
// AdminReportRecover recover report by admin.
func (s *Service) AdminReportRecover(c context.Context, adid, oid, rpID int64, tp, audit int8, remark string) (err error) {
// check subject
now := time.Now()
if !reply.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
s.dao.Databus.AdminReportRecover(c, adid, oid, rpID, remark, now.Unix(), tp, audit)
return
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"text/template"
model "go-common/app/interface/main/reply/model/reply"
"go-common/library/log"
"go-common/library/xstr"
)
var _emptyRecords = make([]*model.Record, 0)
// Records return reply record from es.
func (s *Service) Records(c context.Context, types []int64, mid, stime, etime int64, order, sort string, pn, ps int32) (res []*model.Record, total int32, err error) {
var midAts []int64
if res, total, err = s.search.RecordPaginate(c, types, mid, stime, etime, order, sort, pn, ps); err != nil {
log.Error("s.search.RecordPaginate(%d,%d,%d,%d,%s,%s) error(%v)", mid, sort, pn, ps, stime, etime, err)
return
}
if res == nil {
res = _emptyRecords
return
}
for _, r := range res {
r.Message = template.HTMLEscapeString(r.Message)
if len(r.Ats) == 0 {
continue
}
var ats []int64
if ats, err = xstr.SplitInts(r.Ats); err != nil {
log.Error("xstr.SplitInts(%s) error(%v)", r.Ats, err)
err = nil
}
midAts = append(midAts, ats...)
}
if len(midAts) == 0 {
return
}
accMap, _ := s.getAccInfo(c, midAts)
for _, r := range res {
r.FillAts(accMap)
}
return
}

View File

@@ -0,0 +1,442 @@
package service
import (
"context"
"fmt"
"time"
model "go-common/app/interface/main/reply/model/reply"
artmdl "go-common/app/interface/openplatform/article/model"
"go-common/app/service/main/archive/api"
arcmdl "go-common/app/service/main/archive/model/archive"
figmdl "go-common/app/service/main/figure/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/net/metadata"
"go-common/library/queue/databus/report"
xtime "go-common/library/time"
)
const (
_reportNormalCnt = 5
_reportAddSecs = 5
_reportMaxSecs = 180
)
// AddReport report a reply.
func (s *Service) AddReport(c context.Context, mid, oid, rpID int64, tp, reason int8, cont, platform string, build int64, buvid string) (cd int, err error) {
var (
r *model.Reply
now = time.Now()
ip = metadata.String(c, metadata.RemoteIP)
)
// check subject
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if err = model.CheckReportReason(reason); err != nil {
return
}
cnt, err := s.dao.Redis.GetUserReportCnt(c, mid, now)
if err != nil {
log.Error("AddReport failed, replyCacheDao.GetUserReportCnt(%d), err is (%v)", mid, err)
return
}
if cnt > _reportNormalCnt {
var ttl int
// When report count max 5 at one day, extra add 5s to user TTL at a time, and the max user TTL is 180s.
// The last report time, we set Redis key TTL for one day seconds, use one day seconds sub current TTL value is user TTL.
if ttl, err = s.dao.Redis.GetUserReportTTL(c, mid, now); err != nil {
log.Error("AddReport failed, replyCacheDao.GetUserReportTTL(%d), err is (%v)", mid, err)
return
}
if ttl >= 0 {
uttl := s.oneDaySec - ttl
maxttl := (cnt - _reportNormalCnt) * _reportAddSecs
if maxttl > _reportMaxSecs {
maxttl = _reportMaxSecs
}
if uttl < maxttl {
cd = maxttl - uttl
err = ecode.ReplyReportDeniedAsCD
return
}
}
}
if r, err = s.Reply(c, oid, tp, rpID); err != nil || r == nil || r.Content == nil || r.IsDeleted() {
return
}
// upper report reply of self in upper's arc ,treat it as del
if s.isUpper(c, mid, oid, tp) && r.Mid == mid {
s.dao.Databus.Delete(c, mid, oid, rpID, now.Unix(), tp, false)
report.User(&report.UserInfo{
Mid: mid,
Platform: platform,
Build: build,
Buvid: buvid,
Business: 41,
Type: int(r.Type),
Oid: r.Oid,
Action: model.ReportReplyDel,
Ctime: time.Now(),
IP: ip,
Index: []interface{}{
r.RpID,
r.State,
model.ReplyStateUpDel,
},
})
return
}
// 信用评分获取,用于优先举报排序处理
var score int
arg := &figmdl.ArgUserFigure{Mid: mid}
fig, err := s.figure.UserFigure(c, arg)
if err != nil {
log.Error("s.figure.UserFigure(mid:%d) error(%v)", mid, err)
err = nil
} else {
score = 100 - int(fig.Percentage)
}
ctime := xtime.Time(now.Unix())
rpt := &model.Report{
RpID: rpID,
Oid: oid,
Type: tp,
Mid: mid,
Reason: reason,
Count: 1,
Content: cont,
Score: score,
State: model.GetReportType(reason),
CTime: ctime,
MTime: ctime,
}
rptUser := &model.ReportUser{
Oid: oid,
Type: tp,
RpID: rpID,
Mid: mid,
Reason: reason,
Content: cont,
State: model.ReportUserStateNew,
CTime: ctime,
MTime: ctime,
}
if rpt.ID, err = s.dao.Report.InsertUser(c, rptUser); err != nil {
return
}
if rpt.ID == 0 {
err = ecode.ReplyReported
return
}
if tp != model.SubTypeActArc && tp != model.SubTypePlaylist && tp != model.SubTypeComicSeason && tp != model.SubTypeComicEpisode {
var (
m = make(map[int64]string)
title, link string
typeid int32
)
m[rpID] = ""
message := r.Content.Message
s.dao.FilterContents(c, m)
if m[rpID] != "" {
message = m[rpID]
}
title, link, typeid, _ = s.TitleLink(c, oid, tp)
if link != "" {
link = fmt.Sprintf("%s#reply%d", link, rpID)
}
err = s.workflow.AddReport(c, oid, tp, typeid, rpID, score, reason, mid, r.Mid, r.Like, message, link, title)
if err != nil {
return
}
}
if rpt.ID, err = s.dao.Report.Insert(c, rpt); err != nil {
return
}
// set report count and set user report dao.Redis key TTL for one day seconds.
if err = s.dao.Redis.SetUserReportCnt(c, mid, cnt+1, now); err != nil {
log.Error("s.redis.SetUserReportCnt(%v) error(%v) or row==0", rpt, err)
return
}
s.dao.Databus.AddReport(c, oid, rpID, tp)
report.User(&report.UserInfo{
Mid: rptUser.Mid,
Platform: platform,
Build: build,
Buvid: buvid,
Business: 41,
Type: int(tp),
Oid: oid,
Action: model.ReportReplyReport,
Ctime: now,
IP: ip,
Index: []interface{}{
r.Mid,
r.RpID,
r.State,
fmt.Sprint(reason),
},
Content: map[string]interface{}{
"count": cnt,
"score": score,
"content": cont,
},
})
log.Info("AddReport(%d) oid:%d mid:%d score:%d percentage:%v", rpt.ID, oid, mid, score, fig)
return
}
// ReportRelated get related replies of report.
func (s *Service) ReportRelated(c context.Context, mid, oid, rpid int64, tp int8, escape bool) (sub *model.Subject, root *model.Reply, related []*model.Reply, err error) {
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
// root reply
sub, root, snds, err := s.ReportReply(c, mid, oid, rpid, tp, 1, s.sndDefCnt, escape)
if err != nil {
return
}
root.Replies = snds
// related reply
start := root.Floor - 6
end := root.Floor + 6
relIDs, err := s.dao.Reply.GetIDsByFloorOffset(c, oid, tp, start, end)
if err != nil {
return
}
relMap, err := s.repliesMap(c, oid, tp, relIDs)
if err != nil {
return
}
related = make([]*model.Reply, 0, len(relMap))
bs := make([]*model.Reply, 0, len(relMap))
for _, rpID := range relIDs {
rp, ok := relMap[rpID]
if ok && rp.RpID != root.RpID {
if rp.Replies, err = s.reportReplies(c, rp, 1, s.sndDefCnt); err != nil {
return
}
related = append(related, rp)
// to build replies
bs = append(bs, rp)
bs = append(bs, rp.Replies...)
}
}
if err = s.buildReply(c, sub, bs, mid, escape); err != nil {
return
}
return
}
// ReportReply get report reply.
func (s *Service) ReportReply(c context.Context, mid, oid, rpid int64, tp int8, pn, ps int, escape bool) (sub *model.Subject, root *model.Reply, seconds []*model.Reply, err error) {
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
if sub, err = s.subject(c, oid, tp); err != nil {
return
}
if root, err = s.ReplyContent(c, oid, rpid, tp); err != nil {
return
}
if root.Root != 0 {
if root, err = s.ReplyContent(c, root.Oid, root.Root, root.Type); err != nil {
return
}
}
if seconds, err = s.reportReplies(c, root, pn, ps); err != nil {
return
}
bs := make([]*model.Reply, 0, len(seconds)+1)
bs = append(bs, root)
bs = append(bs, seconds...)
if err = s.buildReply(c, sub, bs, mid, escape); err != nil {
return
}
return
}
func (s *Service) reportReplies(c context.Context, rp *model.Reply, pn, ps int) (rs []*model.Reply, err error) {
var (
start = (pn - 1) * ps
)
rs = _emptyReplies
if start >= rp.Count {
return
}
sndIDs, err := s.dao.Reply.GetIDsByRootWithoutState(c, rp.Oid, rp.RpID, rp.Type, start, ps)
if err != nil {
return
}
sndMap, err := s.repliesMap(c, rp.Oid, rp.Type, sndIDs)
if err != nil {
return
}
rs = make([]*model.Reply, 0, len(sndMap))
for _, rpID := range sndIDs {
rp, ok := sndMap[rpID]
if ok {
rs = append(rs, rp)
}
}
return
}
func (s *Service) linkByOids(c context.Context, oids map[int64]string, typ int8) (err error) {
if len(oids) == 0 {
return
}
if typ == model.SubTypeActivity {
err = s.workflow.TopicsLink(c, oids, false)
} else {
for oid := range oids {
var link string
switch typ {
case model.SubTypeTopic:
link = fmt.Sprintf("https://www.bilibili.com/topic/%d.html", oid)
case model.SubTypeArchive:
link = fmt.Sprintf("https://www.bilibili.com/video/av%d", oid)
case model.SubTypeForbiden:
link = fmt.Sprintf("https://www.bilibili.com/blackroom/ban/%d", oid)
case model.SubTypeNotice:
link = fmt.Sprintf("https://www.bilibili.com/blackroom/notice/%d", oid)
case model.SubTypeActArc:
_, link, err = s.workflow.ActivitySub(c, oid)
if err != nil {
return
}
case model.SubTypeArticle:
link = fmt.Sprintf("https://www.bilibili.com/read/cv%d", oid)
case model.SubTypeMusic:
link = fmt.Sprintf("https://www.bilibili.com/audio/au%d", oid)
case model.SubTypeMusicList:
link = fmt.Sprintf("https://www.bilibili.com/audio/am%d", oid)
case model.SubTypeLive:
link = fmt.Sprintf("https://vc.bilibili.com/video/%d", oid)
case model.SubTypeLiveAct:
_, link, err = s.workflow.LiveActivityTitle(c, oid)
if err != nil {
return
}
case model.SubTypeLivePicture:
link = fmt.Sprintf("https://h.bilibili.com/ywh/%d", oid)
case model.SubTypeCredit:
link = fmt.Sprintf("https://www.bilibili.com/judgement/case/%d", oid)
case model.SubTypeDynamic:
link = fmt.Sprintf("https://t.bilibili.com/%d", oid)
case model.SubTypeLiveNotice:
link = fmt.Sprintf("http://link.bilibili.com/p/eden/news#/newsdetail?id=%d", oid)
default:
return
}
oids[oid] = link
}
}
return
}
// TitleLink TitleLink
func (s *Service) TitleLink(c context.Context, oid int64, typ int8) (title, link string, typeId int32, err error) {
switch typ {
case model.SubTypeArchive:
arg := &arcmdl.ArgAid2{
Aid: oid,
}
var m *api.Arc
m, err = s.arcSrv.Archive3(c, arg)
if err != nil || m == nil {
log.Error("s.arcSrv.Archive3(%v) ret:%v error(%v)", arg, m, err)
return
}
link = fmt.Sprintf("http://www.bilibili.com/video/av%d/", oid)
title = m.Title
typeId = m.TypeID
case model.SubTypeTopic:
if title, link, err = s.workflow.TopicTitle(c, oid); err != nil {
log.Error("s.noticeDao.Topic(%d) error(%v)", oid, err)
return
}
case model.SubTypeMusic:
link = fmt.Sprintf("https://www.bilibili.com/audio/au%d", oid)
case model.SubTypeMusicList:
link = fmt.Sprintf("https://www.bilibili.com/audio/am%d", oid)
case model.SubTypeActivity:
if title, link, err = s.workflow.TopicTitle(c, oid); err != nil {
log.Error("s.noticeDao.Activity(%d) error(%v)", oid, err)
return
}
case model.SubTypeForbiden:
title, link, err = s.workflow.BanTitle(c, oid)
if err != nil {
return
}
case model.SubTypeNotice:
title, link, err = s.workflow.NoticeTitle(c, oid)
if err != nil {
return
}
case model.SubTypeActArc:
if title, link, err = s.workflow.TopicTitle(c, oid); err != nil {
log.Error("s.noticeDao.ActivitySub(%d) error(%v)", oid, err)
return
}
case model.SubTypeArticle:
arg := &artmdl.ArgAids{
Aids: []int64{oid},
}
var m map[int64]*artmdl.Meta
m, err = s.articleSrv.ArticleMetas(c, arg)
if err != nil || m == nil {
log.Error("s.articleSrv.ArticleMetas(%v) ret:%v error(%v)", arg, m, err)
return
}
if meta, ok := m[oid]; ok {
title = meta.Title
link = fmt.Sprintf("http://www.bilibili.com/read/cv%d", oid)
}
case model.SubTypeLive:
if title, link, err = s.workflow.LiveVideoTitle(c, oid); err != nil {
log.Error("s.noticeDao.LiveSmallVideo(%d) error(%v)", oid, err)
return
}
case model.SubTypeLiveAct:
if title, link, err = s.workflow.LiveActivityTitle(c, oid); err != nil {
log.Error("s.noticeDao.LiveActivity(%d) error(%v)", oid, err)
return
}
case model.SubTypeLiveNotice:
if title, err = s.workflow.LiveNotice(c, oid); err != nil {
log.Error("s.noticeDao.LiveNotice(%d) error(%v)", oid, err)
return
}
link = fmt.Sprintf("http://link.bilibili.com/p/eden/news#/newsdetail?id=%d", oid)
return
case model.SubTypeLivePicture:
if title, link, err = s.workflow.LivePictureTitle(c, oid); err != nil {
log.Error("s.noticeDao.LivePiture(%d) error(%v)", oid, err)
return
}
case model.SubTypeCredit:
if title, link, err = s.workflow.CreditTitle(c, oid); err != nil {
log.Error("s.noticeDao.Credit(%d) error(%v)", oid, err)
return
}
case model.SubTypeDynamic:
if title, link, err = s.workflow.DynamicTitle(c, oid); err != nil {
log.Error("s.noticeDao.Dynamic(%d) error(%v)", oid, err)
return
}
case model.SubTypeHuoniao:
if title, link, err = s.workflow.HuoniaoTitle(c, oid); err != nil {
log.Error("s.workflow.HuoniaoTitle(%d) error(%v)", oid, err)
return
}
default:
return
}
return
}

View File

@@ -0,0 +1,98 @@
package service
import (
"context"
"testing"
model "go-common/app/interface/main/reply/model/reply"
)
func TestRepressEmotion(t *testing.T) {
got := repressEmotion("this is a test[12345]message", "[12345]")
shouldBe := "this is a test【12345】message"
if got != shouldBe {
t.Fatalf("repressEmotion Error, should be: %v, got: %v", shouldBe, got)
}
}
func TestRepressEmotions(t *testing.T) {
got := RepressEmotions("this [8888]is a test[12345]message[4657]", []string{"[12345]", "[8888]", "[4657]"})
shouldBe := "this 【8888】is a test【12345】message【4657】"
if got != shouldBe {
t.Fatalf("RepressEmotions Error, should be: %v, got: %v", shouldBe, got)
}
}
func TestGetReportReply(t *testing.T) {
s := &Service{}
s.ReportReply(context.Background(), 1, 1, 1, int8(2), 3, 4, true)
}
func TestGetReplyByIDs(t *testing.T) {
s := &Service{}
s.GetReplyByIDs(context.Background(), int64(1), int8(1), []int64{1, 2, 3, 4})
}
func TestCheckAssist(t *testing.T) {
s := &Service{}
s.CheckAssist(context.Background(), int64(1), 1)
}
func TestGetRelationMap(t *testing.T) {
s := &Service{}
s.GetRelationMap(context.Background(), 1, []int64{}, "")
}
func TestPing(t *testing.T) {
s := &Service{}
s.Ping(context.Background())
}
func TestClose(t *testing.T) {
s := &Service{}
s.Close()
}
func TestFillRootReplies(t *testing.T) {
s := &Service{}
s.FillRootReplies(context.Background(),
[]*model.Reply{},
22,
"123",
true,
&model.Subject{})
}
func TestGetBlacklist(t *testing.T) {
s := &Service{}
s.GetBlacklist(context.Background(), 56)
}
func TestGetFansMap(t *testing.T) {
s := &Service{}
s.GetFansMap(context.Background(), []int64{}, 11, "")
}
func TestGetReplyCounts(t *testing.T) {
s := &Service{}
s.GetReplyCounts(context.Background(), []int64{}, int8(1))
}
func TestGetAssistMap(t *testing.T) {
s := &Service{}
s.GetAssistMap(context.Background(), 11, "")
}
func TestUserBlockStatus(t *testing.T) {
s := &Service{}
s.UserBlockStatus(context.Background(), 1854)
}
func TestGetRootReplyIDs(t *testing.T) {
s := &Service{}
s.GetRootReplyIDs(context.Background(), 11, int8(1), int8(1), 22, 65)
}
func TestAdminReportRecover(t *testing.T) {
s := &Service{}
s.AdminReportRecover(context.Background(), 11, 12, 13, int8(1), int8(1), "")
}
func TestReplyContent(t *testing.T) {
s := &Service{}
s.ReplyContent(context.Background(), 11, 11, int8(1))
}

View File

@@ -0,0 +1,50 @@
package service
import (
"context"
accmdl "go-common/app/service/main/account/api"
feedmdl "go-common/app/service/main/reply-feed/api"
"go-common/library/log"
)
// nextID return next reply id.
func (s *Service) nextID(c context.Context) (int64, error) {
rpID, err := s.seqSrv.ID32(c, s.seqArg)
if err != nil {
log.Error("s.seqSrv.ID(%v) error(%v)", s.seqArg, err)
return 0, err
}
return int64(rpID), err
}
func (s *Service) userInfo(c context.Context, mid int64) (*accmdl.Profile, error) {
arg := &accmdl.MidReq{
Mid: mid,
}
res, err := s.acc.Profile3(c, arg)
if err != nil {
log.Error("s.acc.UserInfo(%d) error(%v)", mid, err)
return nil, err
}
return res.Profile, nil
}
func (s *Service) replyFeed(c context.Context, mid int64, pn, ps int) (res *feedmdl.ReplyRes, err error) {
req := &feedmdl.ReplyReq{Mid: mid, Pn: int32(pn), Ps: int32(ps)}
if res, err = s.feedClient.Reply(c, req); err != nil {
log.Error("s.feedClient.Reply(%v) error(%v)", req, err)
return nil, err
}
return
}
func (s *Service) replyHotFeed(c context.Context, mid, oid int64, tp, pn, ps int) (res *feedmdl.HotReplyRes, err error) {
req := &feedmdl.HotReplyReq{Mid: mid, Oid: oid, Tp: int32(tp), Pn: int32(pn), Ps: int32(ps)}
if res, err = s.feedClient.HotReply(c, req); err != nil {
log.Error("s.feedClient.HotReply(%v) error(%v)", req, err)
return nil, err
}
return
}

View File

@@ -0,0 +1,336 @@
package service
import (
"context"
"encoding/json"
"runtime"
"strconv"
"time"
"go-common/app/interface/main/reply/conf"
"go-common/app/interface/main/reply/dao/bigdata"
"go-common/app/interface/main/reply/dao/drawyoo"
"go-common/app/interface/main/reply/dao/fans"
"go-common/app/interface/main/reply/dao/reply"
"go-common/app/interface/main/reply/dao/search"
"go-common/app/interface/main/reply/dao/vip"
"go-common/app/interface/main/reply/dao/workflow"
model "go-common/app/interface/main/reply/model/reply"
artrpc "go-common/app/interface/openplatform/article/rpc/client"
accrpc "go-common/app/service/main/account/api"
arcrpc "go-common/app/service/main/archive/api/gorpc"
assrpc "go-common/app/service/main/assist/rpc/client"
figrpc "go-common/app/service/main/figure/rpc/client"
filgrpc "go-common/app/service/main/filter/api/grpc/v1"
replyfeed "go-common/app/service/main/reply-feed/api"
locrpc "go-common/app/service/main/location/rpc/client"
seqmdl "go-common/app/service/main/seq-server/model"
seqrpc "go-common/app/service/main/seq-server/rpc/client"
thumbup "go-common/app/service/main/thumbup/rpc/client"
ugcpay "go-common/app/service/main/ugcpay/api/grpc/v1"
"go-common/library/log"
"go-common/library/log/infoc"
"go-common/library/sync/pipeline/fanout"
)
var _defMemberJSON = []byte(`{"mid":"0","uname":"","sex":"保密","sign":"没签名","avatar":"http://static.hdslb.com/images/member/noface.gif","rank":"10000","DisplayRank":"0","level_info":{"current_level":0,"current_min":0,"current_exp":0,"next_exp":0},"pendant":{"pid":0,"name":"","image":"","expire":0},"nameplate":{"nid":0,"name":"","image":"","image_small":"","level":"","condition":""},"official_verify":{"type":-1,"desc":""},"vip":{"vipType":0,"vipDueDate":0,"dueRemark":"","accessStatus":1,"vipStatus":0,"vipStatusWarn":""}}`)
// Service is reply service
type Service struct {
sndDefCnt int
oneDaySec int // If block time from word filter is greater than one day, then banned reply and add moral to account.
useBigData bool
// content filter
bigdata *bigdata.Dao
drawyoo *drawyoo.Dao
// fans http get method
fans *fans.Dao
// http request
search *search.Dao
// rpc client
acc accrpc.AccountClient
feedClient replyfeed.ReplyFeedClient
filcli filgrpc.FilterClient
location *locrpc.Service
assist *assrpc.Service
figure *figrpc.Service
thumbup thumbup.ThumbupRPC
// seq rpc
seqArg *seqmdl.ArgBusiness
seqSrv *seqrpc.Service2
// workflow dao
workflow *workflow.Dao
arcSrv *arcrpc.Service2
articleSrv *artrpc.Service
ugcpay ugcpay.UGCPayClient
// reply cache dao
dao *reply.Dao
// asynchronized add cache use channel
replyChan chan replyChan
topRpChan chan topRpChan
// vip
vip *vip.Dao
emojisM map[string]int64
emojis []*model.EmojiPackage
// reply notice
notice map[int8][]*model.Notice
// default member
defMember *model.Info
cache *fanout.Fanout
typeMapping map[int32]string
aliasMapping map[string]int32
aidWhiteList []int64
infoc *infoc.Infoc
bnjAid map[int64]struct{}
sortByHot map[int64]int8
sortByTime map[int64]int8
hideFloor map[int64]int8
hotReplyConfig map[int8]map[int64]int
}
// New new a service
func New(c *conf.Config) (s *Service) {
s = &Service{
cache: fanout.New("cache", fanout.Worker(1), fanout.Buffer(1024)),
sndDefCnt: conf.Conf.Reply.SecondDefSize,
oneDaySec: 24 * 60 * 60,
useBigData: conf.Conf.Reply.BigdataFilter,
replyChan: make(chan replyChan, _replyChanBuf),
topRpChan: make(chan topRpChan, _topRpChanBuf),
aidWhiteList: c.Reply.AidWhiteList,
arcSrv: arcrpc.New2(c.RPCClient2.Archive),
articleSrv: artrpc.New(c.RPCClient2.Article),
infoc: infoc.New(c.Infoc),
bnjAid: make(map[int64]struct{}),
sortByTime: make(map[int64]int8),
sortByHot: make(map[int64]int8),
hideFloor: make(map[int64]int8),
hotReplyConfig: make(map[int8]map[int64]int),
}
for oidStr, tp := range c.Reply.SortByHotOids {
var (
oid int64
err error
)
if oid, err = strconv.ParseInt(oidStr, 10, 64); err != nil {
panic(err)
}
s.sortByHot[oid] = tp
}
for oidStr, tp := range c.Reply.SortByTimeOids {
var (
oid int64
err error
)
if oid, err = strconv.ParseInt(oidStr, 10, 64); err != nil {
panic(err)
}
s.sortByTime[oid] = tp
}
for oidStr, tp := range c.Reply.HideFloorOids {
var (
oid int64
err error
)
if oid, err = strconv.ParseInt(oidStr, 10, 64); err != nil {
panic(err)
}
s.hideFloor[oid] = tp
}
for tpStr, mapping := range c.Reply.HotReplyConfig {
var (
tp int64
oid int64
err error
)
if tp, err = strconv.ParseInt(tpStr, 10, 64); err != nil {
panic(err)
}
businessConfig := make(map[int64]int)
for oidStr, num := range mapping {
if oid, err = strconv.ParseInt(oidStr, 10, 64); err != nil {
panic(err)
}
businessConfig[oid] = num
}
s.hotReplyConfig[int8(tp)] = businessConfig
}
for _, oid := range c.Reply.BnjAidList {
s.bnjAid[oid] = struct{}{}
}
acc, err := accrpc.NewClient(c.AccountGRPCClient)
if err != nil {
panic(err)
}
s.acc = acc
filcli, err := filgrpc.NewClient(c.FilterGRPCClient)
if err != nil {
panic(err)
}
s.filcli = filcli
feedClient, err := replyfeed.NewClient(c.FeedGRPCClient)
if err != nil {
panic(err)
}
s.feedClient = feedClient
// init depend dao
s.fans = fans.New(c)
s.search = search.New(c)
s.drawyoo = drawyoo.New(c)
s.bigdata = bigdata.New(c)
s.vip = vip.New(c)
s.emojisM = make(map[string]int64)
s.emojis = make([]*model.EmojiPackage, 0)
// init rpc client
s.location = locrpc.New(c.RPCClient2.Location)
s.assist = assrpc.New(c.RPCClient2.Assist)
s.figure = figrpc.New(c.RPCClient2.Figure)
s.seqSrv = seqrpc.New2(c.RPCClient2.Seq)
s.seqArg = &seqmdl.ArgBusiness{Token: c.Seq.Token, BusinessID: c.Seq.BusinessID}
s.thumbup = thumbup.New(c.RPCClient2.Thumbup)
s.workflow = workflow.New(c)
s.ugcpay, err = ugcpay.NewClient(nil)
if err != nil {
panic(err)
}
// init reply cache dao
s.dao = reply.New(c)
// default member
s.defMember = &model.Info{}
s.typeMapping = make(map[int32]string)
s.aliasMapping = make(map[string]int32)
if err := json.Unmarshal(_defMemberJSON, s.defMember); err != nil {
panic(err)
}
for i := 0; i < runtime.NumCPU(); i++ {
go s.cacheproc()
}
s.loadEmoji()
s.loadRplNotice()
s.loadBusiness()
go s.loadproc()
return
}
// TypeToAlias map type to alias
func (s *Service) TypeToAlias(t int32) (alias string, exists bool) {
alias, exists = s.typeMapping[t]
return
}
// AliasToType map alias to type
func (s *Service) AliasToType(alias string) (t int32, exists bool) {
t, exists = s.aliasMapping[alias]
return
}
func (s *Service) loadproc() {
for {
s.loadEmoji()
s.loadRplNotice()
s.loadBusiness()
time.Sleep(time.Duration(conf.Conf.Reply.EmojiExpire))
}
}
// Ping check service health
func (s *Service) Ping(c context.Context) (err error) {
return s.dao.Ping(c)
}
// Close close service connection
func (s *Service) Close() {
s.dao.Close()
}
// ResetFloor ...
func (s *Service) ResetFloor(rps ...*model.Reply) {
for _, rp := range rps {
if rp == nil {
continue
}
rp.Floor = 0
if rp.Replies == nil {
continue
}
for _, childRp := range rp.Replies {
if childRp == nil {
continue
}
childRp.Floor = 0
}
}
}
// hotNum ...
func (s *Service) hotNumWeb(oid int64, tp int8) int {
if businessConfig, ok := s.hotReplyConfig[tp]; ok {
if num, exists := businessConfig[oid]; exists {
return num
}
}
return _hotSizeWeb
}
// hotNum ...
func (s *Service) hotNum(oid int64, tp int8) int {
if businessConfig, ok := s.hotReplyConfig[tp]; ok {
if num, exists := businessConfig[oid]; exists {
return num
}
}
return _hotSize
}
// IsBnj avid...
func (s *Service) IsBnj(oid int64, tp int8) bool {
if _, ok := s.bnjAid[oid]; ok && tp == model.SubTypeArchive {
return true
}
return false
}
// ShowFloor ...
func (s *Service) ShowFloor(oid int64, tp int8) bool {
if typ, ok := s.hideFloor[oid]; ok && typ == tp {
return false
}
return true
}
// loadEmoji load emoji
func (s *Service) loadEmoji() (err error) {
var (
emojis = make([]*model.EmojiPackage, 0)
c = context.Background()
emjM = make(map[string]int64)
)
emjs, err := s.dao.Emoji.ListEmojiPack(c)
if err != nil {
log.Error("service.ListEmojiPack error (%v)", err)
return err
}
for _, d := range emjs {
d.Emojis, err = s.dao.Emoji.EmojiListByPid(c, d.ID)
if err != nil {
log.Error("service.EmojiListByPid err (%v)", err)
return err
}
for _, emo := range d.Emojis {
emjM[emo.Name] = emo.ID
}
if len(d.Emojis) > 0 {
emojis = append(emojis, d)
}
}
s.emojis = emojis
s.emojisM = emjM
return
}

View File

@@ -0,0 +1,43 @@
package service
import (
"context"
"flag"
"path/filepath"
"testing"
"time"
"go-common/app/interface/main/reply/conf"
. "github.com/smartystreets/goconvey/convey"
)
var (
testSvr *Service
)
func init() {
dir, _ := filepath.Abs("../cmd/reply-test.toml")
flag.Set("conf", dir)
conf.Init()
testSvr = New(conf.Conf)
time.Sleep(time.Second)
}
func CleanCache() {
//c := context.Background()
//pool := redis.NewPool(conf.Conf.Redis.Config)
//pool.Get(c).Do("FLUSHDB")
}
func WithService(f func(s *Service)) func() {
return func() {
Reset(func() { CleanCache() })
f(testSvr)
}
}
func TestFetchFans(t *testing.T) {
s := &Service{}
s.FetchFans(context.Background(), []int64{1, 2}, 11)
}

View File

@@ -0,0 +1,119 @@
package service
import (
"context"
model "go-common/app/interface/main/reply/model/reply"
"go-common/library/ecode"
"go-common/library/log"
)
func (s *Service) subject(c context.Context, oid int64, tp int8) (sub *model.Subject, err error) {
if sub, err = s.dao.Mc.GetSubject(c, oid, tp); err != nil {
log.Error("replyCacheDao.GetSubject(%d,%d) error(%v)", oid, tp, err)
return
} else if sub == nil {
if sub, err = s.dao.Subject.Get(c, oid, tp); err != nil {
log.Error("s.subject.Get(%d,%d) error(%v)", oid, tp, err)
return
}
if sub == nil {
sub = &model.Subject{ID: -1, Oid: oid, Type: tp, State: model.SubStateForbid} // empty cache
}
s.cache.Do(c, func(ctx context.Context) {
if err = s.dao.Mc.AddSubject(ctx, sub); err != nil {
log.Error("s.dao.Mc.AddSubject(%d,%d) error(%v)", oid, tp, err)
}
})
}
return
}
// ReplyCount get all reply count.
func (s *Service) ReplyCount(c context.Context, oid int64, tp int8) (count int, err error) {
if !model.LegalSubjectType(tp) {
err = ecode.ReplyIllegalSubType
return
}
sub, err := s.subject(c, oid, tp)
if err != nil {
return
}
if sub != nil && sub.State != model.SubStateForbid {
count = sub.ACount
}
return
}
// GetReplyCounts get reply counts.
func (s *Service) GetReplyCounts(ctx context.Context, oids []int64, otyp int8) (map[int64]*model.Counts, error) {
caches, missIDs, err := s.dao.Mc.GetMultiSubject(ctx, oids, otyp)
if err != nil {
return nil, err
}
if len(missIDs) > 0 {
miss, err := s.dao.Subject.Gets(ctx, missIDs, otyp)
if err != nil {
log.Error("s.subject.Gets(%v,%d) error(%v)", missIDs, otyp, err)
return nil, err
}
if caches == nil {
caches = make(map[int64]*model.Subject)
}
var subs []*model.Subject
for _, oid := range missIDs {
sub, ok := miss[oid]
if !ok {
sub = &model.Subject{ID: -1, Oid: oid, Type: otyp, State: model.SubStateForbid}
}
caches[oid] = sub
subs = append(subs, sub)
}
s.cache.Do(ctx, func(ctx context.Context) { s.dao.Mc.AddSubject(ctx, subs...) })
}
counts := make(map[int64]*model.Counts)
for _, sub := range caches {
counts[sub.Oid] = &model.Counts{
SubjectState: sub.State,
Counts: int64(sub.ACount),
}
}
return counts, nil
}
// ReplyCounts get all reply count.
func (s *Service) ReplyCounts(c context.Context, oids []int64, tp int8) (counts map[int64]int, err error) {
caches, missIDs, err := s.dao.Mc.GetMultiSubject(c, oids, tp)
if err != nil {
return
}
if len(missIDs) > 0 {
var miss map[int64]*model.Subject
if miss, err = s.dao.Subject.Gets(c, missIDs, tp); err != nil {
log.Error("s.subject.Gets(%v,%d) error(%v)", missIDs, tp, err)
return
}
if caches == nil {
caches = make(map[int64]*model.Subject, len(miss))
}
subs := make([]*model.Subject, 0, len(miss))
for _, oid := range missIDs {
sub, ok := miss[oid]
if !ok {
sub = &model.Subject{ID: -1, Oid: oid, Type: tp, State: model.SubStateForbid} // empty cache
}
caches[oid] = sub
subs = append(subs, sub)
}
s.cache.Do(c, func(ctx context.Context) { s.dao.Mc.AddSubject(ctx, subs...) })
}
counts = make(map[int64]int, len(caches))
for _, sub := range caches {
if sub.State == model.SubStateForbid {
counts[sub.Oid] = 0
} else {
counts[sub.Oid] = sub.ACount
}
}
return
}

View File

@@ -0,0 +1,32 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestSubject(t *testing.T) {
c := context.Background()
Convey("subject test", t, WithService(func(s *Service) {
_, err := s.Subject(c, 1, 1)
So(err, ShouldBeNil)
}))
Convey("subject test", t, WithService(func(s *Service) {
_, err := s.AdminGetSubject(c, 1, 1)
So(err, ShouldBeNil)
}))
Convey("subject test", t, WithService(func(s *Service) {
err := s.AdminSubjectMid(c, 1, 1, 1, 1, "test")
So(err, ShouldBeNil)
}))
Convey("subject test", t, WithService(func(s *Service) {
err := s.AdminSubRegist(c, 1, 1, 1, 1, "test")
So(err, ShouldBeNil)
}))
Convey("subject test", t, WithService(func(s *Service) {
_, err := s.GetSubject(c, 1, 1)
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,388 @@
package service
import (
"context"
"sort"
"go-common/app/interface/main/reply/model/reply"
xmodel "go-common/app/interface/main/reply/model/xreply"
"go-common/library/sync/errgroup.v2"
)
const (
_secondRepliesPn = 1
_secondRepliesPs = 3
_hotSize = 5
_hotSizeWeb = 3
)
func fillHot(rs []*reply.Reply, sub *reply.Subject, maxSize int) (hots []*reply.Reply) {
for _, r := range rs {
if r.Like >= 3 && !r.IsTop() && !r.IsDeleted() {
hots = append(hots, r)
}
}
if hots == nil {
hots = _emptyReplies
} else if len(hots) > maxSize {
hots = hots[:maxSize]
}
if sub.RCount <= 20 && len(hots) > 0 {
hots = _emptyReplies
}
return hots
}
// buildResCursor ...
func buildResCursor(reqCursor *xmodel.Cursor, replies []*reply.Reply, cursorMode int) (resCursor xmodel.CursorRes) {
length := len(replies)
switch cursorMode {
case xmodel.CursorModePage:
// next和prev只可能一个不为0
curPage := reqCursor.Next + reqCursor.Prev
if reqCursor.Latest() {
resCursor.IsBegin = true
if length == 0 {
resCursor.Next = curPage
resCursor.Prev = curPage
resCursor.IsEnd = true
} else {
// 下一页是第二页
resCursor.Next = 2
resCursor.Prev = 1
if length < reqCursor.Ps {
resCursor.IsEnd = true
}
}
} else if reqCursor.Forward() {
resCursor.Next = curPage + 1
resCursor.Prev = curPage
if length < reqCursor.Ps {
resCursor.IsEnd = true
}
} else if reqCursor.Backward() {
resCursor.Next = curPage
resCursor.Prev = curPage - 1
if length < reqCursor.Ps {
resCursor.IsBegin = true
}
}
case xmodel.CursorModeCursor:
if reqCursor.Latest() {
resCursor.IsBegin = true
// Latest, 点进来默认访问的情况
if length == 0 {
resCursor.Next = reqCursor.Next
resCursor.Prev = reqCursor.Prev
resCursor.IsEnd = true
} else {
resCursor.Next = replies[length-1].Floor
resCursor.Prev = replies[0].Floor
if length < reqCursor.Ps {
resCursor.IsEnd = true
}
}
} else if reqCursor.Forward() {
if length == 0 {
resCursor.Next = reqCursor.Next
resCursor.Prev = reqCursor.Prev
resCursor.IsEnd = true
} else {
resCursor.Next = replies[length-1].Floor
resCursor.Prev = replies[0].Floor
if length < reqCursor.Ps {
resCursor.IsEnd = true
}
}
} else if reqCursor.Backward() {
if length == 0 {
resCursor.Next = reqCursor.Next
resCursor.Prev = reqCursor.Prev
resCursor.IsBegin = true
} else {
resCursor.Next = replies[length-1].Floor
resCursor.Prev = replies[0].Floor
if length < reqCursor.Ps {
resCursor.IsBegin = true
}
}
}
}
return
}
// replyMisc ...
func (s *Service) replyCommonRes(ctx context.Context, mid, oid int64, tp int8, sub *reply.Subject) (res xmodel.CommonRes) {
if sub == nil {
return
}
g := errgroup.WithContext(ctx)
g.Go(func(ctx context.Context) error {
if s.RelationBlocked(ctx, sub.Mid, mid) {
res.Blacklist = 1
}
return nil
})
g.Go(func(ctx context.Context) error {
if ok, _ := s.CheckAssist(ctx, sub.Mid, mid); ok {
res.Assist = 1
}
return nil
})
// 默认都是展示
res.Config.ShowAdmin, res.Config.ShowEntry, res.Config.ShowFloor = 1, 1, 1
g.Go(func(ctx context.Context) error {
if cfg, _ := s.GetReplyLogConfig(ctx, sub, 1); cfg != nil {
res.Config.ShowAdmin, res.Config.ShowEntry = cfg.ShowAdmin, cfg.ShowEntry
}
// 特殊稿件不显示楼层
if !s.ShowFloor(oid, tp) {
res.Config.ShowFloor = 0
}
return nil
})
g.Wait()
res.Upper.Mid = sub.Mid
return
}
// fillXreplyRes ...
func (s *Service) fillXreplyRes(ctx context.Context, sub *reply.Subject, req *xmodel.ReplyReq, res *xmodel.ReplyRes) {
res.CommonRes = s.replyCommonRes(ctx, req.Mid, req.Oid, req.Type, sub)
res.Notice = s.RplyNotice(ctx, req.Plat, req.Build, req.MobiApp, req.Buvid)
// 过滤掉可能的脏数据
res.Replies = s.FilDelReply(res.Replies)
res.Hots = s.FilDelReply(res.Hots)
// 过滤掉赞数小于3的
res.Hots = fillHot(res.Hots, sub, s.hotNum(req.Oid, req.Type))
res.Cursor.AllCount = sub.ACount
res.Folder = sub.Folder()
}
// Xreply ...
func (s *Service) Xreply(ctx context.Context, req *xmodel.ReplyReq) (res *xmodel.ReplyRes, err error) {
var (
sub *reply.Subject
cursor *reply.Cursor
cursorRes *reply.RootReplyList
pageRes *reply.PageResult
)
res = new(xmodel.ReplyRes)
mode, supportMode := req.ModeInfo(s.sortByHot, s.sortByTime)
switch mode {
case xmodel.ModeOrigin, xmodel.ModeTime:
// 这里原来用的闭区间,所以这里有坑
if req.Cursor.Forward() {
if req.Cursor.Next > 1 {
req.Cursor.Next--
}
} else if req.Cursor.Backward() {
req.Cursor.Prev++
}
if cursor, err = reply.NewCursor(int64(req.Cursor.Next), int64(req.Cursor.Prev), req.Cursor.Ps, reply.OrderDESC); err != nil {
return
}
if req.Cursor.Backward() {
if cursor, err = reply.NewCursor(int64(req.Cursor.Next), int64(req.Cursor.Prev), req.Cursor.Ps, reply.OrderASC); err != nil {
return
}
}
cursorParams := &reply.CursorParams{
IP: req.IP,
Oid: req.Oid,
OTyp: req.Type,
Sort: reply.SortByFloor,
HTMLEscape: false,
Cursor: cursor,
HotSize: s.hotNum(req.Oid, req.Type),
Mid: req.Mid,
}
if cursorRes, err = s.GetRootReplyListByCursor(ctx, cursorParams); err != nil {
return
}
sub = cursorRes.Subject
res.Replies = cursorRes.Roots
if cursorRes.Header != nil {
res.Top.Admin = cursorRes.Header.TopAdmin
res.Top.Upper = cursorRes.Header.TopUpper
res.Hots = cursorRes.Header.Hots
// 纯按楼层排序 去掉热评
if mode == xmodel.ModeTime {
res.Hots = _emptyReplies
}
}
if !sort.SliceIsSorted(res.Replies, func(i, j int) bool { return res.Replies[i].Floor > res.Replies[j].Floor }) {
sort.Slice(res.Replies, func(i, j int) bool { return res.Replies[i].Floor > res.Replies[j].Floor })
}
// 这里原来用的闭区间,所以这里有坑
if req.Cursor.Next == 1 {
res.Replies = _emptyReplies
}
// 由服务端来控制翻页逻辑
res.Cursor = buildResCursor(&xmodel.Cursor{Prev: req.Cursor.Prev, Next: req.Cursor.Next, Ps: req.Cursor.Ps}, res.Replies, xmodel.CursorModeCursor)
case xmodel.ModeHot:
var curPage = req.Cursor.Prev + req.Cursor.Next
if req.Cursor.Latest() {
curPage = 1
}
pageParams := &reply.PageParams{
Mid: req.Mid,
Oid: req.Oid,
Type: req.Type,
Sort: reply.SortByLike,
PageNum: curPage,
PageSize: req.Cursor.Ps,
NeedSecond: true,
Escape: false,
NeedHot: false,
}
if pageRes, err = s.RootReplies(ctx, pageParams); err != nil {
return
}
sub = pageRes.Subject
res.Replies = pageRes.Roots
res.Top.Admin = pageRes.TopAdmin
res.Top.Upper = pageRes.TopUpper
// 按页码翻页控制返回页码
res.Cursor = buildResCursor(&xmodel.Cursor{Prev: req.Cursor.Prev, Next: req.Cursor.Next, Ps: req.Cursor.Ps}, res.Replies, xmodel.CursorModePage)
}
res.Cursor.Mode = mode
res.Cursor.SupportMode = supportMode
s.fillXreplyRes(ctx, sub, req, res)
return
}
// SubFoldedReply ...
func (s *Service) SubFoldedReply(ctx context.Context, req *xmodel.SubFolderReq) (res *xmodel.SubFolderRes, err error) {
var (
rpIDs []int64
rootMap map[int64]*reply.Reply
childrenMap map[int64][]*reply.Reply
rootRps []*reply.Reply
childrenRps []*reply.Reply
sub *reply.Subject
)
res = new(xmodel.SubFolderRes)
if req.Cursor.Backward() {
return
}
cursor := &xmodel.Cursor{
Ps: req.Cursor.Ps,
Next: req.Cursor.Next,
}
if sub, err = s.Subject(ctx, req.Oid, req.Type); err != nil {
return
}
if rpIDs, err = s.foldedReplies(ctx, sub, 0, cursor); err != nil {
return
}
if rootMap, err = s.repliesMap(ctx, req.Oid, req.Type, rpIDs); err != nil {
return
}
if childrenMap, childrenRps, err = s.secondReplies(ctx, sub, rootMap, req.Mid, _secondRepliesPn, _secondRepliesPs); err != nil {
return
}
for _, rpID := range rpIDs {
if r, ok := rootMap[rpID]; ok {
if children, hasChild := childrenMap[rpID]; hasChild {
r.Replies = children
childrenRps = append(childrenRps, children...)
} else {
r.Replies = _emptyReplies
}
rootRps = append(rootRps, r)
}
}
if rootRps != nil {
res.Replies = rootRps
} else {
res.Replies = _emptyReplies
}
var rps []*reply.Reply
rps = append(rps, rootRps...)
rps = append(rps, childrenRps...)
if err = s.buildReply(ctx, sub, rps, req.Mid, false); err != nil {
return
}
res.Cursor = buildResCursor(&xmodel.Cursor{Prev: req.Cursor.Prev, Next: req.Cursor.Next, Ps: req.Cursor.Ps}, res.Replies, xmodel.CursorModeCursor)
res.CommonRes = s.replyCommonRes(ctx, req.Mid, req.Oid, req.Type, sub)
return
}
// RootFoldedReply ...
func (s *Service) RootFoldedReply(ctx context.Context, req *xmodel.RootFolderReq) (res *xmodel.RootFolderRes, err error) {
var (
rpIDs []int64
childrenMap map[int64]*reply.Reply
childrenRps []*reply.Reply
sub *reply.Subject
)
res = new(xmodel.RootFolderRes)
if req.Cursor.Backward() {
return
}
cursor := &xmodel.Cursor{
Ps: req.Cursor.Ps,
Next: req.Cursor.Next,
}
if sub, err = s.Subject(ctx, req.Oid, req.Type); err != nil {
return
}
if rpIDs, err = s.foldedReplies(ctx, sub, req.Root, cursor); err != nil {
return
}
if childrenMap, err = s.repliesMap(ctx, req.Oid, req.Type, rpIDs); err != nil {
return
}
for _, rpID := range rpIDs {
if r, ok := childrenMap[rpID]; ok {
childrenRps = append(childrenRps, r)
}
}
if childrenRps != nil {
res.Replies = childrenRps
} else {
res.Replies = _emptyReplies
}
if err = s.buildReply(ctx, sub, res.Replies, req.Mid, false); err != nil {
return
}
// 只有往下翻
res.Cursor = buildResCursor(&xmodel.Cursor{Prev: req.Cursor.Prev, Next: req.Cursor.Next, Ps: req.Cursor.Ps}, res.Replies, xmodel.CursorModeCursor)
res.CommonRes = s.replyCommonRes(ctx, req.Mid, req.Oid, req.Type, sub)
return
}
// foldedReplies ...
func (s *Service) foldedReplies(ctx context.Context, sub *reply.Subject, root int64, cursor *xmodel.Cursor) (rpIDs []int64, err error) {
var (
kind string
ID int64
ok bool
)
if root == 0 {
kind = xmodel.FolderKindSub
ID = sub.Oid
} else {
kind = xmodel.FolderKindRoot
ID = root
}
if ok, err = s.dao.Redis.ExpireFolder(ctx, kind, ID); err != nil {
return
}
if ok {
if rpIDs, err = s.dao.Redis.FolderByCursor(ctx, kind, ID, cursor); err != nil {
return
}
} else {
if rpIDs, err = s.dao.Reply.FoldedRepliesCursor(ctx, sub.Oid, sub.Type, root, cursor); err != nil {
return
}
s.cache.Do(ctx, func(ctx context.Context) {
s.dao.Databus.RecoverFolderIdx(ctx, sub.Oid, sub.Type, root)
})
}
return
}