You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1107 lines
31 KiB
1107 lines
31 KiB
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) |
|
}
|
|
|