init: 初始化 web_hgdjb_server 项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,705 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/reportser"
|
||||
"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/models/videoinformationmod"
|
||||
"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 // 短剧剧集评论
|
||||
ObjectTypeVideo int64 = 20 // 视频评论
|
||||
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 isPlayletCommentType(req.ObjectType) {
|
||||
if err := incrementCommentPlayletHeatValue(ctx, playletId, commentPlayletHeatInc); err != nil {
|
||||
log.Error("评论发布后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("评论信息", info), log.E(err))
|
||||
return err
|
||||
}
|
||||
chapterId := int64(0)
|
||||
if req.ObjectType == ObjectTypePlayletChapter {
|
||||
chapterId = req.ObjectId
|
||||
}
|
||||
reportser.ReportPlayletComment(ctx, user, playletId, chapterId, text)
|
||||
} else if req.ObjectType == ObjectTypeVideo {
|
||||
reportser.ReportVideoComment(ctx, user, req.ObjectId, text)
|
||||
}
|
||||
// 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 || !isSupportedCommentType(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 || !isSupportedCommentType(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,
|
||||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||||
}, 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
|
||||
case ObjectTypeVideo:
|
||||
video, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: objectId,
|
||||
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询评论视频目标失败", log.Any("视频ID", objectId), log.E(err))
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "视频不存在或未上架", "")
|
||||
}
|
||||
return video.Title, 0, 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
|
||||
}
|
||||
case ObjectTypeVideo:
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: objectId,
|
||||
}
|
||||
if delta < 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Real.Comments] = bson.M{"$gt": 0}
|
||||
}
|
||||
if err := videoinformationmod.UpdateOneVideoInformation(ctx, filter, bson.M{
|
||||
"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Comments: delta},
|
||||
}); err != nil {
|
||||
if delta < 0 && err.Code == srvdef.ErrNoData {
|
||||
log.Warn("视频评论数已为0,跳过扣减", log.Any("视频ID", objectId))
|
||||
return 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 || !isSupportedCommentType(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, ObjectTypeVideo},
|
||||
},
|
||||
}, 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 isSupportedCommentType(objectType int64) bool {
|
||||
return isPlayletCommentType(objectType) || objectType == ObjectTypeVideo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user