package srv_commend import ( "context" "time" "videoweb/pub/common/log" "videoweb/pub/common/srvdef" "videoweb/pub/models/commentmod" "videoweb/pub/models/playletchaptermod" "videoweb/pub/models/playletmod" "videoweb/pub/mongos" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) const ( commentObjectTypePlaylet int64 = 10 // 短剧评论 commentObjectTypePlayletChapter int64 = 11 // 短剧剧集评论 commentReviewPass int = 1 // 审核通过 commentLevelFirst int64 = 1 // 一级评论 ) // SearchComandList 查询后台评论列表 func SearchComandList(ctx context.Context, param *CommendSearch) (*CommendSearchResp, *srvdef.SrvErr) { filter := bson.M{} opt := &options.FindOptions{} // objectId 只表示业务对象ID,必须配合 objectType 才能准确区分短剧评论和剧集评论,避免不同集合ID碰撞。 if param.ObjectId != nil { filter[commentmod.FN.Comment.ObjectId] = *param.ObjectId } if param.ObjectType != nil { filter[commentmod.FN.Comment.ObjectType] = *param.ObjectType } if param.UserId != nil && *param.UserId > 0 { filter[commentmod.FN.Comment.UserId] = *param.UserId } if param.Level != nil { filter[commentmod.FN.Comment.Level] = *param.Level } if param.UserName != nil { filter[commentmod.FN.Comment.UserName] = bson.M{"$regex": *param.UserName} } if param.Text != nil { filter[commentmod.FN.Comment.Text] = bson.M{"$regex": *param.Text} } if param.ReviewStatus != nil { filter[commentmod.FN.Comment.ReviewStatus] = *param.ReviewStatus } // 置顶评论优先展示,同一置顶级别下按发布时间倒序,保证后台审核和运营处理时优先看到最新评论。 sort := bson.D{ {commentmod.FN.Comment.TopRank, -1}, {commentmod.FN.Comment.CreatedAt, -1}, } opt.SetSort(sort) skip := (param.PageNum - 1) * param.PageSize opt.SetSkip(skip) opt.SetLimit(param.PageSize) total, serr := commentmod.CountComments(ctx, filter) if serr != nil { return nil, serr } if total == 0 { return &CommendSearchResp{}, nil } commends, serr := commentmod.FindManyComment(ctx, filter, opt) if serr != nil { return nil, serr } resp := &CommendSearchResp{ Total: total, List: make([]*PostCommentInfo, 0), } // countLike 是短剧评论点赞数的新字段,likes 保留旧字段兼容后台旧页面读取。 for _, commendInfo := range commends { tmp := &PostCommentInfo{ Id: commendInfo.Id, ObjectId: commendInfo.ObjectId, ObjectType: commendInfo.ObjectType, ObjectName: commendInfo.ObjectName, UserId: commendInfo.UserId, UserName: commendInfo.UserName, UserAvatar: commendInfo.UserAvatar, Level: commendInfo.Level, ParentsId: commendInfo.ParentsId, ReplyCommentId: commendInfo.ReplyCommentId, ReplyUserName: commendInfo.ReplyUserName, Text: commendInfo.Text, ReviewStatus: commendInfo.ReviewStatus, UpdatedAt: commendInfo.UpdatedAt, Ip: commendInfo.Ip, CreatedAt: commendInfo.CreatedAt, CountLike: commendInfo.CountLike, Likes: commendInfo.CountLike, } resp.List = append(resp.List, tmp) } return resp, nil } // DelCommend 删除一条评论 func DelCommend(ctx context.Context, commendId int64) *srvdef.SrvErr { // 先查出评论旧状态,再删除;后续扣减评论数需要依赖旧的 reviewStatus、level、objectType。 commendInfo, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId}) if serr != nil { return serr } _, serr = commentmod.DelOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId}) if serr != nil { return serr } // 删除后按当前已审核通过评论数重算计数,避免历史增量失败或重复操作导致 countComment 不准。 if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil { return serr } return nil } // DelCommend 删除用户所有评论 func DelUserCommend(ctx context.Context, ip string) *srvdef.SrvErr { // 批量删除前先查出全部评论,删除后才能按每条评论的旧状态补偿父评论或短剧/剧集评论数。 commendList, serr := commentmod.FindManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip}) if serr != nil { return serr } _, serr = commentmod.DelManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip}) if serr != nil { return serr } commendId := make([]int64, 0) // 删除用户全部评论后逐条重算受影响对象的计数;重复刷新同一对象只会 set 成相同值,不会重复扣减。 for _, commendInfo := range commendList { if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil { return serr } commendId = append(commendId, commendInfo.Id) } return nil } // AddPostComment 发布评论 func AddPostComment(ctx context.Context, req *CommendReplay) *srvdef.SrvErr { now := time.Now() if req.Score > 10 { log.Error("后台回复评论参数错误", log.Any("评分", req.Score)) return srvdef.New(srvdef.ErrParamError, "", "") } // 后台回复评论沿用旧的帖子评论逻辑,默认审核通过;短剧/剧集评论由 app 评论入口写入。 replyCommentId := int64(0) if req.ParentsId > 0 { replyCommentId = req.ReplyCommentId if replyCommentId <= 0 { replyCommentId = req.ParentsId } } commentInfo := &commentmod.CommentModel{ Id: mongos.Lid.Comment.Get(), ObjectId: req.ObjectId, UserName: req.UserName, ParentsId: req.ParentsId, ReplyCommentId: replyCommentId, Text: req.Text, Level: 1, UpdatedAt: now, CreatedAt: now, } if commentInfo.ParentsId > 0 { commentInfo.Level = 2 } else { commentInfo.Level = 1 } commentInfo.ReviewStatus = 1 // 管理端的默认审核通过 // 插入回复评论成功后,再异步增加父评论回复数,避免回复失败时污染父评论计数。 serr := commentmod.InsertOneComment(ctx, commentInfo) if serr != nil { log.Error("后台回复评论入库失败", log.Any("请求参数", req), log.Any("err", serr)) return serr } go func() { if req.ParentsId > 0 { filter := bson.M{ commentmod.FN.Comment.Id: req.ParentsId, } updateDoc := bson.M{ "$inc": bson.M{ commentmod.FN.Comment.CommentCount: 1, }, } serr = commentmod.UpdateOneComment(ctx, filter, updateDoc) if serr != nil { log.Error("后台回复评论更新父评论回复数失败", log.Any("请求参数", req), log.Any("err", serr)) return } } }() return nil } // 置顶评论 func TopPostComment(ctx context.Context, id int64) *srvdef.SrvErr { // 置顶排序采用递增 topRank,新的置顶评论排在当前最高 topRank 前面。 filter := bson.M{ commentmod.FN.Comment.Id: id, } comment, serr := commentmod.FindOneComment(ctx, filter) if serr != nil { return serr } // 获取当前对象下已置顶排序最高的一条评论,用于生成新的 topRank。 topRankComment, serr := getTopRankComment(ctx, comment.ObjectId) if serr != nil { return serr } if topRankComment.Id == id && topRankComment.TopRank != 0 { return nil } serr = commentmod.UpdateOneComment(ctx, filter, bson.M{ "$set": bson.M{ commentmod.FN.Comment.TopRank: topRankComment.TopRank + 1, }, }) if serr != nil { return serr } return nil } func getTopRankComment(ctx context.Context, objectId int64) (*commentmod.CommentModel, *srvdef.SrvErr) { // 只按 objectId 查是历史逻辑;短剧/剧集评论如果后续支持置顶,建议再补 objectType 避免ID碰撞。 filter := bson.M{ commentmod.FN.Comment.ObjectId: objectId, } opts := options.FindOne().SetSort(bson.M{ commentmod.FN.Comment.TopRank: -1, }) comment, serr := commentmod.FindOneComment(ctx, filter, opts) if serr != nil { return nil, serr } return comment, nil } func ReviewComment(ctx context.Context, id int64, reviewStatus int) *srvdef.SrvErr { // 审核前先读取评论旧状态,用旧状态和新状态判断是否需要同步评论计数。 comment, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: id}) if serr != nil { return serr } updateDoc := bson.M{ commentmod.FN.Comment.ReviewStatus: reviewStatus, } filter := bson.M{ commentmod.FN.Comment.Id: id, } serr = commentmod.UpdateOneComment(ctx, filter, bson.M{"$set": updateDoc}) if serr != nil { log.Error("审核评论失败", log.Any("评论ID", id), log.Any("审核状态", reviewStatus), log.Any("err", serr)) return serr } comment.ReviewStatus = reviewStatus // 审核后按当前已通过评论数重算,重复审核同一状态也能修正历史漏加或漏减的 countComment。 if serr = refreshCommentCountByComment(ctx, comment); serr != nil { return serr } return nil } // ReviewCommendBatch 批量更新 func ReviewCommendBatch(ctx context.Context, commendIds *[]int64, reviewStatus int) *srvdef.SrvErr { if commendIds == nil || len(*commendIds) == 0 { return srvdef.New(srvdef.ErrParamError, "评论ID不能为空", "") } // 批量审核也要先查旧数据,统一用每条评论的旧审核状态计算评论数增减。 commentList, serr := commentmod.FindManyComment(ctx, bson.M{ commentmod.FN.Comment.Id: bson.M{"$in": *commendIds}, }) if serr != nil { return serr } updateDoc := bson.M{ commentmod.FN.Comment.ReviewStatus: reviewStatus, } filter := bson.M{commentmod.FN.Comment.Id: bson.M{"$in": *commendIds}} serr = commentmod.UpdateManyComment(ctx, filter, bson.M{"$set": updateDoc}) if serr != nil { log.Error("批量审核评论失败", log.Any("评论ID集合", *commendIds), log.Any("审核状态", reviewStatus), log.Any("err", serr)) return serr } // 审核状态更新成功后再按对象重算计数,避免状态未更新成功时提前改动短剧/剧集评论数。 for _, comment := range commentList { if comment == nil { continue } if serr = refreshCommentCountByComment(ctx, comment); serr != nil { return serr } } return nil } func refreshCommentCountByComment(ctx context.Context, comment *commentmod.CommentModel) *srvdef.SrvErr { // 根据评论归属重算对应计数:二级评论刷新父评论回复数,一级短剧/剧集评论刷新业务对象评论数。 if comment == nil { return nil } if comment.Level > commentLevelFirst && comment.ParentsId > 0 { return refreshParentCommentCount(ctx, comment.ParentsId) } if isPlayletCommentType(comment.ObjectType) { return refreshCommentTargetCount(ctx, comment.ObjectType, comment.ObjectId) } return nil } func refreshParentCommentCount(ctx context.Context, parentId int64) *srvdef.SrvErr { // 父评论回复数以当前审核通过的子评论数量为准,直接 set 可以修正历史增量不一致问题。 if parentId <= 0 { return nil } count, serr := commentmod.CountComments(ctx, bson.M{ commentmod.FN.Comment.ParentsId: parentId, commentmod.FN.Comment.ReviewStatus: commentReviewPass, }) if serr != nil { log.Error("统计父评论回复数失败", log.Any("父评论ID", parentId), log.E(serr)) return serr } serr = commentmod.UpdateOneComment(ctx, bson.M{ commentmod.FN.Comment.Id: parentId, }, bson.M{ "$set": bson.M{commentmod.FN.Comment.CommentCount: count}, }) if serr != nil { if serr.Code == srvdef.ErrNoData { log.Warn("父评论不存在,跳过刷新回复数", log.Any("父评论ID", parentId), log.Any("回复数", count)) return nil } log.Error("刷新父评论回复数失败", log.Any("父评论ID", parentId), log.Any("回复数", count), log.E(serr)) return serr } return nil } func refreshCommentTargetCount(ctx context.Context, objectType, objectId int64) *srvdef.SrvErr { // 短剧/剧集评论数以当前审核通过的一级评论数量为准,审核通过、驳回和删除都走同一套重算逻辑。 count, serr := commentmod.CountComments(ctx, bson.M{ commentmod.FN.Comment.ObjectId: objectId, commentmod.FN.Comment.ObjectType: objectType, commentmod.FN.Comment.ReviewStatus: commentReviewPass, commentmod.FN.Comment.ParentsId: 0, }) if serr != nil { log.Error("统计短剧评论数失败", log.Any("对象类型", objectType), log.Any("对象ID", objectId), log.E(serr)) return serr } return setCommentTargetCount(ctx, objectType, objectId, count) } func setCommentTargetCount(ctx context.Context, objectType, objectId int64, count int64) *srvdef.SrvErr { // 根据对象类型把重算后的评论数写回短剧或剧集集合。 switch objectType { case commentObjectTypePlaylet: serr := playletmod.UpdateOnePlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: objectId, }, bson.M{ "$set": bson.M{playletmod.FN.Playlet.CountComment: count}, }) if serr != nil { log.Error("刷新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("评论数", count), log.E(serr)) return serr } case commentObjectTypePlayletChapter: serr := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{ playletchaptermod.FN.PlayletChapter.Id: objectId, }, bson.M{ "$set": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: count}, }) if serr != nil { log.Error("刷新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("评论数", count), log.E(serr)) return serr } } return nil } func isPlayletCommentType(objectType int64) bool { // 当前只有 10 和 11 两类评论会影响短剧业务计数,其他历史评论类型不参与本次同步逻辑。 return objectType == commentObjectTypePlaylet || objectType == commentObjectTypePlayletChapter }