feat: 添加评论功能的客户端信息显示选项,更新相关接口和组件

This commit is contained in:
2025-09-13 16:04:09 +08:00
parent 011dc298c2
commit 2d0e1a46e2
10 changed files with 279 additions and 234 deletions

View File

@ -1,47 +1,48 @@
package dto
type CommentDto struct {
ID uint `json:"id"`
TargetID uint `json:"target_id"`
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
Content string `json:"content"`
ReplyID uint `json:"reply_id"` // 回复的评论ID
Depth int `json:"depth"` // 评论的层级深度
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
User UserDto `json:"user"` // 评论的
ReplyCount uint64 `json:"reply_count"` // 回复数量
LikeCount uint64 `json:"like_count"` // 点赞数量
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
IsPrivate bool `json:"is_private"`
Location string `json:"location"` // 用户位置基于IP
OS string `json:"os"` // 用户操作系统基于User-Agent
Browser string `json:"browser"` // 用户浏览器基于User-Agent
ID uint `json:"id"`
TargetID uint `json:"target_id"`
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
Content string `json:"content"`
ReplyID uint `json:"reply_id"` // 回复的评论ID
Depth int `json:"depth"` // 评论的层级深度
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
User UserDto `json:"user"` // 评论的
ReplyCount uint64 `json:"reply_count"` // 回复数量
LikeCount uint64 `json:"like_count"` // 点赞数量
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
IsPrivate bool `json:"is_private"`
Location string `json:"location"` // 用户位置基于IP
OS string `json:"os"` // 用户操作系统基于User-Agent
Browser string `json:"browser"` // 用户浏览器基于User-Agent
}
type CreateCommentReq struct {
TargetID uint `json:"target_id" binding:"required"` // 目标ID
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
Content string `json:"content" binding:"required"` // 评论内容
ReplyID uint `json:"reply_id"` // 回复的评论ID
IsPrivate bool `json:"is_private"` // 是否私密评论默认false
RemoteAddr string `json:"remote_addr"` // 远程地址
UserAgent string `json:"user_agent"` // 用户代理
TargetID uint `json:"target_id" binding:"required"` // 目标ID
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
Content string `json:"content" binding:"required"` // 评论内容
ReplyID uint `json:"reply_id"` // 回复的评论ID
IsPrivate bool `json:"is_private"` // 是否私密评论默认false
RemoteAddr string `json:"remote_addr"` // 远程地址
UserAgent string `json:"user_agent"` // 用户代理
ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息
}
type UpdateCommentReq struct {
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
Content string `json:"content" binding:"required"` // 评论内容
IsPrivate bool `json:"is_private"` // 是否私密
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
Content string `json:"content" binding:"required"` // 评论内容
IsPrivate bool `json:"is_private"` // 是否私密
}
type GetCommentListReq struct {
TargetID uint `json:"target_id" binding:"required"`
TargetType string `json:"target_type" binding:"required"`
CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论
OrderBy string `json:"order_by"` // 排序方式
Page uint64 `json:"page"` // 页码
Size uint64 `json:"size"`
Desc bool `json:"desc"`
Depth int `json:"depth"` // 评论的层级深度
TargetID uint `json:"target_id" binding:"required"`
TargetType string `json:"target_type" binding:"required"`
CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论
OrderBy string `json:"order_by"` // 排序方式
Page uint64 `json:"page"` // 页码
Size uint64 `json:"size"`
Desc bool `json:"desc"`
Depth int `json:"depth"` // 评论的层级深度
}

View File

@ -7,19 +7,20 @@ import (
type Comment struct {
gorm.Model
UserID uint `gorm:"index"` // 评论的用户ID
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
TargetID uint `gorm:"index"` // 目标ID
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
ReplyID uint `gorm:"index"` // 回复的评论ID
Content string `gorm:"type:text"` // 评论内容
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
RemoteAddr string `gorm:"type:text"` // 远程地址
UserAgent string `gorm:"type:text"`
Location string `gorm:"type:text"` // 用户位置基于IP
LikeCount uint64
CommentCount uint64
UserID uint `gorm:"index"` // 评论的用户ID
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
TargetID uint `gorm:"index"` // 目标ID
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
ReplyID uint `gorm:"index"` // 回复的评论ID
Content string `gorm:"type:text"` // 评论内容
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
RemoteAddr string `gorm:"type:text"` // 远程地址
UserAgent string `gorm:"type:text"`
Location string `gorm:"type:text"` // 用户位置基于IP
LikeCount uint64
CommentCount uint64
ShowClientInfo bool `gorm:"default:false"` // 是否显示客户端信息
}
func (c *Comment) AfterCreate(tx *gorm.DB) (err error) {

View File

@ -121,6 +121,7 @@ func (cr *CommentRepo) CreateComment(comment *model.Comment) (uint, error) {
})
return commentID, err
}
func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
if comment.ID == 0 {
return errs.New(http.StatusBadRequest, "invalid comment ID", nil)

View File

@ -1,184 +1,213 @@
package service
import (
"context"
"strconv"
"context"
"strconv"
"github.com/snowykami/neo-blog/pkg/constant"
"github.com/snowykami/neo-blog/pkg/utils"
"github.com/snowykami/neo-blog/pkg/constant"
"github.com/snowykami/neo-blog/pkg/utils"
"github.com/snowykami/neo-blog/internal/ctxutils"
"github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/errs"
"github.com/snowykami/neo-blog/internal/ctxutils"
"github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/errs"
)
type CommentService struct{}
func NewCommentService() *CommentService {
return &CommentService{}
return &CommentService{}
}
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) (uint, error) {
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return 0, errs.ErrUnauthorized
}
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return 0, errs.ErrUnauthorized
}
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
if err != nil {
return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err)
}
return 0, errs.ErrBadRequest
}
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
if err != nil {
return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err)
}
return 0, errs.ErrBadRequest
}
comment := &model.Comment{
Content: req.Content,
ReplyID: req.ReplyID,
TargetID: req.TargetID,
TargetType: req.TargetType,
UserID: currentUser.ID,
IsPrivate: req.IsPrivate,
RemoteAddr: req.RemoteAddr,
UserAgent: req.UserAgent,
}
comment := &model.Comment{
Content: req.Content,
ReplyID: req.ReplyID,
TargetID: req.TargetID,
TargetType: req.TargetType,
UserID: currentUser.ID,
IsPrivate: req.IsPrivate,
RemoteAddr: req.RemoteAddr,
UserAgent: req.UserAgent,
ShowClientInfo: req.ShowClientInfo,
}
commentID, err := repo.Comment.CreateComment(comment)
commentID, err := repo.Comment.CreateComment(comment)
if err != nil {
return 0, err
}
if err != nil {
return 0, err
}
return commentID, nil
return commentID, nil
}
func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error {
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return errs.ErrUnauthorized
}
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return errs.ErrUnauthorized
}
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
if err != nil {
return err
}
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
if err != nil {
return err
}
if currentUser.ID != comment.UserID {
return errs.ErrForbidden
}
if currentUser.ID != comment.UserID {
return errs.ErrForbidden
}
comment.Content = req.Content
comment.IsPrivate = req.IsPrivate
comment.Content = req.Content
comment.IsPrivate = req.IsPrivate
err = repo.Comment.UpdateComment(comment)
err = repo.Comment.UpdateComment(comment)
if err != nil {
return err
}
if err != nil {
return err
}
return nil
return nil
}
func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error {
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return errs.ErrUnauthorized
}
if commentID == "" {
return errs.ErrBadRequest
}
currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok {
return errs.ErrUnauthorized
}
if commentID == "" {
return errs.ErrBadRequest
}
comment, err := repo.Comment.GetComment(commentID)
if err != nil {
return errs.New(errs.ErrNotFound.Code, "comment not found", err)
}
comment, err := repo.Comment.GetComment(commentID)
if err != nil {
return errs.New(errs.ErrNotFound.Code, "comment not found", err)
}
if comment.UserID != currentUser.ID {
return errs.ErrForbidden
}
if comment.UserID != currentUser.ID {
return errs.ErrForbidden
}
if err := repo.Comment.DeleteComment(commentID); err != nil {
return err
}
if err := repo.Comment.DeleteComment(commentID); err != nil {
return err
}
return nil
return nil
}
func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dto.CommentDto, error) {
comment, err := repo.Comment.GetComment(commentID)
comment, err := repo.Comment.GetComment(commentID)
if err != nil {
return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err)
}
if err != nil {
return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err)
}
commentDto := dto.CommentDto{
ID: comment.ID,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
Content: comment.Content,
ReplyID: comment.ReplyID,
Depth: comment.Depth,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
User: comment.User.ToDto(),
}
// TODO: 返回更多字段
currentUserID := uint(0)
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
currentUserID = currentUser.ID
}
if comment.IsPrivate && currentUserID != comment.UserID {
return nil, errs.ErrForbidden
}
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
if !comment.ShowClientInfo {
comment.Location = ""
ua.OS = ""
ua.OSVersion = ""
ua.Browser = ""
ua.BrowserVer = ""
}
return &commentDto, err
commentDto := dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
}
return &commentDto, err
}
func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) {
currentUserID := uint(0)
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
currentUserID = currentUser.ID
}
comments, err := repo.Comment.ListComments(currentUserID, req.TargetID, req.CommentID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc, req.Depth)
if err != nil {
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
}
commentDtos := make([]dto.CommentDto, 0)
for _, comment := range comments {
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
commentDto := dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
}
commentDtos = append(commentDtos, commentDto)
}
return commentDtos, nil
currentUserID := uint(0)
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
currentUserID = currentUser.ID
}
comments, err := repo.Comment.ListComments(currentUserID, req.TargetID, req.CommentID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc, req.Depth)
if err != nil {
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
}
commentDtos := make([]dto.CommentDto, 0)
for _, comment := range comments {
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
commentDto := dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
}
if !comment.ShowClientInfo {
commentDto.Location = ""
commentDto.OS = ""
commentDto.Browser = ""
}
commentDtos = append(commentDtos, commentDto)
}
return commentDtos, nil
}
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
switch targetType {
case constant.TargetTypePost:
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
}
default:
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
}
return true, nil
switch targetType {
case constant.TargetTypePost:
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
}
default:
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
}
return true, nil
}