Files
web_hgdjb_server/app/service/commentser/comment.go
T
rootandClaude Opus 4.7 cd8dea8787 同步 web_hga 的五项改造
- 移除 买点 (user acquisition tracking) 子系统:dataReport SDK、eventmod
  事件模型、reportdata 定时上报 job、reportser/srv_report 包装层,以及业务
  侧的 ~20 处 Report* 调用点;去掉 proto.ConfigInfo 的 ReportSDKConfig
  字段、DataReport yaml 配置块和 pubdef.TrackerAppId。
- 广告获取改走 versionUrl /api/stat/advitise/v2/get(appId=12),移除
  adcenter(Jt) 调用;新增 POST /api/app/ads/click 异步上报;
  proto.AdvitiseData 别名 proxy.AdvitiseData,AList/BList 改名
  AppInfoAList/AppInfoBList。
- web 后台登录取消谷歌验证码(srv_mwacc.AccLogin 去掉 code 参数),密码
  正确即发放 token。
- app 端取消注册/登录/找回密码/游客登录接口及密码、token 相关代码;鉴权
  中间件按 X-User-Agent 中的 devID 识别用户,首次访问自动建号,Redis 缓存
  10 分钟;修改资料不再支持改密码。
- app m3u8 ticket 校验失败不再降级到 fallback 路径,仍按原 m3u8Name 返回。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-11-07 11:22:25 +08:00

698 lines
24 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/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
}
}
// 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
}