移除 pub/dataReport、reportser、srv_report、上报定时任务及 eventmod 事件表, 以及注册/登录/点赞/收藏/评论/解锁/订单/上下架等处的上报调用和 reportSdkConfig 配置下发。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
659 lines
22 KiB
Go
659 lines
22 KiB
Go
package commentser
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
"videoweb/app/api/proto"
|
|
"videoweb/app/service/configser"
|
|
"videoweb/pub/common/enum/redisconst"
|
|
"videoweb/pub/common/log"
|
|
"videoweb/pub/common/netutil"
|
|
"videoweb/pub/common/srvdef"
|
|
"videoweb/pub/models/commentlikemod"
|
|
"videoweb/pub/models/commentmod"
|
|
"videoweb/pub/models/playletchaptermod"
|
|
"videoweb/pub/models/playletmod"
|
|
"videoweb/pub/models/usermod"
|
|
"videoweb/pub/mongos"
|
|
"videoweb/pub/redis"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
)
|
|
|
|
const (
|
|
ObjectTypePlaylet int64 = 10 // 短剧评论
|
|
ObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
|
|
commentLevelFirst int64 = 1 // 一级评论
|
|
commentLevelSecond int64 = 2 // 二级评论
|
|
commentReviewPending int = 0 // 待审核
|
|
commentReviewPass int = 1 // 审核通过
|
|
commentMaxTextLen int = 200 // 评论最大字数
|
|
commentDefaultPageSize int64 = 20 // 默认分页条数
|
|
commentMaxPageSize int64 = 100 // 最大分页条数
|
|
commentSortHot int64 = 1 // 热门
|
|
commentSortNewest int64 = 2 // 最新
|
|
commentPlayletHeatInc int64 = 3 // 发布评论增加的短剧热度值
|
|
operateStatusYes int64 = 1 // 已操作
|
|
operateStatusNo int64 = 2 // 未操作
|
|
)
|
|
|
|
var commentListProjection = bson.D{
|
|
{Key: commentmod.FN.Comment.Id, Value: 1},
|
|
{Key: commentmod.FN.Comment.ObjectId, Value: 1},
|
|
{Key: commentmod.FN.Comment.ObjectType, Value: 1},
|
|
{Key: commentmod.FN.Comment.UserId, Value: 1},
|
|
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
|
{Key: commentmod.FN.Comment.UserAvatar, Value: 1},
|
|
{Key: commentmod.FN.Comment.Text, Value: 1},
|
|
{Key: commentmod.FN.Comment.Level, Value: 1},
|
|
{Key: commentmod.FN.Comment.ParentsId, Value: 1},
|
|
{Key: commentmod.FN.Comment.ReplyCommentId, Value: 1},
|
|
{Key: commentmod.FN.Comment.ReplyUserName, Value: 1},
|
|
{Key: commentmod.FN.Comment.CommentCount, Value: 1},
|
|
{Key: commentmod.FN.Comment.CountLike, Value: 1},
|
|
{Key: commentmod.FN.Comment.CreatedAt, Value: 1},
|
|
}
|
|
|
|
// SendComment 发布评论
|
|
func SendComment(ctx context.Context, userId int64, ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
|
|
// 评论入口先做频控和基础参数校验,再校验短剧/剧集是否可评论,避免无效请求进入写库链路。
|
|
if userId <= 0 {
|
|
return srvdef.New(srvdef.InvalidToken, "请先登录", "")
|
|
}
|
|
if err := checkCommentLimit(ip, req); err != nil {
|
|
return err
|
|
}
|
|
text, err := normalizeCommentText(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectName, playletId, err := validateCommentTarget(ctx, req.ObjectType, req.ObjectId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
level := commentLevelFirst
|
|
parentsId := int64(0)
|
|
replyCommentId := int64(0)
|
|
replyUserName := ""
|
|
if req.ParentsId > 0 {
|
|
replyComment, err := findReplyTargetComment(ctx, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
level = commentLevelSecond
|
|
parentsId = req.ParentsId
|
|
replyCommentId = replyComment.Id
|
|
replyUserName = replyComment.UserName
|
|
}
|
|
user, err := findCommentUser(ctx, userId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
info := commentmod.CommentModel{
|
|
Id: mongos.Lid.Comment.Get(),
|
|
Ip: ip,
|
|
ObjectId: req.ObjectId,
|
|
ObjectName: objectName,
|
|
ObjectType: req.ObjectType,
|
|
UserId: user.Id,
|
|
UserName: commentUserName(user),
|
|
UserAvatar: user.Avatar,
|
|
Level: level,
|
|
ParentsId: parentsId,
|
|
ReplyCommentId: replyCommentId,
|
|
ReplyUserName: replyUserName,
|
|
Text: text,
|
|
CommentCount: 0,
|
|
CountLike: 0,
|
|
ReviewStatus: commentReviewPending,
|
|
UpdatedAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
if err := commentmod.InsertOneComment(ctx, &info); err != nil {
|
|
log.Error("插入短剧评论失败", log.Any("评论信息", info), log.E(err))
|
|
return err
|
|
}
|
|
if err := incrementCommentPlayletHeatValue(ctx, playletId, commentPlayletHeatInc); err != nil {
|
|
log.Error("评论发布后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("评论信息", info), log.E(err))
|
|
return err
|
|
}
|
|
// app端发布评论先进入待审核状态,不立即增加短剧/剧集评论数;后台审核通过时再统一同步 countComment。
|
|
return nil
|
|
}
|
|
|
|
func findCommentUser(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
|
user, err := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
|
if err != nil {
|
|
log.Error("查询评论用户失败", log.Any("用户ID", userId), log.E(err))
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func commentUserName(user *usermod.User) string {
|
|
if user == nil {
|
|
return ""
|
|
}
|
|
if name := strings.TrimSpace(user.NickName); name != "" {
|
|
return name
|
|
}
|
|
if name := strings.TrimSpace(user.AccountNo); name != "" {
|
|
return name
|
|
}
|
|
return "用户" + strconv.FormatInt(user.Id, 10)
|
|
}
|
|
|
|
// GetCommentPage 分页查询评论列表
|
|
func GetCommentPage(ctx context.Context, userId int64, req *proto.GetCommentListReq) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
|
// 列表只查一级且已审核评论,并通过一次批量查询补齐当前用户点赞状态,避免列表 N+1 查询。
|
|
if req == nil || req.ObjectId <= 0 || !isPlayletCommentType(req.ObjectType) {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
|
}
|
|
pageNum, pageSize := normalizeCommentPage(req.PageNum, req.PageSize)
|
|
return getCommentPageData(ctx, userId, req.ObjectId, req.ObjectType, req.SortOrder, pageNum, pageSize)
|
|
}
|
|
|
|
// GetCommentList 兼容旧详情接口的评论查询
|
|
func GetCommentList(ctx context.Context, objectId, objectType, page int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
|
pageNum, _ := normalizeCommentPage(page, commentDefaultPageSize)
|
|
return getCommentPageData(ctx, 0, objectId, objectType, commentSortNewest, pageNum, commentDefaultPageSize)
|
|
}
|
|
|
|
// SetCommentLike 设置当前用户评论点赞状态
|
|
func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
|
|
// 使用评论点赞关系表做去重;只有关系真实变化时才更新 _comment.countLike,重复点赞/取消不会重复增减计数。
|
|
if userId <= 0 {
|
|
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "")
|
|
}
|
|
if req == nil || req.CommentId <= 0 || req.IsLike == nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
|
}
|
|
comment, err := findLikeTargetComment(ctx, req.CommentId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter := bson.M{
|
|
commentlikemod.FN.CommentLike.UserId: userId,
|
|
commentlikemod.FN.CommentLike.CommentId: req.CommentId,
|
|
}
|
|
changed := false
|
|
if *req.IsLike {
|
|
inserted, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
|
|
UserId: userId,
|
|
CommentId: req.CommentId,
|
|
ObjectId: comment.ObjectId,
|
|
ObjectType: comment.ObjectType,
|
|
})
|
|
if err != nil {
|
|
log.Error("新增评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
|
|
return nil, err
|
|
}
|
|
changed = inserted
|
|
} else {
|
|
deleted, err := commentlikemod.DeleteOneCommentLike(ctx, filter)
|
|
if err != nil {
|
|
log.Error("删除评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
|
|
return nil, err
|
|
}
|
|
changed = deleted > 0
|
|
}
|
|
if changed {
|
|
delta := int64(1)
|
|
if !*req.IsLike {
|
|
delta = -1
|
|
}
|
|
if err := incrementCommentLikeCount(ctx, req.CommentId, delta); err != nil {
|
|
rollbackCommentLike(ctx, userId, comment, *req.IsLike)
|
|
return nil, err
|
|
}
|
|
}
|
|
return &proto.CommentLikeSetResp{
|
|
CommentId: req.CommentId,
|
|
IsLike: operateStatusByFlag(*req.IsLike),
|
|
CountLike: applyLocalCounter(comment.CountLike, *req.IsLike, changed),
|
|
}, nil
|
|
}
|
|
|
|
func checkCommentLimit(ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
|
|
curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
|
|
if err != nil {
|
|
log.Error("评论频率限制写入Redis失败", log.E(err), log.Any("IP", ip))
|
|
}
|
|
if curValue > 1 {
|
|
log.Error("评论操作过于频繁", log.Any("IP", ip), log.Any("请求参数", req))
|
|
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
|
}
|
|
dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
|
|
if err != nil {
|
|
log.Error("评论每日限制写入Redis失败", log.E(err), log.Any("IP", ip))
|
|
}
|
|
if dayCurValue > 50 {
|
|
log.Error("评论每日次数超限", log.Any("IP", ip), log.Any("请求参数", req))
|
|
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeCommentText(req *proto.CommentSendReq) (string, *srvdef.SrvErr) {
|
|
if req == nil || req.ObjectId <= 0 || !isPlayletCommentType(req.ObjectType) {
|
|
return "", srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
|
}
|
|
text := strings.TrimSpace(req.Text)
|
|
if text == "" {
|
|
return "", srvdef.New(srvdef.CommentTextIsEmpty, "评论内容不能为空", "")
|
|
}
|
|
if utf8.RuneCountInString(text) > commentMaxTextLen {
|
|
return "", srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字", "")
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func validateCommentTarget(ctx context.Context, objectType, objectId int64) (string, int64, *srvdef.SrvErr) {
|
|
switch objectType {
|
|
case ObjectTypePlaylet:
|
|
playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: objectId,
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("查询评论短剧目标失败", log.Any("短剧ID", objectId), log.E(err))
|
|
return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
|
|
}
|
|
return playlet.Title, playlet.Id, nil
|
|
case ObjectTypePlayletChapter:
|
|
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: objectId,
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
|
{Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
|
|
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("查询评论剧集目标失败", log.Any("剧集ID", objectId), log.E(err))
|
|
return "", 0, srvdef.New(srvdef.ErrParamError, "剧集不存在", "")
|
|
}
|
|
if _, err = playletmod.FindOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
|
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
|
})); err != nil {
|
|
log.Error("查询剧集所属短剧失败", log.Any("剧集ID", objectId), log.Any("短剧ID", chapter.PlayletId), log.E(err))
|
|
return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
|
|
}
|
|
if chapter.Title != "" {
|
|
return chapter.Title, chapter.PlayletId, nil
|
|
}
|
|
return "第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集", chapter.PlayletId, nil
|
|
default:
|
|
return "", 0, srvdef.New(srvdef.ErrParamError, "评论类型有误", "")
|
|
}
|
|
}
|
|
|
|
func findReplyTargetComment(ctx context.Context, req *proto.CommentSendReq) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
|
parentComment, err := commentmod.FindOneComment(ctx, bson.M{
|
|
commentmod.FN.Comment.Id: req.ParentsId,
|
|
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
|
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
|
commentmod.FN.Comment.Level: commentLevelFirst,
|
|
commentmod.FN.Comment.ParentsId: 0,
|
|
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: commentmod.FN.Comment.Id, Value: 1},
|
|
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("查询回复父评论失败", log.Any("父评论ID", req.ParentsId), log.Any("请求参数", req), log.E(err))
|
|
return nil, srvdef.New(srvdef.ErrParamError, "父评论不存在或不可回复", "")
|
|
}
|
|
replyCommentId := req.ReplyCommentId
|
|
if replyCommentId <= 0 {
|
|
replyCommentId = req.ParentsId
|
|
}
|
|
if replyCommentId == parentComment.Id {
|
|
return parentComment, nil
|
|
}
|
|
replyComment, err := commentmod.FindOneComment(ctx, bson.M{
|
|
commentmod.FN.Comment.Id: replyCommentId,
|
|
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
|
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
|
commentmod.FN.Comment.Level: commentLevelSecond,
|
|
commentmod.FN.Comment.ParentsId: req.ParentsId,
|
|
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: commentmod.FN.Comment.Id, Value: 1},
|
|
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("查询被回复评论失败", log.Any("被回复评论ID", replyCommentId), log.Any("请求参数", req), log.E(err))
|
|
return nil, srvdef.New(srvdef.ErrParamError, "被回复评论不存在或不可回复", "")
|
|
}
|
|
return replyComment, nil
|
|
}
|
|
|
|
func incrementTargetCommentCount(ctx context.Context, objectType, objectId int64, delta int64) *srvdef.SrvErr {
|
|
switch objectType {
|
|
case ObjectTypePlaylet:
|
|
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: objectId,
|
|
}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountComment: delta}}); err != nil {
|
|
log.Error("更新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("增量", delta), log.E(err))
|
|
return err
|
|
}
|
|
case ObjectTypePlayletChapter:
|
|
if err := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
|
|
playletchaptermod.FN.PlayletChapter.Id: objectId,
|
|
}, bson.M{"$inc": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: delta}}); err != nil {
|
|
log.Error("更新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("增量", delta), log.E(err))
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func incrementCommentPlayletHeatValue(ctx context.Context, playletId int64, delta int64) *srvdef.SrvErr {
|
|
if playletId <= 0 || delta == 0 {
|
|
return nil
|
|
}
|
|
return playletmod.UpdateOnePlaylet(ctx, bson.M{
|
|
playletmod.FN.Playlet.Id: playletId,
|
|
}, bson.M{
|
|
"$inc": bson.M{
|
|
playletmod.FN.Playlet.HeatValue: delta,
|
|
},
|
|
})
|
|
}
|
|
|
|
func getCommentPageData(ctx context.Context, userId int64, objectId, objectType, sortOrder, pageNum, pageSize int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
|
resp := &proto.GetCommentListResp{
|
|
ObjectId: objectId,
|
|
TotalCount: 0,
|
|
PageSize: int(pageSize),
|
|
PageNum: int(pageNum),
|
|
List: make([]proto.CommentInfo, 0),
|
|
}
|
|
if objectId <= 0 || !isPlayletCommentType(objectType) {
|
|
return resp, nil
|
|
}
|
|
filter := bson.M{
|
|
commentmod.FN.Comment.ObjectId: objectId,
|
|
commentmod.FN.Comment.ObjectType: objectType,
|
|
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
|
commentmod.FN.Comment.Level: commentLevelFirst,
|
|
commentmod.FN.Comment.ParentsId: 0,
|
|
}
|
|
total, err := commentmod.CountComments(ctx, filter)
|
|
if err != nil {
|
|
log.Warn("统计评论列表失败", log.Any("filter", filter), log.E(err))
|
|
return resp, err
|
|
}
|
|
resp.TotalCount = total
|
|
if total == 0 {
|
|
return resp, nil
|
|
}
|
|
skip := (pageNum - 1) * pageSize
|
|
if skip >= total {
|
|
return resp, nil
|
|
}
|
|
opts := options.Find().
|
|
SetProjection(commentListProjection).
|
|
SetSkip(skip).
|
|
SetLimit(pageSize).
|
|
SetSort(commentListSort(sortOrder))
|
|
list, err := commentmod.FindManyComment(ctx, filter, opts)
|
|
if err != nil {
|
|
log.Warn("查询评论列表失败", log.Any("filter", filter), log.E(err))
|
|
return resp, err
|
|
}
|
|
parentIds := commentIdsFromList(list)
|
|
children, err := findChildComments(ctx, parentIds)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
allComments := make(commentmod.CommentModelList, 0, len(list)+len(children))
|
|
allComments = append(allComments, list...)
|
|
allComments = append(allComments, children...)
|
|
likedCommentIds, err := findLikedCommentIdMap(ctx, userId, allComments)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
childrenByParent := make(map[int64][]proto.CommentInfo)
|
|
for _, item := range children {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
_, liked := likedCommentIds[item.Id]
|
|
childrenByParent[item.ParentsId] = append(childrenByParent[item.ParentsId], buildCommentInfo(ctx, item, liked))
|
|
}
|
|
resp.List = make([]proto.CommentInfo, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
_, liked := likedCommentIds[item.Id]
|
|
info := buildCommentInfo(ctx, item, liked)
|
|
info.Children = childrenByParent[item.Id]
|
|
resp.List = append(resp.List, info)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func commentListSort(sortOrder int64) bson.D {
|
|
switch sortOrder {
|
|
case commentSortHot:
|
|
return bson.D{
|
|
{Key: commentmod.FN.Comment.CountLike, Value: -1},
|
|
{Key: commentmod.FN.Comment.Id, Value: -1},
|
|
}
|
|
case commentSortNewest:
|
|
fallthrough
|
|
default:
|
|
return bson.D{
|
|
{Key: commentmod.FN.Comment.CreatedAt, Value: -1},
|
|
{Key: commentmod.FN.Comment.Id, Value: -1},
|
|
}
|
|
}
|
|
}
|
|
|
|
func commentIdsFromList(list commentmod.CommentModelList) []int64 {
|
|
commentIds := make([]int64, 0, len(list))
|
|
for _, item := range list {
|
|
if item != nil {
|
|
commentIds = append(commentIds, item.Id)
|
|
}
|
|
}
|
|
return commentIds
|
|
}
|
|
|
|
func findChildComments(ctx context.Context, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
|
|
if len(parentIds) == 0 {
|
|
return make(commentmod.CommentModelList, 0), nil
|
|
}
|
|
filter := bson.M{
|
|
commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
|
|
commentmod.FN.Comment.Level: commentLevelSecond,
|
|
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
|
}
|
|
opts := options.Find().
|
|
SetProjection(commentListProjection).
|
|
SetSort(bson.D{
|
|
{Key: commentmod.FN.Comment.ParentsId, Value: 1},
|
|
{Key: commentmod.FN.Comment.CreatedAt, Value: 1},
|
|
{Key: commentmod.FN.Comment.Id, Value: 1},
|
|
})
|
|
children, err := commentmod.FindManyComment(ctx, filter, opts)
|
|
if err != nil {
|
|
log.Warn("查询二级评论列表失败", log.Any("filter", filter), log.E(err))
|
|
return nil, err
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
func findLikedCommentIdMap(ctx context.Context, userId int64, list commentmod.CommentModelList) (map[int64]struct{}, *srvdef.SrvErr) {
|
|
likedMap := make(map[int64]struct{})
|
|
if userId <= 0 || len(list) == 0 {
|
|
return likedMap, nil
|
|
}
|
|
commentIds := commentIdsFromList(list)
|
|
if len(commentIds) == 0 {
|
|
return likedMap, nil
|
|
}
|
|
likes, err := commentlikemod.FindManyCommentLike(ctx, bson.M{
|
|
commentlikemod.FN.CommentLike.UserId: userId,
|
|
commentlikemod.FN.CommentLike.CommentId: bson.M{"$in": commentIds},
|
|
}, options.Find().SetProjection(bson.D{
|
|
{Key: commentlikemod.FN.CommentLike.CommentId, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("批量查询评论点赞状态失败", log.Any("用户ID", userId), log.E(err))
|
|
return nil, err
|
|
}
|
|
for _, item := range likes {
|
|
if item != nil {
|
|
likedMap[item.CommentId] = struct{}{}
|
|
}
|
|
}
|
|
return likedMap, nil
|
|
}
|
|
|
|
func buildCommentInfo(ctx context.Context, item *commentmod.CommentModel, liked bool) proto.CommentInfo {
|
|
return proto.CommentInfo{
|
|
Id: item.Id,
|
|
ObjectId: item.ObjectId,
|
|
ObjectType: item.ObjectType,
|
|
UserId: item.UserId,
|
|
UserName: item.UserName,
|
|
UserAvatar: withCommentAvatarDomain(ctx, item.UserAvatar),
|
|
Text: item.Text,
|
|
Level: item.Level,
|
|
ParentsId: item.ParentsId,
|
|
ReplyCommentId: item.ReplyCommentId,
|
|
ReplyUserName: item.ReplyUserName,
|
|
CommentCount: item.CommentCount,
|
|
CountLike: item.CountLike,
|
|
IsLike: operateStatusByFlag(liked),
|
|
CreatedAt: item.CreatedAt,
|
|
Children: make([]proto.CommentInfo, 0),
|
|
}
|
|
}
|
|
|
|
func withCommentAvatarDomain(ctx context.Context, path string) string {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
|
|
if imgCdn == "" {
|
|
return path
|
|
}
|
|
return netutil.MergeURL(imgCdn, path)
|
|
}
|
|
|
|
func findLikeTargetComment(ctx context.Context, commentId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
|
comment, err := commentmod.FindOneComment(ctx, bson.M{
|
|
commentmod.FN.Comment.Id: commentId,
|
|
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
|
commentmod.FN.Comment.ObjectType: bson.M{
|
|
"$in": []int64{ObjectTypePlaylet, ObjectTypePlayletChapter},
|
|
},
|
|
}, options.FindOne().SetProjection(bson.D{
|
|
{Key: commentmod.FN.Comment.Id, Value: 1},
|
|
{Key: commentmod.FN.Comment.ObjectId, Value: 1},
|
|
{Key: commentmod.FN.Comment.ObjectType, Value: 1},
|
|
{Key: commentmod.FN.Comment.CountLike, Value: 1},
|
|
}))
|
|
if err != nil {
|
|
log.Error("查询评论点赞目标失败", log.Any("评论ID", commentId), log.E(err))
|
|
return nil, srvdef.New(srvdef.ErrParamError, "评论不存在", "")
|
|
}
|
|
return comment, nil
|
|
}
|
|
|
|
func incrementCommentLikeCount(ctx context.Context, commentId int64, delta int64) *srvdef.SrvErr {
|
|
filter := bson.M{
|
|
commentmod.FN.Comment.Id: commentId,
|
|
}
|
|
if delta < 0 {
|
|
filter[commentmod.FN.Comment.CountLike] = bson.M{"$gt": 0}
|
|
}
|
|
err := commentmod.UpdateOneComment(ctx, filter, bson.M{
|
|
"$inc": bson.M{
|
|
commentmod.FN.Comment.CountLike: delta,
|
|
},
|
|
})
|
|
if err != nil {
|
|
if delta < 0 && err.Code == srvdef.ErrNoData {
|
|
log.Warn("评论点赞数已为0,跳过扣减", log.Any("评论ID", commentId))
|
|
return nil
|
|
}
|
|
log.Error("更新评论点赞数失败", log.Any("评论ID", commentId), log.Any("增量", delta), log.E(err))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rollbackCommentLike(ctx context.Context, userId int64, comment *commentmod.CommentModel, enabled bool) {
|
|
filter := bson.M{
|
|
commentlikemod.FN.CommentLike.UserId: userId,
|
|
commentlikemod.FN.CommentLike.CommentId: comment.Id,
|
|
}
|
|
if enabled {
|
|
if _, err := commentlikemod.DeleteOneCommentLike(ctx, filter); err != nil {
|
|
log.Error("回滚评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
|
|
}
|
|
return
|
|
}
|
|
if _, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
|
|
UserId: userId,
|
|
CommentId: comment.Id,
|
|
ObjectId: comment.ObjectId,
|
|
ObjectType: comment.ObjectType,
|
|
}); err != nil {
|
|
log.Error("恢复评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
|
|
}
|
|
}
|
|
|
|
func normalizeCommentPage(pageNum, pageSize int64) (int64, int64) {
|
|
if pageNum <= 0 {
|
|
pageNum = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = commentDefaultPageSize
|
|
}
|
|
if pageSize > commentMaxPageSize {
|
|
pageSize = commentMaxPageSize
|
|
}
|
|
return pageNum, pageSize
|
|
}
|
|
|
|
func isPlayletCommentType(objectType int64) bool {
|
|
return objectType == ObjectTypePlaylet || objectType == ObjectTypePlayletChapter
|
|
}
|
|
|
|
func applyLocalCounter(current int64, enabled bool, changed bool) int64 {
|
|
if !changed {
|
|
return current
|
|
}
|
|
if enabled {
|
|
return current + 1
|
|
}
|
|
if current <= 0 {
|
|
return 0
|
|
}
|
|
return current - 1
|
|
}
|
|
|
|
func operateStatusByFlag(enabled bool) int64 {
|
|
if enabled {
|
|
return operateStatusYes
|
|
}
|
|
return operateStatusNo
|
|
}
|