Files
go-common/app/interface/main/reply/service/cursor.go
2019-04-22 18:49:16 +08:00

1108 lines
31 KiB
Go

package service
import (
"context"
"errors"
"strconv"
"time"
"go-common/app/interface/main/reply/dao/reply"
model "go-common/app/interface/main/reply/model/reply"
xmodel "go-common/app/interface/main/reply/model/xreply"
accmdl "go-common/app/service/main/account/api"
assmdl "go-common/app/service/main/assist/model/assist"
"go-common/library/ecode"
"go-common/library/log"
"sort"
"go-common/library/sync/errgroup.v2"
)
const (
defaultChildrenSize = 5
)
// NewCursorByReplyID NewCursorByReplyID
func (s *Service) NewCursorByReplyID(ctx context.Context, oid int64,
otyp int8, replyID int64, size int, cmp model.Comp) (*model.Cursor, error) {
rs, err := s.GetReplyByIDs(ctx, oid, otyp, []int64{replyID})
if err != nil {
return nil, err
}
if r, ok := rs[replyID]; ok {
return model.NewCursor(int64(r.Floor), 0, size, cmp)
}
return nil, ecode.ReplyNotExist
}
// NewSubCursorByReplyID NewSubCursorByReplyID
func (s *Service) NewSubCursorByReplyID(ctx context.Context, oid int64, otyp int8, replyID int64, size int, cmp model.Comp) (rootID int64, cursor *model.Cursor, err error) {
rs, err := s.GetReplyByIDs(ctx, oid, otyp, []int64{replyID})
if err != nil {
return 0, nil, err
}
if r, ok := rs[replyID]; ok {
if r.IsRoot() {
rootID = r.RpID
cursor, err = model.NewCursor(0, 1, size, cmp)
return
}
// 不足一页面时,展示够一页
floor := r.Floor
if floor < size {
floor = size
}
rootID = r.Root
cursor, err = model.NewCursor(int64(floor), 0, size, cmp)
return
}
return 0, nil, ecode.ReplyNotExist
}
// GetRootReplyListHeader GetRootReplyListHeader
func (s *Service) GetRootReplyListHeader(ctx context.Context, sub *model.Subject, params *model.CursorParams) (*model.RootReplyListHeader, error) {
var hotIDs []int64
res, err := s.replyHotFeed(ctx, params.Mid, sub.Oid, int(sub.Type), 1, params.HotSize+2)
if err == nil && res != nil && len(res.RpIDs) > 0 {
log.Info("reply-feed(test): reply abtest mid(%d) oid(%d) type(%d) test name(%s) rpIDs(%v)", params.Mid, sub.Oid, sub.Type, res.Name, res.RpIDs)
hotIDs = res.RpIDs
} else {
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)", params.Mid, sub.Oid, sub.Type, res.Name, res.RpIDs)
}
if hotIDs, err = s.GetRootReplyIDs(ctx, sub.Oid, sub.Type, model.SortByLike, 0, int64(params.HotSize+2)); err != nil {
log.Error("%v", err)
return nil, err
}
}
var parentIDs []int64
parentIDs = append(parentIDs, hotIDs...)
var adminTopReply, upperTopReply *model.Reply
if sub.AttrVal(model.SubAttrAdminTop) == model.AttrYes {
adminTopReply, err = s.GetTopReply(ctx, params.Oid, params.OTyp, model.SubAttrAdminTop)
if err != nil {
return nil, err
}
if adminTopReply != nil {
parentIDs = append(parentIDs, adminTopReply.RpID)
}
}
if sub.AttrVal(model.SubAttrUpperTop) == model.AttrYes {
upperTopReply, err = s.GetTopReply(ctx, params.Oid, params.OTyp, model.SubAttrUpperTop)
if err != nil {
return nil, err
}
if upperTopReply != nil {
if !upperTopReply.IsNormal() && sub.Mid != params.Mid {
upperTopReply = nil
} else {
parentIDs = append(parentIDs, upperTopReply.RpID)
}
}
}
parentChildrenIDRelation, err := s.ParentChildrenReplyIDRelation(ctx, sub, parentIDs)
if err != nil {
return nil, err
}
idReplyMap, err := s.IDReplyMap(ctx, sub, parentChildrenIDRelation)
if err != nil {
return nil, err
}
rootIDReplyMap := assemble(idReplyMap, parentChildrenIDRelation)
if adminTopReply != nil {
if r, ok := rootIDReplyMap[adminTopReply.RpID]; ok {
adminTopReply = r
}
// For historic reasons, TopReply and HotReply may be overlapped
hotIDs = Remove(hotIDs, adminTopReply.RpID)
}
if upperTopReply != nil {
if r, ok := rootIDReplyMap[upperTopReply.RpID]; ok {
upperTopReply = r
}
// For historic reasons, TopReply and HotReply may be overlapped
hotIDs = Remove(hotIDs, upperTopReply.RpID)
}
return &model.RootReplyListHeader{
TopAdmin: adminTopReply,
TopUpper: upperTopReply,
Hots: filterHot(Fetch(rootIDReplyMap, hotIDs), params.HotSize),
}, nil
}
func filterHot(rs []*model.Reply, maxSize int) (hots []*model.Reply) {
for _, r := range rs {
if r.Like >= 3 {
hots = append(hots, r)
}
}
if hots == nil {
hots = _emptyReplies
} else if len(hots) > maxSize {
hots = hots[:maxSize]
}
return hots
}
func needHeader(cursor *model.Cursor, rootLen int) bool {
return cursor.Latest() ||
(cursor.Increase() && rootLen < int(cursor.Len()))
}
// NeedInsertPendingReply NeedInsertPendingReply
func NeedInsertPendingReply(params *model.CursorParams, sub *model.Subject) bool {
return params.Mid > 0 &&
params.Sort == model.SortByFloor &&
sub.AttrVal(model.SubAttrAudit) == model.AttrYes
}
func collect(r *model.Reply, allIDs []int64, allMIDs []int64,
allReply []*model.Reply) ([]int64, []int64, []*model.Reply) {
if r == nil {
return nil, nil, nil
}
allIDs = append(allIDs, r.RpID)
allReply = append(allReply, r)
allMIDs = append(allMIDs, r.Mid)
if r.Content != nil {
for _, mid := range r.Content.Ats {
allMIDs = append(allMIDs, mid)
}
}
return allIDs, allMIDs, allReply
}
// IDReplyMap IDReplyMap
func (s *Service) IDReplyMap(ctx context.Context, sub *model.Subject,
parentChildrenIDRelation map[int64][]int64) (map[int64]*model.Reply, error) {
var allIDs []int64
for parentID, childrenIDs := range parentChildrenIDRelation {
allIDs = append(allIDs, childrenIDs...)
allIDs = append(allIDs, parentID)
}
// WARNING: GetReplyByIDs should not contains subReplies, but currently there
// exists a bug, which makes `idReplyMap` may contains sub_reply
idReplyMap, err := s.GetReplyByIDs(ctx, sub.Oid, sub.Type, allIDs)
if err != nil {
return nil, err
}
// temporary solution :(, remove all children replies
for _, reply := range idReplyMap {
if reply.Replies != nil {
reply.Replies = reply.Replies[:0]
}
}
return idReplyMap, nil
}
// RootReplyListByCursor RootReplyListByCursor
func (s *Service) RootReplyListByCursor(ctx context.Context, sub *model.Subject, params *model.CursorParams) ([]*model.Reply, error) {
var parentIDs []int64
if params.Cursor.Latest() {
// 忽略错误,这个请求只为了增加统计数据
s.replyFeed(ctx, params.Mid, 1, 20)
} else {
s.replyFeed(ctx, params.Mid, 2, 20)
}
rootIDs, err := s.GetRootReplyIDsByCursor(ctx, sub, params.Sort, params.Cursor)
if err != nil {
return nil, err
}
// 老版本折叠评论的逻辑
if params.ShowFolded && sub.HasFolded() {
foldedrpIDs, _ := s.foldedRepliesCursor(ctx, sub, 0, params.Cursor)
if len(foldedrpIDs) > 0 {
rootIDs = append(rootIDs, foldedrpIDs...)
sort.Slice(rootIDs, func(x, y int) bool { return rootIDs[x] > rootIDs[y] })
length := len(rootIDs)
if length > params.Cursor.Len() {
if params.Cursor.Increase() {
// 对于根评论列表,往楼层大的方向翻页是向上翻,需要从后往前截断
rootIDs = rootIDs[length-params.Cursor.Len():]
} else {
rootIDs = rootIDs[:params.Cursor.Len()]
}
}
}
}
parentIDs = append(parentIDs, rootIDs...)
parentChildrenIDRelation, err := s.ParentChildrenReplyIDRelation(ctx, sub, parentIDs)
if err != nil {
return nil, err
}
idReplyMap, err := s.IDReplyMap(ctx, sub, parentChildrenIDRelation)
if err != nil {
return nil, err
}
if NeedInsertPendingReply(params, sub) {
// WARNING: here we assume that pending replies have no children
// otherwise, we need to change logic here
pendingIDReplyMap, err := s.GetPendingReply(ctx, params.Mid, sub.Oid, sub.Type)
if err != nil {
return nil, err
}
for id, r := range pendingIDReplyMap {
if r.IsRoot() && !r.IsTop() {
// insert pending reply into root reply list
if params.Cursor.Latest() &&
((len(rootIDs) > 0 && id > rootIDs[0]) || len(rootIDs) == 0) {
// when fetch latest reply list, and root reply list's length < the default size
// and the pending reply ID > the max rootID
// then just append the pending reply
rootIDs = append([]int64{id}, rootIDs...)
if len(rootIDs) > int(params.Cursor.Len()) {
rootIDs = rootIDs[:params.Cursor.Len()]
}
} else {
// otherwise, we need an algorithm to insert pending replyID into
// rootIDs
rootIDs = InsertInto(rootIDs, id, int(params.Cursor.Len()), model.OrderDESC)
}
parentChildrenIDRelation[id] = []int64{}
} else if _, ok := idReplyMap[r.Root]; ok {
// insert pending reply into sub reply list
parentChildrenIDRelation[r.Root] = InsertInto(parentChildrenIDRelation[r.Root], id, defaultChildrenSize, model.OrderASC)
} else {
continue
}
sub.ACount++
idReplyMap[id] = r
}
}
return Fetch(assemble(idReplyMap, parentChildrenIDRelation), rootIDs), nil
}
// Remove Remove
func Remove(arr []int64, k int64) []int64 {
b := arr[:0]
for _, a := range arr {
if a != k {
b = append(b, a)
}
}
return b
}
// Unique Unique
func Unique(arr []int64) []int64 {
m := make(map[int64]struct{})
for _, a := range arr {
m[a] = struct{}{}
}
res := make([]int64, 0)
for a := range m {
res = append(res, a)
}
return res
}
// GetTopReply GetTopReply
func (s *Service) GetTopReply(ctx context.Context, oid int64, otyp int8, topType uint32) (*model.Reply, error) {
r, err := s.dao.Mc.GetTop(ctx, oid, otyp, topType)
if err != nil {
return nil, err
}
if r == nil {
s.dao.Databus.AddTop(ctx, oid, otyp, topType)
return nil, nil
}
return r, nil
}
// GetReplyFromDBByIDs GetReplyFromDBByIDs
func (s *Service) GetReplyFromDBByIDs(ctx context.Context, oid int64, otyp int8, ids []int64) ([]*model.Reply, error) {
rs := make([]*model.Reply, 0)
if len(ids) == 0 {
return rs, nil
}
idReplyMap, err := s.dao.Reply.GetByIds(ctx, oid, otyp, ids)
if err != nil {
return nil, err
}
idReplyContentMap, err := s.dao.Content.GetByIds(ctx, oid, ids)
if err != nil {
return nil, err
}
for _, id := range ids {
if r, ok := idReplyMap[id]; ok {
if r == nil {
rs = append(rs, nil)
continue
}
if content, ok := idReplyContentMap[id]; ok {
r.Content = content
}
rs = append(rs, r)
}
}
return rs, nil
}
// GetReplyByIDs GetReplyByIDs
func (s *Service) GetReplyByIDs(ctx context.Context, oid int64, otyp int8, ids []int64) (map[int64]*model.Reply, error) {
res := make(map[int64]*model.Reply)
if len(ids) == 0 {
return res, nil
}
cachedReplies, missedIDs, err := s.dao.Mc.GetReplyByIDs(ctx, ids)
var rs []*model.Reply
if err != nil {
rs, err = s.GetReplyFromDBByIDs(ctx, oid, otyp, ids)
if err != nil {
return nil, err
}
for _, r := range rs {
res[r.RpID] = r
}
return res, nil
}
for _, r := range cachedReplies {
res[r.RpID] = r
}
if len(missedIDs) == 0 {
return res, nil
}
missedReplies, err := s.GetReplyFromDBByIDs(ctx, oid, otyp, missedIDs)
if err != nil {
return nil, err
}
select {
case s.replyChan <- replyChan{rps: missedReplies}:
default:
log.Error("s.replyChan is full")
}
for _, r := range missedReplies {
res[r.RpID] = r.Clone()
}
return res, nil
}
// GetChildrenIDsByCursor GetChildrenIDsByCursor
func (s *Service) GetChildrenIDsByCursor(ctx context.Context, sub *model.Subject, rootID int64, sort int8, cursor *model.Cursor) ([]int64, error) {
k := reply.GenNewChildrenKeyByRootReplyID(rootID)
cacheExist, err := s.dao.Redis.ExpireCache(ctx, k)
if err != nil {
return nil, err
}
var ids []int64
if cacheExist {
ids, err = s.dao.Redis.RangeChildrenIDByCursorScore(ctx, k, cursor)
if err != nil {
return nil, err
}
return ids, nil
}
s.dao.Databus.RecoverIndexByRoot(ctx, sub.Oid, rootID, sub.Type)
switch sort {
case model.SortByFloor:
ids, err = s.dao.Reply.ChildrenIDSortByFloorCursor(ctx, sub.Oid, sub.Type, rootID, cursor)
default:
return nil, ecode.RequestErr
}
if err != nil {
return nil, err
}
return ids, nil
}
// GetRootReplyIDsByCursor GetRootReplyIDsByCursor
func (s *Service) GetRootReplyIDsByCursor(ctx context.Context, sub *model.Subject, sort int8, cursor *model.Cursor) ([]int64, error) {
var (
ids []int64
isEnd bool
)
if sub.RCount == 0 {
return []int64{}, nil
}
k := s.dao.Redis.CacheKeyRootReplyIDs(sub.Oid, sub.Type, sort)
cacheExist, err := s.dao.Redis.ExpireCache(ctx, k)
if err != nil {
return nil, err
}
minFloor := cursor.Current() - 20
if cursor.Latest() {
minFloor = int64(sub.Count) - 20
}
if minFloor <= 0 {
minFloor = 1
}
if cacheExist {
ids, isEnd, err = s.dao.Redis.RangeRootIDByCursorScore(ctx, k, cursor)
if err != nil {
return nil, err
}
if sort == model.SortByFloor && len(ids) < cursor.Len() && !cursor.Increase() && !isEnd {
ids, err = s.dao.Reply.RootIDSortByFloorCursor(ctx, sub.Oid, sub.Type, cursor)
if err != nil {
return nil, err
}
s.dao.Databus.RecoverFloorIdx(ctx, sub.Oid, sub.Type, int(minFloor), true)
}
return ids, nil
}
switch sort {
case model.SortByFloor:
s.dao.Databus.RecoverFloorIdx(ctx, sub.Oid, sub.Type, int(minFloor), true)
ids, err = s.dao.Reply.RootIDSortByFloorCursor(ctx, sub.Oid, sub.Type, cursor)
default:
return nil, ecode.RequestErr
}
if err != nil {
return nil, err
}
return ids, nil
}
// GetRootReplyIDs GetRootReplyIDs
func (s *Service) GetRootReplyIDs(ctx context.Context, oid int64, otyp int8, sort int8, offset, limit int64) ([]int64, error) {
var ids []int64
k := s.dao.Redis.CacheKeyRootReplyIDs(oid, otyp, sort)
cacheExist, err := s.dao.Redis.ExpireCache(ctx, k)
if err != nil {
return nil, err
}
if cacheExist {
ids, err = s.dao.Redis.RangeRootReplyIDs(ctx, k, int(offset), int(offset+limit-1))
if err != nil {
return nil, err
}
return ids, nil
}
s.dao.Databus.RecoverIndex(ctx, oid, otyp, sort)
switch sort {
case model.SortByFloor:
ids, err = s.dao.Reply.GetIdsSortFloor(ctx, oid, otyp, int(offset), int(limit))
case model.SortByCount:
ids, err = s.dao.Reply.GetIdsSortCount(ctx, oid, otyp, int(offset), int(limit))
case model.SortByLike:
ids, err = s.dao.Reply.GetIdsSortLike(ctx, oid, otyp, int(offset), int(limit))
default:
log.Error("unsupported sort:%d", sort)
return nil, ecode.RequestErr
}
if err != nil {
return nil, err
}
return ids, nil
}
// GetSubject GetSubject
func (s *Service) GetSubject(ctx context.Context, oid int64, tp int8) (*model.Subject, error) {
subject, err := s.getSubject(ctx, oid, tp)
if err != nil {
return nil, err
}
if subject.State == model.SubStateForbid {
return nil, ecode.ReplyForbidReply
}
return subject, nil
}
func elementOf(k int64, arr []int64) bool {
for _, i := range arr {
if i == k {
return true
}
}
return false
}
// InsertInto insert `id` into sorted list(order by `cmp`) `ids`
// after insertion if the total length > `size`, truncate the extra element
func InsertInto(ids []int64, id int64, size int, cmp model.Comp) []int64 {
if elementOf(id, ids) {
return ids
}
if len(ids) < size {
return model.SortArr(append(ids, id), cmp)
}
ids = model.SortArr(ids, cmp)
if !withInRange(id, ids[0], ids[len(ids)-1]) {
return ids
}
for i := 0; i < len(ids); i++ {
if cmp(id, ids[i]) {
ids = append(ids[:i], append([]int64{id}, ids[i:]...)...)
break
}
}
return ids[:size]
}
func withInRange(i, begin, end int64) bool {
return (begin > i && end < i) || (begin < i && end > i)
}
// FillRootReplies FillRootReplies
func (s *Service) FillRootReplies(ctx context.Context,
rs []*model.Reply,
mid int64,
ip string,
htmlEscape bool,
sub *model.Subject) {
var (
allReply []*model.Reply
allIDs, allMIDs []int64
)
if mid > 0 {
allMIDs = append(allMIDs, mid)
}
for _, r := range rs {
allIDs, allMIDs, allReply = collect(r, allIDs, allMIDs, allReply)
for _, rr := range r.Replies {
allIDs, allMIDs, allReply = collect(rr, allIDs, allMIDs, allReply)
}
}
s.fillReplies(ctx, sub, allIDs, allReply, Unique(allMIDs), mid, ip, htmlEscape)
}
func (s *Service) fillReplies(ctx context.Context,
sub *model.Subject,
allReplyIDs []int64,
rs []*model.Reply,
mids []int64,
reqMid int64,
ip string,
htmlEscape bool) {
var (
actionMap map[int64]int8
blackedMap map[int64]bool
relationMap map[int64]*accmdl.RelationReply
assistMap map[int64]int
fansMap map[int64]*model.FansDetail
accountMap map[int64]*accmdl.Card
)
g := errgroup.WithContext(ctx)
if reqMid > 0 {
g.Go(func(ctx context.Context) error {
actionMap, _ = s.actions(ctx, reqMid, sub.Oid, allReplyIDs)
return nil
})
g.Go(func(ctx context.Context) error {
relationMap, _ = s.GetRelationMap(ctx, reqMid, mids, ip)
return nil
})
g.Go(func(ctx context.Context) error {
blackedMap, _ = s.GetBlacklistMap(ctx, reqMid, ip)
return nil
})
}
g.Go(func(ctx context.Context) error {
accountMap, _ = s.GetAccountInfoMap(ctx, mids, ip)
return nil
})
if !(s.IsWhiteAid(sub.Oid, sub.Type)) {
g.Go(func(ctx context.Context) error {
assistMap, _ = s.GetAssistMap(ctx, sub.Mid, ip)
return nil
})
g.Go(func(ctx context.Context) error {
fansMap, _ = s.GetFansMap(ctx, mids, sub.Mid, ip)
return nil
})
}
g.Wait()
for _, r := range rs {
s.fillReply(r,
htmlEscape,
accountMap,
actionMap,
fansMap,
blackedMap,
assistMap,
relationMap)
}
}
func (s *Service) fillReply(r *model.Reply,
escape bool,
accountMap map[int64]*accmdl.Card,
actionMap map[int64]int8,
fansMap map[int64]*model.FansDetail,
blackedMap map[int64]bool,
assistMap map[int64]int,
relationMap map[int64]*accmdl.RelationReply) {
if r == nil {
return
}
r.FillFolder()
r.FillStr(escape)
if r.Content != nil {
r.Content.FillAts(accountMap)
}
r.Action = actionMap[r.RpID]
r.Member = new(model.Member)
var (
ok bool
blacked bool
card *accmdl.Card
)
if card, ok = accountMap[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 blacked, ok = blackedMap[r.Mid]; ok && blacked {
r.State = model.ReplyStateBlacklist
}
if r.Replies == nil {
r.Replies = []*model.Reply{}
}
if _, ok = assistMap[r.Mid]; ok {
r.Assist = 1
}
if attetion, ok := relationMap[r.Mid]; ok {
if attetion.Following {
r.Member.Following = 1
}
}
if r.RCount < 0 {
r.RCount = 0
}
}
// Fetch Fetch
func Fetch(idReplyMap map[int64]*model.Reply, ids []int64) []*model.Reply {
res := make([]*model.Reply, 0, len(ids))
for _, pid := range ids {
if p, ok := idReplyMap[pid]; ok && p != nil {
res = append(res, p)
}
}
return res
}
// assemble insert children replies into their corresponding parents
func assemble(idReplyMap map[int64]*model.Reply, parentChildrenMap map[int64][]int64) map[int64]*model.Reply {
parentIDs := make([]int64, 0)
for pid := range parentChildrenMap {
parentIDs = append(parentIDs, pid)
}
res := make(map[int64]*model.Reply)
for _, pid := range parentIDs {
if p, ok := idReplyMap[pid]; ok {
if childrenIDs, ok := parentChildrenMap[pid]; ok {
for _, childID := range childrenIDs {
if r, ok := idReplyMap[childID]; ok {
p.Replies = append(p.Replies, r)
}
}
}
res[pid] = p
}
}
return res
}
// ParentChildrenReplyIDRelation ParentChildrenReplyIDRelation
func (s *Service) ParentChildrenReplyIDRelation(ctx context.Context, sub *model.Subject, parentIDs []int64) (map[int64][]int64, error) {
idReplyMap, err := s.GetReplyByIDs(ctx, sub.Oid, sub.Type, parentIDs)
if err != nil {
return nil, err
}
var parentWithChildren, parentWithoutChildren []int64
for id, reply := range idReplyMap {
if reply.RCount > 0 {
parentWithChildren = append(parentWithChildren, id)
} else {
parentWithoutChildren = append(parentWithoutChildren, id)
}
}
parentChildrenIDRelation, err := s.parentChildrenReplyIDRelation(ctx, sub.Oid, sub.Type, parentWithChildren)
if err != nil {
return nil, err
}
for _, pid := range parentWithoutChildren {
parentChildrenIDRelation[pid] = []int64{}
}
return parentChildrenIDRelation, nil
}
func (s *Service) parentChildrenReplyIDRelation(ctx context.Context, oid int64,
tp int8, parentIDs []int64) (map[int64][]int64, error) {
parentChildrenIDRelation, missedIDs, err := s.dao.Redis.ParentChildrenReplyIDMap(ctx, parentIDs, 0, 4)
if err != nil {
return nil, err
}
if len(missedIDs) > 0 {
for _, rootID := range missedIDs {
childrenIDs, err := s.dao.Reply.ChildrenIDsOfRootReply(ctx, oid, rootID, tp, 0, defaultChildrenSize)
if err != nil {
return nil, err
}
parentChildrenIDRelation[rootID] = childrenIDs
s.dao.Databus.RecoverIndexByRoot(ctx, oid, rootID, tp)
}
}
return parentChildrenIDRelation, nil
}
// GetAccountInfoMap fn
func (s *Service) GetAccountInfoMap(ctx context.Context, mids []int64, ip string) (map[int64]*accmdl.Card, error) {
if len(mids) == 0 {
return _emptyCards, nil
}
args := &accmdl.MidsReq{Mids: mids}
res, err := s.acc.Cards3(ctx, args)
if err != nil {
log.Error("s.acc.Infos2(%v), error(%v)", args, err)
return nil, err
}
return res.Cards, nil
}
// GetFansMap fn
func (s *Service) GetFansMap(ctx context.Context, uids []int64, mid int64, ip string) (map[int64]*model.FansDetail, error) {
fans, err := s.fans.Fetch(ctx, uids, mid, time.Now())
if err != nil {
return nil, err
}
return fans, nil
}
// GetAssistMap fn
func (s *Service) GetAssistMap(ctx context.Context, mid int64, ip string) (assistMap map[int64]int, err error) {
arg := &assmdl.ArgAssists{
Mid: mid,
RealIP: ip,
}
assistMap = make(map[int64]int)
ids, err := s.assist.AssistIDs(ctx, arg)
if err != nil {
log.Error("s.assist.AssistIDs(%v), error(%v)", arg, err)
return
}
for _, id := range ids {
assistMap[id] = 1
}
return
}
// GetRelationMap GetRelationMap
func (s *Service) GetRelationMap(ctx context.Context, mid int64, targetMids []int64, ip string) (map[int64]*accmdl.RelationReply, error) {
if len(targetMids) == 0 {
return _emptyRelations, nil
}
relations, err := s.acc.Relations3(ctx, &accmdl.RelationsReq{Mid: mid, Owners: targetMids, RealIp: ip})
if err != nil {
log.Error("s.acc.Relations2(%v, %v) error(%v)", mid, targetMids, err)
return nil, err
}
return relations.Relations, nil
}
// GetBlacklistMap GetBlacklistMap
func (s *Service) GetBlacklistMap(ctx context.Context,
mid int64, ip string) (map[int64]bool, error) {
if mid == 0 {
return _emptyBlackList, nil
}
args := &accmdl.MidReq{Mid: mid}
blacklistMap, err := s.acc.Blacks3(ctx, args)
if err != nil {
log.Error("s.acc.Blacks(%v) error(%v)", args, err)
return nil, err
}
return blacklistMap.BlackList, nil
}
// GetPendingReply GetPendingReply
func (s *Service) GetPendingReply(ctx context.Context, mid int64, oid int64, typ int8) (map[int64]*model.Reply, error) {
// WARNING: here we assume that pending replies have no children
// otherwise, we need to change logic here
pendingIDs, err := s.dao.Redis.UserAuditReplies(ctx, mid, oid, typ)
if err != nil {
return nil, err
}
pendingIDReplyMap, err := s.GetReplyByIDs(ctx, oid, typ, pendingIDs)
if err != nil {
return nil, err
}
return pendingIDReplyMap, nil
}
// GetSubReplyListByCursor GetSubReplyListByCursor
func (s *Service) GetSubReplyListByCursor(ctx context.Context, params *model.CursorParams) (*model.RootReplyList, error) {
var (
hasFolded bool
)
sub, err := s.Subject(ctx, params.Oid, params.OTyp)
if err != nil {
return nil, err
}
rp, err := s.ReplyContent(ctx, params.Oid, params.RootID, params.OTyp)
if err != nil {
return nil, err
}
if rp.IsRoot() && rp.HasFolded() {
hasFolded = true
}
if rp.Root != 0 {
params.RootID = rp.Root
root, _ := s.reply(ctx, 0, params.Oid, rp.Root, params.OTyp)
if root != nil && rp.IsRoot() && rp.HasFolded() {
hasFolded = true
}
}
childrenIDs, err := s.GetChildrenIDsByCursor(ctx, sub, params.RootID, params.Sort, params.Cursor)
if err != nil {
return nil, err
}
// 这里是处理被折叠的评论的逻辑
if params.ShowFolded && hasFolded {
foldedRpIDs, _ := s.foldedRepliesCursor(ctx, sub, params.RootID, params.Cursor)
if len(foldedRpIDs) > 0 {
childrenIDs = append(childrenIDs, foldedRpIDs...)
sort.Slice(childrenIDs, func(x, y int) bool { return childrenIDs[x] < childrenIDs[y] })
length := len(childrenIDs)
if length > params.Cursor.Len() {
if params.Cursor.Descrease() {
// 往楼层小的地方翻页, 对于子评论就是往上翻页,这个时候要从后往前截断
childrenIDs = childrenIDs[length-params.Cursor.Len():]
} else {
childrenIDs = childrenIDs[:params.Cursor.Len()]
}
}
}
}
parentChildrenIDRelation := map[int64][]int64{params.RootID: childrenIDs}
idReplyMap, err := s.IDReplyMap(ctx, sub, parentChildrenIDRelation)
if err != nil {
return nil, err
}
if NeedInsertPendingReply(params, sub) {
var pendingIDReplyMap map[int64]*model.Reply
pendingIDReplyMap, err = s.GetPendingReply(ctx, params.Mid, sub.Oid, sub.Type)
if err != nil {
return nil, err
}
for id, r := range pendingIDReplyMap {
if _, ok := idReplyMap[r.Root]; ok {
parentChildrenIDRelation[r.Root] = InsertInto(parentChildrenIDRelation[r.Root], id, defaultChildrenSize, model.OrderASC)
sub.ACount++
idReplyMap[id] = r
}
}
}
rootReply := assemble(idReplyMap, parentChildrenIDRelation)[params.RootID]
if rootReply == nil || rootReply.IsDeleted() {
return nil, ecode.ReplyNotExist
}
max, min, err := cursorRange(rootReply.Replies, params.Sort)
if err != nil {
return nil, err
}
s.FillRootReplies(ctx, []*model.Reply{rootReply}, params.Mid, params.IP, params.HTMLEscape, sub)
return &model.RootReplyList{
Subject: sub,
Roots: []*model.Reply{rootReply},
CursorRangeMax: max,
CursorRangeMin: min,
}, nil
}
func cursorRange(rs []*model.Reply, sort int8) (max, min int64, err error) {
if len(rs) > 0 {
switch sort {
case model.SortByFloor:
// NOTE("这里是为了12月13号给bishi搞零时置顶子评论做的ios兼容逻辑")
var head int64
if rs[0].RpID != 1237270231 {
head = int64(rs[0].Floor)
} else {
if len(rs) > 1 {
head = int64(rs[1].Floor)
} else {
head = int64(1)
}
}
tail := int64(rs[len(rs)-1].Floor)
if model.OrderDESC(head, tail) {
max, min = head, tail
} else {
max, min = tail, head
}
return
default:
err = errors.New("unsupported cursor type")
log.Error("%v", err)
return 0, 0, err
}
}
return
}
// GetRootReplyListByCursor GetRootReplyListByCursor
func (s *Service) GetRootReplyListByCursor(ctx context.Context, params *model.CursorParams) (*model.RootReplyList, error) {
params.HotSize = s.hotNum(params.Oid, params.OTyp)
sub, err := s.Subject(ctx, params.Oid, params.OTyp)
if err != nil {
return nil, err
}
roots, err := s.RootReplyListByCursor(ctx, sub, params)
if err != nil {
return nil, err
}
max, min, err := cursorRange(roots, params.Sort)
if err != nil {
return nil, err
}
// WARN: rootIDs AND hotIDs may be overlapped
var allRootReply []*model.Reply
allRootReply = append(allRootReply, roots...)
var header *model.RootReplyListHeader
if needHeader(params.Cursor, len(roots)) {
header, err = s.GetRootReplyListHeader(ctx, sub, params)
if err != nil {
return nil, err
}
allRootReply = append(allRootReply, header.Hots...)
if header.TopAdmin != nil {
allRootReply = append(allRootReply, header.TopAdmin)
}
if header.TopUpper != nil {
allRootReply = append(allRootReply, header.TopUpper)
}
}
s.FillRootReplies(ctx, allRootReply, params.Mid, params.IP, params.HTMLEscape, sub)
return &model.RootReplyList{
Subject: sub,
Roots: roots,
Header: header,
CursorRangeMax: max,
CursorRangeMin: min,
}, nil
}
// DialogMaxMinFloor return max and min floor in dialog
func (s *Service) DialogMaxMinFloor(c context.Context, oid int64, tp int8, root, dialog int64) (maxFloor, minFloor int, err error) {
var (
ok bool
)
if ok, err = s.dao.Redis.ExpireDialogIndex(c, dialog); err != nil {
log.Error("s.dao.Redis.ExpireDialogIndex error (%v)", err)
return
}
if ok {
minFloor, maxFloor, err = s.dao.Redis.DialogMinMaxFloor(c, dialog)
} else {
minFloor, maxFloor, err = s.dao.Reply.GetDialogMinMaxFloor(c, oid, tp, root, dialog)
}
return
}
// DialogByCursor ...
func (s *Service) DialogByCursor(c context.Context, mid, oid int64, tp int8, root, dialog int64, cursor *model.Cursor) (rps []*model.Reply, dialogCursor *model.DialogCursor, dialogMeta *model.DialogMeta, err error) {
var (
ok bool
rpIDs []int64
rpMap map[int64]*model.Reply
)
dialogCursor = new(model.DialogCursor)
dialogMeta = new(model.DialogMeta)
dialogMeta.MaxFloor, dialogMeta.MinFloor, err = s.DialogMaxMinFloor(c, oid, tp, root, dialog)
if err != nil {
log.Error("get max and min floor for dialog from redis or db error", err)
return
}
if (cursor.Max() != 0 && cursor.Max() > int64(dialogMeta.MaxFloor)) || (cursor.Min() != 0 && cursor.Min() < int64(dialogMeta.MinFloor)) {
log.Warn("cursor max %d min %d, dialogmeta max %d min %d", cursor.Max(), cursor.Min(), dialogMeta.MinFloor, dialogMeta.MinFloor)
err = ecode.RequestErr
return
}
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.DialogByCursor(c, dialog, cursor)
} else {
s.dao.Databus.RecoverDialogIdx(c, oid, tp, root, dialog)
if cursor.Latest() {
rpIDs, err = s.dao.Reply.GetIDsByDialogAsc(c, oid, tp, root, dialog, int64(dialogMeta.MinFloor), cursor.Len())
} else if cursor.Descrease() {
rpIDs, err = s.dao.Reply.GetIDsByDialogDesc(c, oid, tp, root, dialog, cursor.Current(), cursor.Len())
} else if cursor.Increase() {
rpIDs, err = s.dao.Reply.GetIDsByDialogAsc(c, oid, tp, root, dialog, cursor.Current(), cursor.Len())
} else {
err = ecode.RequestErr
}
}
if err != nil {
log.Error("dialog by cursor 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)
}
}
if !sort.SliceIsSorted(rps, func(i, j int) bool { return rps[i].Floor < rps[j].Floor }) {
sort.Slice(rps, func(i, j int) bool { return rps[i].Floor < rps[j].Floor })
}
sub, err := s.Subject(c, oid, tp)
if err != nil {
log.Error("s.dao.Subject.Get(%d, %d) error(%v)", oid, tp, err)
return
}
if err = s.buildReply(c, sub, rps, mid, false); err != nil {
return
}
dialogCursor.Size = len(rps)
if dialogCursor.Size == 0 {
return
}
dialogCursor.MinFloor = rps[0].Floor
dialogCursor.MaxFloor = rps[dialogCursor.Size-1].Floor
return
}
// ...
func (s *Service) foldedRepliesCursor(c context.Context, sub *model.Subject, root int64, cursor *model.Cursor) (foldedRpIDs []int64, err error) {
var (
xcursor = new(xmodel.Cursor)
max = int(cursor.Max())
min = int(cursor.Min())
)
xcursor.Ps = cursor.Len()
// 针对子评论的情况
if cursor.Increase() {
xcursor.Prev = min
} else if cursor.Descrease() {
xcursor.Next = max
}
return s.foldedReplies(c, sub, root, xcursor)
}