Initial commit

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 10:47:18 +08:00
co-authored by Claude Opus 4.7
commit 79ac89b458
708 changed files with 151634 additions and 0 deletions
+429
View File
@@ -0,0 +1,429 @@
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/models/videoinformationmod"
"videoweb/pub/mongos"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
commentObjectTypePlaylet int64 = 10 // 短剧评论
commentObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
commentObjectTypeVideo int64 = 20 // 视频评论
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.ObjectName != nil {
filter[commentmod.FN.Comment.ObjectName] = bson.M{"$regex": *param.ObjectName}
}
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 isSupportedCommentType(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
}
case commentObjectTypeVideo:
serr := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: objectId,
}, bson.M{
"$set": bson.M{videoinformationmod.FN.VideoInformation.Real.Comments: 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 {
return objectType == commentObjectTypePlaylet || objectType == commentObjectTypePlayletChapter
}
func isSupportedCommentType(objectType int64) bool {
return isPlayletCommentType(objectType) || objectType == commentObjectTypeVideo
}
+80
View File
@@ -0,0 +1,80 @@
package srv_commend
import (
"time"
"videoweb/pub/pubdef"
)
// CommendSearch 查询评论
type CommendSearch struct {
ObjectId *int64 `json:"objectId" bson:"objectId"` // 对象ID
ObjectType *int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集 20-视频
ObjectName *string `json:"objectName" bson:"objectName"` // 对象名(模糊)
UserId *int64 `json:"userId"` // 评论用户ID
UserName *string `json:"userName" bson:"userName"` // 用户名称
Level *int64 `json:"level" bson:"level"` // 评论级别
Text *string `json:"text" bson:"text"` // 评论内容
ReviewStatus *int `json:"reviewStatus"`
PageNum int64 `json:"pageNum"`
PageSize int64 `json:"pageSize"`
}
type PostCommentInfo struct {
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
Ip string `json:"ip" bson:"ip"` // 评论IP
ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
ObjectType int64 `json:"objectType" bson:"objectType"` // 对象类型 10-短剧 11-短剧剧集 20-视频
ObjectName string `json:"objectName" bson:"objectName"` // 对象名
UserId int64 `json:"userId" bson:"userId"` // 评论用户ID
UserName string `json:"userName" bson:"userName"` // 用户名称
UserAvatar string `json:"userAvatar" bson:"userAvatar"` // 评论用户头像
Level int64 `json:"level" bson:"level"` // 评论级别
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
ReplyCommentId int64 `json:"replyCommentId" bson:"replyCommentId"` // 被回复评论ID
ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
Text string `json:"text" bson:"text"` // 评论内容
ReviewStatus int `json:"reviewStatus" bson:"reviewStatus"` // 审核状态 0 未审核 1审核成功 2 审核失败
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排序
Likes int64 `json:"likes" bson:"likes"` // 点赞数
CountLike int64 `json:"countLike" bson:"countLike"` // 评论点赞数
Status pubdef.UserStatus `json:"status" bson:"status"` // 评论用户的状态
}
type CommendSearchResp struct {
Total int64 `json:"total"`
List []*PostCommentInfo `json:"list"`
}
type CommendDelete struct {
Id *int64 `json:"id" bson:"_id" binding:"required"` // Id
}
type CommendReplay struct {
ObjectId int64 `json:"objectId"` // 帖子ID
UserName string `json:"userName"` // 用户名
ParentsId int64 `json:"parentsId"` // 一级评论ID
ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID
Text string `json:"text"` // 评论内容
Score int64 `json:"score"` // 评分
}
type CommendUpdate struct {
Score *int64 `json:"score" bson:"score,omitempty"` // 评分
Likes *int64 `json:"likes" bson:"likes,omitempty"` // 点赞数量
}
type CommendUpdateReq struct {
Param CommendUpdate `json:"param" bson:"param"` // 参数
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
}
type CommendTopReq struct {
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
}
type CommendReviewReq struct {
Id int64 `json:"id"`
ReviewStatus int `json:"reviewStatus"`
}