Initial commit: web_hghl_server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 16:28:14 +08:00
co-authored by Claude Opus 4.7
commit 65fca60622
653 changed files with 135901 additions and 0 deletions
+506
View File
@@ -0,0 +1,506 @@
package commentser
import (
"context"
"encoding/json"
"strings"
"time"
"unicode/utf8"
"videoweb/app/api/proto"
"videoweb/app/service/configser"
"videoweb/app/service/moreser"
"videoweb/app/service/reportser"
"videoweb/pub/common/enum/redisconst"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/response"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"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/pubdef"
"videoweb/pub/redis"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
commentListLimit int64 = 200
subCommentListLimit int64 = 400
commentTreeDefaultPageSize int64 = 10
commentTreeMaxPageSize int64 = 100
commentTreeMaxDepth int = 5
)
type commentTreeNode struct {
info proto.CommentInfo
children []*commentTreeNode
}
// ListCommentTree 分页查询指定对象评论列表,并按父子关系组装树形结构
func ListCommentTree(ctx context.Context, userId int64, req *proto.CommentListReq) (*proto.CommentListResp, *srvdef.SrvErr) {
if req.ObjectId <= 0 {
return nil, srvdef.New(srvdef.ErrParamError, "对象ID不能为空")
}
if !pubdef.ValidCommentObjectType(req.ObjectType) {
return nil, srvdef.New(srvdef.ErrParamError, "评论类型有误")
}
if req.SortOrder != 0 && req.SortOrder != 1 && req.SortOrder != 2 {
return nil, srvdef.New(srvdef.ErrParamError, "排序方式有误")
}
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, commentTreeDefaultPageSize, commentTreeMaxPageSize)
filter := bson.M{
commentmod.FN.Comment.ObjectId: req.ObjectId,
commentmod.FN.Comment.ObjectType: req.ObjectType,
commentmod.FN.Comment.ReviewStatus: 1,
commentmod.FN.Comment.Level: 1,
}
total, err := commentmod.CountComments(ctx, filter)
if err != nil {
log.Warn("ListCommentTree.CountComments", log.Any("filter", filter), log.E(err))
return nil, err
}
resp := &proto.CommentListResp{BasePageResp: response.BasePageResp{
TotalCount: total,
PageSize: int(pageSize),
PageNum: int(pageNum),
List: make([]proto.CommentInfo, 0),
}}
if total == 0 {
return resp, nil
}
imgCdn := configser.ImageCdnFromCtx(ctx)
topList, err := commentmod.FindManyComment(ctx, filter, options.Find().
SetProjection(commentListProjection()).
SetSort(commentListSort(req.SortOrder)).
SetSkip((pageNum-1)*pageSize).
SetLimit(pageSize))
if err != nil {
log.Warn("ListCommentTree.FindManyComment", log.Any("filter", filter), log.E(err))
return nil, err
}
children, err := commentTreeChildren(ctx, req.ObjectId, req.ObjectType, collectCommentIds(topList))
if err != nil {
return nil, err
}
ids := append(collectCommentIds(topList), collectCommentIds(children)...)
likes, err := commentlikemod.FindByUserAndComments(ctx, userId, ids)
if err != nil {
return nil, err
}
liked := make(map[int64]bool, len(likes))
for _, like := range likes {
liked[like.CommentId] = true
}
resp.List = buildCommentTree(topList, children, imgCdn, liked)
return resp, nil
}
// commentListSort 热门按点赞数排序,默认按发布时间排序
func commentListSort(sortOrder int64) bson.D {
if sortOrder == 1 {
return bson.D{{Key: commentmod.FN.Comment.CountLike, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
}
return bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
}
// GetCommentList 查询指定对象的首屏评论列表,并缓存短时间内重复读取
func GetCommentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
cacheKey := redisconst.CommentListCacheKey(objectId, objectType)
cacheData, cacheErr := redis.Get(cacheKey)
if cacheErr != nil {
log.Warn("获取缓存失败", log.Any("cacheKey", cacheKey), log.E(cacheErr))
}
if cacheData != nil {
var commentListResp *proto.GetCommentListResp
if err := json.Unmarshal([]byte(*cacheData), &commentListResp); err != nil {
log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
} else if commentListResp != nil {
configser.RewriteImageCdn(ctx, commentListResp)
return commentListResp, nil
}
}
data, err := commentList(ctx, objectId, objectType)
if err != nil {
return nil, err
}
bytes, marshalErr := json.Marshal(data)
if marshalErr != nil {
log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(marshalErr))
configser.RewriteImageCdn(ctx, data)
return data, nil
}
if setErr := redis.Set(cacheKey, string(bytes), redisconst.CommentListKeyExpire); setErr != nil {
log.Warn("设置缓存失败", log.Any("cacheKey", cacheKey), log.E(setErr))
}
configser.RewriteImageCdn(ctx, data)
return data, nil
}
// commentList 查询顶级评论,并批量补齐二级回复,避免逐条查询子评论
func commentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
filter := bson.M{
commentmod.FN.Comment.ObjectId: objectId,
commentmod.FN.Comment.ObjectType: objectType,
commentmod.FN.Comment.ReviewStatus: 1,
commentmod.FN.Comment.Level: 1,
}
total, err := commentmod.CountComments(ctx, filter)
if err != nil {
log.Warn("commentList.CountComments", log.Any("filter", filter), log.E(err))
return nil, err
}
resp := &proto.GetCommentListResp{
ObjectId: objectId,
Total: total,
List: make([]proto.CommentInfo, 0),
}
if total == 0 {
return resp, nil
}
imgCdn := configser.GetConfigInfo().ImgCdn
list, err := commentmod.FindManyComment(ctx, filter, options.Find().
SetProjection(commentListProjection()).
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
SetLimit(commentListLimit))
if err != nil {
log.Warn("commentList.FindManyComment", log.Any("filter", filter), log.E(err))
return nil, err
}
parentIds := make([]int64, 0, len(list))
for _, item := range list {
parentIds = append(parentIds, item.Id)
}
subCommentMap, err := subCommentList(ctx, parentIds, imgCdn)
if err != nil {
return nil, err
}
for _, item := range list {
info := commentInfoData(item, imgCdn)
info.SubComment = subCommentMap[item.Id]
resp.List = append(resp.List, info)
}
return resp, nil
}
// subCommentList 批量查询二级评论,并按父评论ID分组
func subCommentList(ctx context.Context, parentIds []int64, imgCdn string) (map[int64][]proto.CommentInfo, *srvdef.SrvErr) {
data := make(map[int64][]proto.CommentInfo, len(parentIds))
if len(parentIds) == 0 {
return data, nil
}
list, err := commentmod.FindManyComment(ctx, bson.M{
commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
commentmod.FN.Comment.ReviewStatus: 1,
}, options.Find().
SetProjection(commentListProjection()).
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
SetLimit(subCommentListLimit))
if err != nil {
log.Warn("subCommentList.FindManyComment", log.Any("parentIds", parentIds), log.E(err))
return nil, err
}
for _, item := range list {
data[item.ParentsId] = append(data[item.ParentsId], commentInfoData(item, imgCdn))
}
return data, nil
}
// commentTreeChildren 按层批量查询当前页一级评论的子孙评论,避免 N+1 查询
func commentTreeChildren(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
data := make(commentmod.CommentModelList, 0)
currentParentIds := uniqueCommentIds(parentIds)
if len(currentParentIds) == 0 {
return data, nil
}
seenIds := make(map[int64]struct{}, len(currentParentIds))
for _, id := range currentParentIds {
seenIds[id] = struct{}{}
}
for depth := 0; len(currentParentIds) > 0 && depth < commentTreeMaxDepth; depth++ {
filter := bson.M{
commentmod.FN.Comment.ObjectId: objectId,
commentmod.FN.Comment.ObjectType: objectType,
commentmod.FN.Comment.ParentsId: bson.M{"$in": currentParentIds},
commentmod.FN.Comment.ReviewStatus: 1,
}
list, err := commentmod.FindManyComment(ctx, filter, options.Find().
SetProjection(commentListProjection()).
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}))
if err != nil {
log.Warn("commentTreeChildren.FindManyComment", log.Any("filter", filter), log.E(err))
return nil, err
}
nextParentIds := make([]int64, 0, len(list))
for _, item := range list {
if item == nil {
continue
}
if _, ok := seenIds[item.Id]; ok {
continue
}
seenIds[item.Id] = struct{}{}
data = append(data, item)
nextParentIds = append(nextParentIds, item.Id)
}
currentParentIds = nextParentIds
}
return data, nil
}
// buildCommentTree 将一级评论和子孙评论按 parentsId 组装为树形结构
func buildCommentTree(topList commentmod.CommentModelList, children commentmod.CommentModelList, imgCdn string, liked map[int64]bool) []proto.CommentInfo {
nodes := make(map[int64]*commentTreeNode, len(topList)+len(children))
roots := make([]*commentTreeNode, 0, len(topList))
for _, item := range topList {
if item == nil {
continue
}
info := commentInfoData(item, imgCdn)
info.IsLike = liked[item.Id]
node := &commentTreeNode{info: info}
nodes[item.Id] = node
roots = append(roots, node)
}
for _, item := range children {
if item == nil {
continue
}
info := commentInfoData(item, imgCdn)
info.IsLike = liked[item.Id]
nodes[item.Id] = &commentTreeNode{info: info}
}
for _, item := range children {
if item == nil {
continue
}
parentNode, ok := nodes[item.ParentsId]
if !ok {
continue
}
if node, ok := nodes[item.Id]; ok {
parentNode.children = append(parentNode.children, node)
}
}
return commentTreeNodesToInfos(roots)
}
// commentTreeNodesToInfos 递归输出评论树响应
func commentTreeNodesToInfos(nodes []*commentTreeNode) []proto.CommentInfo {
list := make([]proto.CommentInfo, 0, len(nodes))
for _, node := range nodes {
if node == nil {
continue
}
info := node.info
info.SubComment = commentTreeNodesToInfos(node.children)
list = append(list, info)
}
return list
}
// collectCommentIds 收集评论ID集合
func collectCommentIds(list commentmod.CommentModelList) []int64 {
ids := make([]int64, 0, len(list))
for _, item := range list {
if item == nil || item.Id <= 0 {
continue
}
ids = append(ids, item.Id)
}
return ids
}
// uniqueCommentIds 去重评论ID集合
func uniqueCommentIds(ids []int64) []int64 {
seen := make(map[int64]struct{}, len(ids))
result := make([]int64, 0, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
// commentInfoData 将评论模型转换为 APP 评论响应结构
func commentInfoData(item *commentmod.CommentModel, imgCdn string) proto.CommentInfo {
return proto.CommentInfo{
Id: item.Id,
ObjectId: item.ObjectId,
UserName: item.UserName,
Avatar: netutil.MergeURL(imgCdn, item.Avatar),
Level: item.Level,
ParentsId: item.ParentsId,
ReplyUserName: item.ReplyUserName,
Text: item.Text,
CommentCount: item.CommentCount,
CountLike: item.CountLike,
SubComment: make([]proto.CommentInfo, 0),
CreatedAt: item.CreatedAt,
}
}
// commentListProjection 限定评论列表字段,减少详情页评论查询的数据传输
func commentListProjection() bson.M {
return bson.M{
commentmod.FN.Comment.Id: 1,
commentmod.FN.Comment.ObjectId: 1,
commentmod.FN.Comment.UserName: 1,
commentmod.FN.Comment.Avatar: 1,
commentmod.FN.Comment.Level: 1,
commentmod.FN.Comment.ParentsId: 1,
commentmod.FN.Comment.ReplyUserName: 1,
commentmod.FN.Comment.Text: 1,
commentmod.FN.Comment.CommentCount: 1,
commentmod.FN.Comment.CountLike: 1,
commentmod.FN.Comment.CreatedAt: 1,
}
}
func SendComment(ctx context.Context, ip string, userId int64, req *proto.CommentSendReq, ua useragent.UA) *srvdef.SrvErr {
if req == nil || req.ObjectId <= 0 || req.ParentsId < 0 || !pubdef.ValidCommentObjectType(req.ObjectType) {
return srvdef.New(srvdef.ErrParamError, "评论参数有误")
}
req.Text = strings.TrimSpace(req.Text)
if req.Text == "" {
return srvdef.New(srvdef.ErrParamError, "评论内容不能为空")
}
if utf8.RuneCountInString(req.Text) > 200 {
return srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字")
}
curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
if err != nil {
log.Error("SendComment redis incr failed", log.E(err))
}
//15秒超3评论提示频繁操作
if curValue > 1 {
log.Error("SendComment user comment too frequently!",
log.Any("ip", ip), log.Any("req", req), log.E(err))
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
}
//操作频繁度判断
dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
if err != nil {
log.Error("SendComment redis incr failed", log.E(err))
}
// 每天20条
if dayCurValue > 50 {
log.Error("SendComment user comment too frequently!",
log.Any("ip", ip), log.Any("req", req), log.E(err))
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
}
if !pubdef.ValidCommentObjectType(req.ObjectType) {
return srvdef.New(srvdef.ErrParamError, "评论类型有误")
}
objectName := pubdef.CommentObjectTypeName(req.ObjectType)
var videoInfo2 videoinformationmod.VideoInformation
switch req.ObjectType {
case pubdef.CommentObjectTypeVideo:
videoInfo, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: req.ObjectId,
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
})
if err1 != nil {
log.Warn("SendComment.FindOneVideoInformation error", log.Any("objectId", req.ObjectId), log.E(err1))
return err1
}
videoInfo2 = *videoInfo
case pubdef.CommentObjectTypePlaylet:
_, err1 := playletmod.FindOnePlaylet(ctx, bson.M{
playletmod.FN.Playlet.Id: req.ObjectId,
playletmod.FN.Playlet.Status: playletmod.StatusUp,
})
if err1 != nil {
log.Warn("SendComment.FindOnePlaylet error", log.Any("objectId", req.ObjectId), log.E(err1))
return err1
}
case pubdef.CommentObjectTypePlayletChapter:
chapter, err1 := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
playletchaptermod.FN.PlayletChapter.Id: req.ObjectId,
})
if err1 != nil {
return err1
}
_, err1 = playletmod.FindOnePlaylet(ctx, bson.M{
playletmod.FN.Playlet.Id: chapter.PlayletId,
playletmod.FN.Playlet.Status: playletmod.StatusUp,
})
if err1 != nil {
return err1
}
}
// 通过 token 中的 userId 查询用户信息,补充用户名和头像
userInfo, err1 := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
if err1 != nil {
log.Warn("SendComment.FindOneUser error", log.Any("userId", userId), log.E(err1))
return err1
}
userName := userInfo.NickName
if userName == "" {
userName = userInfo.AccountNo
}
level := int64(1)
replyUserName := ""
if req.ParentsId > 0 {
// 回复评论
parentsInfo, err1 := 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.ReviewStatus: 1,
})
if err1 != nil {
log.Warn("SendComment.FindOneComment error", log.Any("objectId", req.ObjectId), log.Any("parentsId", req.ParentsId), log.E(err1))
return err1
}
level = parentsInfo.Level + 1
if level > int64(commentTreeMaxDepth+1) {
return srvdef.New(srvdef.ErrParamError, "回复层级过深")
}
replyUserName = parentsInfo.UserName
}
info := commentmod.CommentModel{
Id: mongos.Lid.Comment.Get(),
Ip: ip,
ObjectId: req.ObjectId,
ObjectName: objectName,
ObjectType: req.ObjectType,
UserId: userId,
UserName: userName,
Avatar: userInfo.Avatar,
Level: level,
ParentsId: req.ParentsId,
ReplyUserName: replyUserName,
Text: req.Text,
CommentCount: 0,
ReviewStatus: 0,
UpdatedAt: time.Now(),
CreatedAt: time.Now(),
}
if err1 := commentmod.InsertOneComment(ctx, &info); err1 != nil {
return err1
}
var reportErr error
switch req.ObjectType {
case pubdef.CommentObjectTypeVideo:
reportErr = reportser.ReportVideoComment(userId, ua, ip, &videoInfo2, req.Text)
}
if reportErr != nil {
log.Warn("SendComment.ReportCommentEvent error", log.Any("objectType", req.ObjectType), log.Any("objectId", req.ObjectId), log.E(reportErr))
}
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package commentser
import (
"context"
"videoweb/app/api/proto"
"videoweb/pub/common/enum/redisconst"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/commentlikemod"
"videoweb/pub/models/commentmod"
"videoweb/pub/redis"
"go.mongodb.org/mongo-driver/bson"
)
// SetCommentLike 设置评论点赞状态
func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
if userId <= 0 || req == nil || req.CommentId <= 0 || req.IsLike == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
comment, err := commentmod.FindOneComment(ctx, bson.M{
commentmod.FN.Comment.Id: req.CommentId,
commentmod.FN.Comment.ReviewStatus: 1,
})
if err != nil {
return nil, err
}
var changed bool
if *req.IsLike {
changed, err = commentlikemod.Add(ctx, userId, req.CommentId)
} else {
changed, err = commentlikemod.Remove(ctx, userId, req.CommentId)
}
if err != nil {
return nil, err
}
count := comment.CountLike
if changed {
delta := int64(1)
if !*req.IsLike {
delta = -1
}
count, err = commentmod.ChangeLikeCount(ctx, req.CommentId, delta)
if err != nil {
if *req.IsLike {
_, _ = commentlikemod.Remove(ctx, userId, req.CommentId)
} else {
_, _ = commentlikemod.Add(ctx, userId, req.CommentId)
}
return nil, err
}
} else {
current, findErr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: req.CommentId})
if findErr != nil {
return nil, findErr
}
count = current.CountLike
}
if cacheErr := redis.Del(redisconst.CommentListCacheKey(comment.ObjectId, comment.ObjectType)); cacheErr != nil {
log.Warn("SetCommentLike clear cache", log.E(cacheErr))
}
return &proto.CommentLikeSetResp{CommentId: req.CommentId, IsLike: *req.IsLike, CountLike: count}, nil
}